/**
 * POST /api/chatbot
 *
 * Chatbot recherche sémantique — Mistral Small
 * Spec : F §7 (endpoint), F §8 (rate limit), E-spec §6 (détails chatbot)
 *
 * Flow :
 *   1. Rate limit : 10 req/IP/jour (JSON fichier, SHA-256)
 *   2. Circuit breaker : budget 20€/mois
 *   3. Fetch top-N fiches (keyword match sur nom+description+fonctions)
 *   4. Appel Mistral Small avec contexte JSON compact
 *   5. Parse JSON → { reponse_texte, fiches_recommandees }
 *   6. Log stats_usage
 *
 * Réponse 200 : { reponse_texte, fiches_recommandees: [{ id, nom, explication }] }
 * Réponse 429 : rate limit dépassé
 * Réponse 503 : budget IA épuisé
 */

import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
import { checkBudget, calcCoutMistralSmall } from '~/server/utils/circuitBreaker'

// ── Types ──────────────────────────────────────────────────────────────────────

interface OrgRow {
  Id: number
  nom: string
  description_enrichie?: string | null
  description_user?: string | null
  tags_fonction?: string | null
  echelle?: string | null
  localisation_ville?: string | null
}

interface FicheReco {
  id: number
  nom: string
  explication: string
}

interface MistralResponse {
  reponse_texte: string
  fiches_recommandees: FicheReco[]
}

// ── System prompt Mistral Small ────────────────────────────────────────────────
// Construit depuis E-spec-frontend.md §Détails chatbot + §Prompt système
// (F-spec §3 concerne Mistral Nemo enrichissement — ne pas confondre)

const SYSTEM_PROMPT = `Tu es un assistant engagé au service de la transition écologique des pratiques architecturales. Tu accèdes à AEP (Architecture d'Écologie Politique) — Écosystème Entraide, une base de données collaborative qui référence les acteurs de l'écologie politique appliquée à l'architecture et au territoire (organisations, outils, ressources) pour les architectes en France.

RÈGLES ABSOLUES :
1. Tu ne peux recommander QUE des organisations présentes dans le contexte ci-dessous.
2. Ne jamais inventer d'organisation absente du contexte.
3. Cite chaque organisation recommandée par son nom exact et son identifiant id.
4. Si le contexte ne contient aucune organisation pertinente, dis-le honnêtement.
5. Réponses concises par défaut (200 mots max). Si l'usager demande explicitement plus de détail, tu peux développer.
6. Retourne UNIQUEMENT un objet JSON valide, sans texte avant ou après.
7. Si la question est hors du champ architecture / écologie / territoire / transition, recadre poliment vers le périmètre de la carte.

FORMAT DE SORTIE :
{
  "reponse_texte": "Ta réponse en prose (max 200 mots), orientée vers les besoins de l'architecte",
  "fiches_recommandees": [
    { "id": 123, "explication": "Pourquoi cette fiche répond à la question (1-2 phrases max)" }
  ]
}

CONTEXTE — Organisations disponibles dans la base NAV :
{{FICHES_JSON}}`

// ── Recherche par mots-clés ────────────────────────────────────────────────────

function scoreOrg(org: OrgRow, keywords: string[]): number {
  if (keywords.length === 0) return 1
  const haystack = [
    org.nom,
    org.description_enrichie,
    org.description_user,
    org.tags_fonction,
    org.localisation_ville,
    org.echelle,
  ]
    .filter(Boolean)
    .join(' ')
    .toLowerCase()

  return keywords.reduce((score, kw) => {
    return score + (haystack.includes(kw) ? 1 : 0)
  }, 0)
}

function extractKeywords(question: string): string[] {
  return question
    .toLowerCase()
    .replace(/[^\w\sàâäéèêëîïôùûüç-]/g, ' ')
    .split(/\s+/)
    .filter((w) => w.length >= 3)
    .slice(0, 10)
}

// ── Fetch fiches depuis NocoDB ─────────────────────────────────────────────────

async function fetchApprovedOrgs(config: {
  nocodbUrl: string
  nocodbToken: string
  orgTableId: string
}): Promise<OrgRow[]> {
  const { nocodbUrl, nocodbToken, orgTableId } = config
  const url = `${nocodbUrl}/api/v2/tables/${orgTableId}/records`

  try {
    const res = await $fetch<{ list: OrgRow[] }>(url, {
      headers: { 'xc-token': nocodbToken },
      query: {
        where: '(moderation_status,eq,approved)',
        limit: 200,
        fields: 'Id,nom,description_enrichie,description_user,tags_fonction,echelle,localisation_ville',
      },
    })
    return res?.list ?? []
  } catch (e) {
    console.error('[chatbot] Erreur fetch NocoDB:', (e as Error).message)
    return []
  }
}

// ── Log stats_usage ────────────────────────────────────────────────────────────

async function logUsage(params: {
  nocodbUrl: string
  nocodbToken: string
  statsTableId: string
  tokensIn: number
  tokensOut: number
  coutEur: number
}) {
  const { nocodbUrl, nocodbToken, statsTableId, tokensIn, tokensOut, coutEur } = params
  const logUrl = `${nocodbUrl}/api/v2/tables/${statsTableId}/records`
  try {
    await $fetch(logUrl, {
      method: 'POST',
      headers: { 'xc-token': nocodbToken, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: 'mistral-small-latest',
        endpoint: 'chatbot',
        tokens_in: tokensIn,
        tokens_out: tokensOut,
        cout_eur: coutEur,
        timestamp: new Date().toISOString(),
        orga_id: null,
      }),
    })
  } catch (e) {
    console.warn('[chatbot] Log stats_usage échoué (non bloquant):', (e as Error).message)
  }
}

