/**
 * OpenRouter AI Service
 *
 * Activation:
 *   VITE_AI_PROVIDER=openrouter
 *   VITE_OPENROUTER_API_KEY=<your-key>
 *   VITE_OPENROUTER_MODEL=anthropic/claude-3-5-haiku  (optional, default shown)
 *
 * Every method follows this contract:
 *   1. No API key        → warn + MockAIService fallback (fallbackUsed: true)
 *   2. HTTP error        → error log + MockAIService fallback
 *   3. JSON parse fail   → warn + MockAIService fallback
 *   4. Zod schema fail   → warn + MockAIService fallback  ← NEW
 *   5. Success (full AI) → AI response, source: 'ai', validationPassed: true
 *   6. Hybrid            → source: 'hybrid', documented per-method
 *
 * No invalid data ever reaches the UI.
 */
import type { CreateNeedInput } from '../../../domain/need'
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../../../domain/needBuilder'
import type { UnifiedMatchResult } from '../../../domain/unifiedResult'
import type { AssetType } from '../../../domain/enums'
import type {
  IAIService,
  AIResponse,
  AIProvenance,
  DecisionBrief,
  ComparisonSummary,
  CriteriaExtractionResult,
  OfferEmailPayload,
  MatchExplanationInput,
  MatchExplanation,
  TradeOffInput,
  TradeOffSummary,
  DataQualityInput,
  DataQualitySummary,
  MarketSignalClassification,
} from '../IAIService'
import { ServiceErrorCode } from '../../types'
import { AppError } from '../../errors'
import { aiTraceStore, provenanceToStatus } from '../tracing'
import type { AITraceErrorType, AITraceValidationStatus } from '../tracing'
import {
  NeedParsingResponseSchema,
  FollowUpQuestionsResponseSchema,
  TradeOffSummaryResponseSchema,
  CompareSummaryResponseSchema,
  DecisionBriefResponseSchema,
  DataQualitySummaryResponseSchema,
  MarketSignalClassificationResponseSchema,
  OfferEmailResponseSchema,
  validateAIResponse,
} from '../schemas'
import { buildNeedParsingPrompt } from '../prompts/needParsingPrompt'
import { buildFollowUpQuestionsPrompt } from '../prompts/followUpQuestionsPrompt'
import { buildMatchExplanationPrompt } from '../prompts/matchExplanationPrompt'
import { buildTradeOffPrompt } from '../prompts/tradeOffPrompt'
import { buildCompareSummaryPrompt } from '../prompts/compareSummaryPrompt'
import { buildDecisionBriefPrompt } from '../prompts/decisionBriefPrompt'
import { buildDataQualityPrompt } from '../prompts/dataQualityPrompt'
import { buildMarketSignalPrompt } from '../prompts/marketSignalPrompt'
import { MockAIService } from '../mock/MockAIService'

// ── Config ────────────────────────────────────────────────────────────────────

const API_BASE       = 'https://openrouter.ai/api/v1'
const DEFAULT_MODEL  = 'anthropic/claude-3-5-haiku'
const PROMPT_VERSION = 'v1.1'
const SCHEMA_VERSION = 'v1.0'

interface OpenRouterConfig {
  apiKey: string
  model: string
}

function getConfig(): OpenRouterConfig | null {
  const apiKey = import.meta.env.VITE_OPENROUTER_API_KEY as string | undefined
  if (!apiKey) return null
  return {
    apiKey,
    model: (import.meta.env.VITE_OPENROUTER_MODEL as string | undefined) ?? DEFAULT_MODEL,
  }
}

function makeProvenance(
  config: OpenRouterConfig,
  source: AIProvenance['source'],
  fallbackUsed: boolean,
  validationPassed: boolean,
  extras: { fallbackReason?: string } = {},
): AIProvenance {
  return {
    provider: 'openrouter',
    model: config.model,
    generatedAt: new Date().toISOString(),
    promptVersion: PROMPT_VERSION,
    schemaVersion: SCHEMA_VERSION,
    source,
    fallbackUsed,
    validationPassed,
    traceId: crypto.randomUUID(),
    fallbackReason: extras.fallbackReason,
  }
}

// ── HTTP helper ───────────────────────────────────────────────────────────────