// ── Handler principal ──────────────────────────────────────────────────────────

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()

  // 1. IP (proxy-aware)
  const ip =
    getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() ||
    event.node.req.socket?.remoteAddress ||
    '0.0.0.0'

  // 2. Rate limit : 10 req/IP/jour (JSON + SHA-256)
  const allowed = checkRateLimitJson(ip, 'chatbot', 10)
  if (!allowed) {
    throw createError({
      statusCode: 429,
      statusMessage: 'Limite de 10 questions par jour atteinte.',
    })
  }

  // 3. Lire le body
  const body = await readBody(event)
  const question: string = (body?.question ?? '').trim()
  const filters: { fonction?: string; echelle?: string } = body?.filters ?? {}

  if (!question || question.length < 3) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Question trop courte.',
    })
  }

  // 4. Circuit breaker budget
  const statsTableId = process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc'
  const budget = await checkBudget({
    nocodbUrl: config.nocodbUrl as string,
    nocodbToken: config.nocodbToken as string,
    statsTableId,
  })

  if (budget.blocked) {
    throw createError({
      statusCode: 503,
      statusMessage: 'Budget IA mensuel épuisé — réouverture le 1er du mois prochain.',
    })
  }

  // 5. Fetch fiches et scoring par mots-clés
  const allOrgs = await fetchApprovedOrgs({
    nocodbUrl: config.nocodbUrl as string,
    nocodbToken: config.nocodbToken as string,
    orgTableId: config.orgTableId as string,
  })

  const keywords = extractKeywords(question)

  // Filtrage optionnel par taxonomie si filtres fournis
  let filtered = allOrgs
  if (filters.fonction) {
    filtered = filtered.filter((o) =>
      (o.tags_fonction ?? '').toLowerCase().includes(filters.fonction!.toLowerCase()),
    )
  }
  if (filters.echelle) {
    filtered = filtered.filter((o) =>
      (o.echelle ?? '').toLowerCase() === filters.echelle!.toLowerCase(),
    )
  }

  // Score + top 20
  const scored = filtered
    .map((o) => ({ org: o, score: scoreOrg(o, keywords) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, 20)
    .map((x) => x.org)

  // Contexte JSON compact pour le prompt
  const fichesContext = scored.map((o) => ({
    id: o.Id,
    nom: o.nom,
    fonctions: o.tags_fonction ?? '',
    echelle: o.echelle ?? '',
    description: (o.description_enrichie ?? o.description_user ?? '').slice(0, 200),
    ville: o.localisation_ville ?? '',
  }))

  const systemPrompt = SYSTEM_PROMPT.replace(
    '{{FICHES_JSON}}',
    JSON.stringify(fichesContext, null, 0),
  )

  // 6. Appel Mistral Small
  const mistralApiKey = config.mistralApiKey as string

  if (!mistralApiKey) {
    throw createError({
      statusCode: 500,
      statusMessage: 'Clé API Mistral manquante.',
    })
  }

  let mistralRaw: string
  let tokensIn = 0
  let tokensOut = 0

  try {
    const mistralRes = await $fetch<{
      choices: { message: { content: string } }[]
      usage?: { prompt_tokens: number; completion_tokens: number }
    }>('https://api.mistral.ai/v1/chat/completions', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${mistralApiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'mistral-small-latest',
        temperature: 0.3,
        max_tokens: 600,
        response_format: { type: 'json_object' },
        messages: [
          { role: 'system', content: systemPrompt },
          { role: 'user', content: question },
        ],
      }),
    })

    mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
    tokensIn = mistralRes.usage?.prompt_tokens ?? 0
    tokensOut = mistralRes.usage?.completion_tokens ?? 0
  } catch (e: any) {
    console.error('[chatbot] Erreur Mistral Small:', e?.message ?? e)
    throw createError({
      statusCode: 502,
      statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
    })
  }

  // 7. Parse JSON
  let parsed: MistralResponse
  try {
    const raw = JSON.parse(mistralRaw)
    parsed = {
      reponse_texte: raw.reponse_texte ?? 'Je n\'ai pas pu analyser ta demande.',
      fiches_recommandees: (raw.fiches_recommandees ?? []).map((f: any) => {
        const org = scored.find((o) => o.Id === f.id)
        return {
          id: f.id,
          nom: org?.nom ?? f.nom ?? `Fiche #${f.id}`,
          explication: f.explication ?? '',
        }
      }),
    }
  } catch {
    parsed = {
      reponse_texte: 'Je n\'ai pas pu analyser ta demande correctement.',
      fiches_recommandees: [],
    }
  }

  // 8. Log usage (non bloquant)
  const coutEur = calcCoutMistralSmall(tokensIn, tokensOut)
  logUsage({
    nocodbUrl: config.nocodbUrl as string,
    nocodbToken: config.nocodbToken as string,
    statsTableId,
    tokensIn,
    tokensOut,
    coutEur,
  })

  return parsed
})