async function chat(config: OpenRouterConfig, system: string, user: string): Promise<string> {
  const res = await fetch(`${API_BASE}/chat/completions`, {
    method: 'POST',
    headers: {
      'Authorization':  `Bearer ${config.apiKey}`,
      'Content-Type':   'application/json',
      'HTTP-Referer':   window.location.origin,
    },
    body: JSON.stringify({
      model: config.model,
      messages: [
        { role: 'system', content: system },
        { role: 'user',   content: user   },
      ],
    }),
  })
  if (!res.ok) {
    const body = await res.text()
    throw new AppError({
      code:    ServiceErrorCode.AI_GENERATION_FAILED,
      message: `OpenRouter error ${res.status}: ${body}`,
    })
  }
  const json = await res.json() as { choices: Array<{ message: { content: string } }> }
  return json.choices[0]?.message?.content ?? ''
}

// ── JSON extraction ───────────────────────────────────────────────────────────

function extractJSON<T>(raw: string): T | null {
  const fenced    = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)
  const candidate = fenced ? fenced[1] : raw.match(/([\[{][\s\S]*[\]}])/)?.[1] ?? raw
  try {
    return JSON.parse(candidate) as T
  } catch {
    return null
  }
}

// ── ParseNeed helpers ─────────────────────────────────────────────────────────

type RawNeedParseAI = {
  assetType?: string | null
  areaRange?: { min: number; max: number } | null
  preferredLocations?: string[]
  budgetRange?: { maxPerSqm: number; currency: string } | null
  timing?: { earliestMoveIn: string; latestMoveIn?: string; flexibleTiming: boolean } | null
  mustHaveCriteria?: string[]
  missingFields?: string[]
  assumptions?: string[]
}

function followUpForField(field: string): string {
  const MAP: Record<string, string> = {
    assetType:          'Welchen Nutzungstyp suchen Sie (Büro, Retail, Logistik, Produktion)?',
    areaRange:          'Welche Fläche benötigen Sie (min–max in m²)?',
    preferredLocations: 'In welchen Städten oder Regionen suchen Sie?',
    budgetRange:        'Was ist Ihr maximales Budget pro m² und Jahr?',
    timing:             'Wann möchten Sie spätestens einziehen?',
    mustHaveCriteria:   'Haben Sie zwingende Anforderungen (ÖV-Anbindung, Parkplätze, Laderampe)?',
  }
  return MAP[field] ?? `Können Sie "${field}" präzisieren?`
}

function defaultSuggestedWeights(): Record<string, number> {
  return {
    area: 0.25, location: 0.20, budget: 0.20, timing: 0.15,
    prestige: 0.05, accessibility: 0.05, expansionPotential: 0.02,
    flexibility: 0.02, visibility: 0.02, footfall: 0.01, talentAccess: 0.01,
    esg: 0.01, taxEnvironment: 0.01,
  }
}

// ── Fallback wrapper ──────────────────────────────────────────────────────────

type FallbackFn<T> = () => Promise<AIResponse<T>>

async function withFallback<T>(
  label: string,
  fn: (config: OpenRouterConfig) => Promise<AIResponse<T>>,
  fallback: FallbackFn<T>,
  inputSizeChars?: number,
): Promise<AIResponse<T>> {
  const config = getConfig()
  const startMs = Date.now()
  const callId = crypto.randomUUID()

  if (!config) {
    console.warn(`[OpenRouterAIService] ${label}: no API key — using MockAIService`)
    const result = await fallback()
    const latencyMs = Date.now() - startMs
    const provenance: AIProvenance = {
      ...result.provenance,
      fallbackUsed: true,
      traceId: callId,
      fallbackReason: 'no_api_key',
      schemaVersion: SCHEMA_VERSION,
      latencyMs,
    }
    aiTraceStore.add({
      id: callId,
      method: label,
      provider: 'openrouter',
      model: DEFAULT_MODEL,
      promptVersion: PROMPT_VERSION,
      latencyMs,
      fallbackUsed: true,
      validationPassed: false,
      responseValidationStatus: 'fallback',
      errorType: 'no_api_key',
      fallbackReason: 'no_api_key',
      source: 'mock',
      createdAt: new Date().toISOString(),
      inputSizeChars,
    })
    return { ...result, provenance }
  }

  try {
    const result = await fn(config)
    const latencyMs = Date.now() - startMs
    const prov = result.provenance
    const provenance: AIProvenance = {
      ...prov,
      traceId: callId,
      latencyMs,
      schemaVersion: SCHEMA_VERSION,
    }
    aiTraceStore.add({
      id: callId,
      method: label,
      provider: prov.provider,
      model: prov.model,
      promptVersion: prov.promptVersion,
      latencyMs,
      fallbackUsed: prov.fallbackUsed,
      validationPassed: prov.validationPassed,
      responseValidationStatus: provenanceToStatus(prov.fallbackUsed, prov.source, prov.fallbackReason),
      fallbackReason: prov.fallbackReason,
      source: prov.source,
      createdAt: prov.generatedAt,
      inputSizeChars,
    })
    return { ...result, provenance }
  } catch (err) {
    console.error(`[OpenRouterAIService] ${label} failed:`, err)
    const result = await fallback()
    const latencyMs = Date.now() - startMs
    const errorType: AITraceErrorType =
      err instanceof AppError && err.code === ServiceErrorCode.AI_GENERATION_FAILED
        ? 'api_error'
        : err instanceof TypeError
          ? 'network'
          : 'unknown'
    const responseValidationStatus: AITraceValidationStatus =
      err instanceof AppError && err.code === ServiceErrorCode.AI_GENERATION_FAILED
        ? 'api_error'
        : 'network_error'
    const fallbackReason = `${errorType}: ${err instanceof Error ? err.message.slice(0, 100) : 'unknown error'}`
    const provenance: AIProvenance = {
      ...result.provenance,
      fallbackUsed: true,
      traceId: callId,
      fallbackReason,
      schemaVersion: SCHEMA_VERSION,
      latencyMs,
    }
    aiTraceStore.add({
      id: callId,
      method: label,
      provider: 'openrouter',
      model: config.model,
      promptVersion: PROMPT_VERSION,
      latencyMs,
      fallbackUsed: true,
      validationPassed: false,
      responseValidationStatus,
      errorType,
      fallbackReason,
      source: 'mock',
      createdAt: new Date().toISOString(),
      inputSizeChars,
    })
    return { ...result, provenance }
  }
}

// ── Service ───────────────────────────────────────────────────────────────────

export const OpenRouterAIService: IAIService = {

  // ── parseNeed ───────────────────────────────────────────────────────────────
  parseNeed(input: string): Promise<AIResponse<ParseNeedResult>> {
    return withFallback('parseNeed', async (config) => {
      const { system, user } = buildNeedParsingPrompt({ userInput: input })
      const raw  = await chat(config, system, user)
      const json = extractJSON<RawNeedParseAI>(raw)
      const ai   = json ? validateAIResponse(NeedParsingResponseSchema, json, 'parseNeed') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] parseNeed: invalid response — using mock fallback')
        const fb = await MockAIService.parseNeed(input)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }

      const extractedCriteria: ParsedNeedCriteria = {
        assetType:          (ai.assetType ?? undefined) as AssetType | undefined,
        areaRange:          ai.areaRange ?? undefined,
        preferredLocations: ai.preferredLocations,
        budgetRange:        ai.budgetRange ?? undefined,
        timing: ai.timing
          ? { ...ai.timing, latestMoveIn: ai.timing.latestMoveIn ?? undefined }
          : undefined,
        mustHaveCriteria: ai.mustHaveCriteria,
      }
      const missingFields = ai.missingFields ?? []
      const confidenceByField: Record<string, number> = {}
      Object.keys(extractedCriteria).forEach(k => {
        confidenceByField[k] = extractedCriteria[k as keyof ParsedNeedCriteria] != null ? 0.85 : 0
      })
      missingFields.forEach(f => { confidenceByField[f] = 0 })
      const followUpQuestionCandidates: FollowUpQuestion[] = missingFields.map((field, i) => ({
        id:          `fq-or-${i}`,
        questionText: followUpForField(field),
        targetField:  field,
        reason:       `Feld "${field}" nicht im Text erkannt`,
        importance:   'recommended' as const,
      }))
      return {
        data: {
          extractedCriteria,
          confidenceByField,
          missingFields,
          assumptions:              ai.assumptions ?? [],
          suggestedWeights:         defaultSuggestedWeights(),
          followUpQuestionCandidates,
          rawSummary:               raw.substring(0, 500),
          promptVersion:            PROMPT_VERSION,
          schemaVersion:            SCHEMA_VERSION,
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.parseNeed(input), input.length)
  },

  // ── generateFollowUpQuestions ───────────────────────────────────────────────
  generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<AIResponse<FollowUpQuestion[]>> {
    return withFallback('generateFollowUpQuestions', async (config) => {
      const missingFields = [
        ...(!criteria.assetType ? ['assetType'] : []),
        ...(!criteria.areaRange || (criteria.areaRange.min <= 0 && criteria.areaRange.max <= 0) ? ['areaRange'] : []),
        ...(!criteria.preferredLocations?.length ? ['preferredLocations'] : []),
        ...(!criteria.budgetRange ? ['budgetRange'] : []),
        ...(!criteria.timing ? ['timing'] : []),
        ...(!criteria.mustHaveCriteria?.length ? ['mustHaveCriteria'] : []),
      ]
      const { system, user } = buildFollowUpQuestionsPrompt({ criteria, missingFields })
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown[]>(raw)
      const ai   = json ? validateAIResponse(FollowUpQuestionsResponseSchema, json, 'generateFollowUpQuestions') : null

      if (!ai?.length) {
        console.warn('[OpenRouterAIService] generateFollowUpQuestions: invalid response — using mock fallback')
        const fb = await MockAIService.generateFollowUpQuestions(criteria)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data: ai.map((q, i) => ({
          id:                     `fq-or-${i}`,
          questionText:           q.questionText,
          targetField:            q.targetField,
          reason:                 q.reason ?? 'AI-generiert',
          suggestedAnswerOptions: q.suggestedAnswerOptions,
          importance:             (q.importance ?? 'recommended') as FollowUpQuestion['importance'],
        })),
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.generateFollowUpQuestions(criteria))
  },

  // ── generateMatchExplanation ────────────────────────────────────────────────
  // Plain-text response — no JSON schema to validate, but non-empty check enforced.
  generateMatchExplanation(input: MatchExplanationInput): Promise<AIResponse<MatchExplanation>> {
    return withFallback('generateMatchExplanation', async (config) => {
      const { system, user } = buildMatchExplanationPrompt(input)
      const raw     = await chat(config, system, user)
      const summary = raw.trim()

      if (!summary) {
        console.warn('[OpenRouterAIService] generateMatchExplanation: empty response — using mock fallback')
        const fb = await MockAIService.generateMatchExplanation(input)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: 'empty_response' }) }
      }
      const scoreLabel = input.matchScore >= 78 ? 'Starkes' : input.matchScore >= 52 ? 'Gutes' : 'Schwaches'
      return {
        data: {
          headline: `${scoreLabel} Match — ${input.propertyTitle} (${input.matchScore}/100)`,
          summary,
          keyReasons: [
            ...input.positiveFactors.slice(0, 2).map(f => `+ ${f.explanation}`),
            ...input.negativeFactors.slice(0, 1).map(f => `− ${f.explanation}`),
          ],
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.generateMatchExplanation(input))
  },

  // ── summarizeTradeOffs ──────────────────────────────────────────────────────
  summarizeTradeOffs(tradeoffs: TradeOffInput[]): Promise<AIResponse<TradeOffSummary>> {
    return withFallback('summarizeTradeOffs', async (config) => {
      const { system, user } = buildTradeOffPrompt(tradeoffs, 'Objekt')
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown>(raw)
      const ai   = json ? validateAIResponse(TradeOffSummaryResponseSchema, json, 'summarizeTradeOffs') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] summarizeTradeOffs: invalid response — using mock fallback')
        const fb = await MockAIService.summarizeTradeOffs(tradeoffs)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data: {
          headline:    ai.headline,
          items:       ai.items.map(item => ({
            concern:    item.concern,
            severity:   item.severity,
            mitigation: item.mitigation,
          })),
          overallRisk: ai.overallRisk,
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.summarizeTradeOffs(tradeoffs))
  },

  // ── summarizeComparison ─────────────────────────────────────────────────────
  // Hybrid: AI provides narrative text; mock provides structural per-property data.
  // source: 'hybrid' — both are labeled in provenance.
  summarizeComparison(items: UnifiedMatchResult[]): Promise<AIResponse<ComparisonSummary>> {
    return withFallback('summarizeComparison', async (config) => {
      type ItemWithProp = UnifiedMatchResult & {
        property?: { title?: string; location?: { city?: string }; rentPricePerSqm?: number }
      }
      const properties = (items as ItemWithProp[])
        .filter(i => i.resultType !== 'FUTURE_AVAILABILITY')
        .map(i => ({
          title:           i.property?.title ?? `Match ${i.matchScore}`,
          matchScore:      i.matchScore,
          city:            i.property?.location?.city ?? '–',
          rentPerSqm:      i.property?.rentPricePerSqm ?? 0,
          positiveFactors: i.match.positiveFactors.slice(0, 2).map(f => f.explanation ?? f.criterion),
          negativeFactors: i.match.negativeFactors.slice(0, 2).map(f => f.explanation ?? f.criterion),
        }))
      const { system, user } = buildCompareSummaryPrompt({ properties })
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown>(raw)
      const ai   = json ? validateAIResponse(CompareSummaryResponseSchema, json, 'summarizeComparison') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] summarizeComparison: invalid response — using mock fallback')
        const fb = await MockAIService.summarizeComparison(items)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      const mock = await MockAIService.summarizeComparison(items)
      return {
        data: {
          ...mock.data,
          overallAssessment: ai.overallAssessment,
          recommendation:    ai.recommendation ?? mock.data.recommendation,
        },
        provenance: makeProvenance(config, 'hybrid', false, true),
      }
    }, () => MockAIService.summarizeComparison(items))
  },

  // ── generateDecisionBrief ───────────────────────────────────────────────────
  // Hybrid: AI generates narrative summary + sections; mock fills structural metadata.
  generateDecisionBrief(shortlistId: string): Promise<AIResponse<DecisionBrief>> {
    return withFallback('generateDecisionBrief', async (config) => {
      const { system, user } = buildDecisionBriefPrompt({ shortlistItems: [], needSummary: shortlistId })
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown>(raw)
      const ai   = json ? validateAIResponse(DecisionBriefResponseSchema, json, 'generateDecisionBrief') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] generateDecisionBrief: invalid response — using mock fallback')
        const fb = await MockAIService.generateDecisionBrief(shortlistId)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      const mock = await MockAIService.generateDecisionBrief(shortlistId)
      return {
        data: {
          ...mock.data,
          summary:  ai.summary,
          sections: ai.sections.map(s => ({ title: s.title, body: s.body })),
        },
        provenance: makeProvenance(config, 'hybrid', false, true),
      }
    }, () => MockAIService.generateDecisionBrief(shortlistId))
  },

  // ── generateDataQualitySummary ──────────────────────────────────────────────
  generateDataQualitySummary(propertyId: string, quality: DataQualityInput): Promise<AIResponse<DataQualitySummary>> {
    return withFallback('generateDataQualitySummary', async (config) => {
      const { system, user } = buildDataQualityPrompt(propertyId, quality)
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown>(raw)
      const ai   = json ? validateAIResponse(DataQualitySummaryResponseSchema, json, 'generateDataQualitySummary') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] generateDataQualitySummary: invalid response — using mock fallback')
        const fb = await MockAIService.generateDataQualitySummary(propertyId, quality)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data: {
          overallAssessment:     ai.overallAssessment,
          missingCriticalFields: ai.missingCriticalFields ?? quality.missingCriticalFields,
          recommendation:        ai.recommendation,
          confidence:            ai.confidence,
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.generateDataQualitySummary(propertyId, quality))
  },

  // ── classifyMarketSignal ────────────────────────────────────────────────────
  classifyMarketSignal(signalText: string): Promise<AIResponse<MarketSignalClassification>> {
    return withFallback('classifyMarketSignal', async (config) => {
      const { system, user } = buildMarketSignalPrompt(signalText)
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown>(raw)
      const ai   = json
        ? validateAIResponse(MarketSignalClassificationResponseSchema, json, 'classifyMarketSignal')
        : null

      if (!ai) {
        console.warn('[OpenRouterAIService] classifyMarketSignal: invalid response — using mock fallback')
        const fb = await MockAIService.classifyMarketSignal(signalText)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data: {
          signalType:        ai.signalType,
          probability:       ai.probability,
          timeHorizonMonths: ai.timeHorizonMonths ?? null,
          areaSqmEstimate:   ai.areaSqmEstimate ?? null,
          credibility:       ai.credibility,
          reasoning:         ai.reasoning,
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.classifyMarketSignal(signalText), signalText.length)
  },

  // ── generateOfferEmail ──────────────────────────────────────────────────────
  generateOfferEmail(payload: OfferEmailPayload): Promise<AIResponse<{ subject: string; body: string }>> {
    return withFallback('generateOfferEmail', async (config) => {
      const propertyList = payload.properties
        .map((p, i) => `• ${p} (Match-Score: ${payload.matchScores[i]}%)`)
        .join('\n')
      const system = `Du bist Immobilienmakler bei Wincasa AG. Erstelle eine professionelle, knappe Angebotsmail auf Deutsch. Antworte als JSON: { "subject": "...", "body": "..." }`
      const user   = `Suchanfrage: "${payload.needTitle}"\n\nObjekte:\n${propertyList}\n\nErstelle eine professionelle Angebotsmail.`
      const raw    = await chat(config, system, user)
      const json   = extractJSON<unknown>(raw)
      const ai     = json ? validateAIResponse(OfferEmailResponseSchema, json, 'generateOfferEmail') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] generateOfferEmail: invalid response — using mock fallback')
        const fb = await MockAIService.generateOfferEmail(payload)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data:      { subject: ai.subject, body: ai.body },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.generateOfferEmail(payload))
  },

  // ── Legacy: extractCriteria ─────────────────────────────────────────────────
  extractCriteria(input: string): Promise<AIResponse<CriteriaExtractionResult>> {
    return withFallback('extractCriteria', async (config) => {
      const { system, user } = buildNeedParsingPrompt({ userInput: input })
      const raw  = await chat(config, system, user)
      const json = extractJSON<RawNeedParseAI>(raw)
      const ai   = json ? validateAIResponse(NeedParsingResponseSchema, json, 'extractCriteria') : null

      if (!ai) {
        console.warn('[OpenRouterAIService] extractCriteria: invalid response — using mock fallback')
        const fb = await MockAIService.extractCriteria(input)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data: {
          extractedCriteria: {
            assetType:          ai.assetType as AssetType | undefined ?? undefined,
            requiredArea:       ai.areaRange ?? undefined,
            preferredLocations: ai.preferredLocations ?? [],
            budgetRange:        ai.budgetRange ?? undefined,
          },
          confidence:         0.80,
          missingFields:      ai.missingFields ?? [],
          assumptions:        ai.assumptions ?? [],
          followUpQuestions:  (ai.missingFields ?? []).map(followUpForField),
        },
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.extractCriteria(input))
  },

  // ── Legacy: generateFollowUp ────────────────────────────────────────────────
  generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<AIResponse<string[]>> {
    return withFallback('generateFollowUp', async (config) => {
      const missingFields = [
        ...(!partialNeed.assetType             ? ['assetType']          : []),
        ...(!partialNeed.preferredLocations?.length ? ['preferredLocations'] : []),
        ...(!partialNeed.timing                ? ['timing']             : []),
        ...(!partialNeed.budgetRange           ? ['budgetRange']        : []),
      ]
      if (!missingFields.length) {
        return { data: [], provenance: makeProvenance(config, 'ai', false, true) }
      }
      const { system, user } = buildFollowUpQuestionsPrompt({
        criteria: partialNeed as ParsedNeedCriteria,
        missingFields,
      })
      const raw  = await chat(config, system, user)
      const json = extractJSON<unknown[]>(raw)
      const ai   = json ? validateAIResponse(FollowUpQuestionsResponseSchema, json, 'generateFollowUp') : null

      if (!ai?.length) {
        console.warn('[OpenRouterAIService] generateFollowUp: invalid response — using mock fallback')
        const fb = await MockAIService.generateFollowUp(partialNeed)
        return { ...fb, provenance: makeProvenance(config, 'mock', true, false, { fallbackReason: json ? 'schema_validation' : 'json_parse' }) }
      }
      return {
        data:      ai.map(q => q.questionText).filter(Boolean),
        provenance: makeProvenance(config, 'ai', false, true),
      }
    }, () => MockAIService.generateFollowUp(partialNeed))
  },
}
