/**
 * Lead AI console ("KI-Konsole") — free-form chat with Claude about ONE lead, grounded in
 * the same dossier the KI-Analyse uses (ERP record, activities, offers / contract, signing
 * links, attachments, the mailpiler e-mail history with bodies, call transcripts) plus the
 * last KI-Analyse result when one exists.
 *
 * Two parts, both persisted in data/lead-chats/<leadId>.json (git-ignored):
 *  - context: the dossier build (same steps / background model as the assessment —
 *    iDempiere reads inside the request, archive + PBX in the background, polled via GET).
 *    The dossier text itself stays server-side; the client only sees steps / counts.
 *  - messages: the conversation (user / assistant turns) so the console reopens with its
 *    history. Answers stream to the browser as SSE while they are generated.
 *
 * Claude: one request per question with the full history; the dossier sits in a cached
 * system block (1 h TTL) so follow-up questions only pay for the new turn. Long answers
 * (mail drafts, scripts) get up to 32k output tokens per request and are auto-continued
 * twice when they still hit the limit.
 */
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises'
import { join } from 'node:path'
import {
  type AssessmentJob, type AssessmentStep, newSteps, collectErpContext, collectMails, collectCalls, buildDossier,
  readAssessmentJob, cfg, val, anthropicClient, anthropicErrorMessage, LANG_NAMES
} from './leadAssessment'

const CONTEXT_STALE_MS = 20 * 60000     // a pending context build older than this is dead
const MAX_TURNS = 40                    // history turns sent to the model (newest kept)
const MAX_QUESTION_CHARS = 6000
const DEFAULT_ANSWER_TOKENS = 32000     // per request; ANTHROPIC_CHAT_MAX_TOKENS overrides (1000 … 128000)
const MAX_CONTINUATIONS = 2             // extra requests when an answer still hits max_tokens
const CONTINUE_PROMPT = 'Deine Antwort wurde am Token-Limit abgeschnitten. Fahre exakt an der Stelle fort, an der du aufgehört hast – ohne Wiederholung des bisherigen Textes und ohne Vorbemerkung.'
const answerTokens = () => Math.min(128000, Math.max(1000, cfg().chatMaxTokens || DEFAULT_ANSWER_TOKENS))

export interface ChatMessage {
  id: string
  role: 'user' | 'assistant'
  text: string
  at: string
  userId?: number
  model?: string
  usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }
  ms?: number
  error?: string
  truncated?: boolean
}
export interface ChatContext extends AssessmentJob {
  dossier?: string
  dossierChars?: number
  assessmentAt?: string      // finishedAt of the KI-Analyse that was folded into the context
}
export interface ChatFile {
  leadId: number
  context: ChatContext | null
  messages: ChatMessage[]
}

// ---- persistence ------------------------------------------------------------------
const dir = () => join(process.cwd(), 'data', 'lead-chats')
const filePath = (leadId: number) => join(dir(), `${leadId}.json`)
export const readChat = async (leadId: number): Promise<ChatFile> => {
  try {
    const p = filePath(leadId)
    if (!existsSync(p)) return { leadId, context: null, messages: [] }
    const f: ChatFile = JSON.parse(await readFile(p, 'utf8'))
    f.messages = Array.isArray(f.messages) ? f.messages : []
    const c = f.context
    if (c?.state === 'pending' && Date.now() - new Date(c.startedAt).getTime() > CONTEXT_STALE_MS) {
      c.state = 'failed'; c.error = 'Kontextaufbau abgebrochen (Zeitüberschreitung)'; c.finishedAt = new Date().toISOString()
    }
    return f
  } catch { return { leadId, context: null, messages: [] } }
}
const saveChat = async (f: ChatFile) => {
  try {
    if (!existsSync(dir())) await mkdir(dir(), { recursive: true })
    await writeFile(filePath(f.leadId), JSON.stringify(f), 'utf8')
  } catch (err: any) { console.warn('[lead-chat] persist failed:', err?.message || err) }
}

/** What the browser gets: the context without the (large) dossier text. */
export const publicContext = (c: ChatContext | null) => {
  if (!c) return null
  const { dossier, ...rest } = c
  return rest
}

// ---- context build ------------------------------------------------------------------
const running = new Map<number, Promise<void>>()

/**
 * Builds (or rebuilds with `force`) the dossier for the console. Returns immediately after
 * the iDempiere reads; mails / calls continue in the background. A build that is still
 * running, or a finished one without `force`, is returned as-is.
 */
export const startChatContext = async (event: any, token: string, leadId: number, userId: number, force: boolean): Promise<ChatFile> => {
  const file = await readChat(leadId)
  if (file.context?.state === 'pending') return file
  if (file.context?.state === 'done' && file.context.dossier && !force) return file

  const ctx: ChatContext = {
    leadId, state: 'pending', startedAt: new Date().toISOString(), startedBy: userId, language: 'de', transcribeMissing: false,
    steps: newSteps(['ai']), warnings: []
  }
  // The assessment helpers call their own saveJob(); route it to this file instead.
  Object.defineProperty(ctx, '__persist', { value: () => saveChat(file), enumerable: false, configurable: true })
  file.context = ctx
  await saveChat(file)

  let erp
  try {
    erp = await collectErpContext(event, token, leadId, ctx)
  } catch (err: any) {
    ctx.state = 'failed'; ctx.error = err?.message || String(err); ctx.finishedAt = new Date().toISOString()
    const s = ctx.steps.find((x: AssessmentStep) => x.key === 'lead'); if (s) { s.state = 'error'; s.message = ctx.error }
    await saveChat(file)
    return file
  }
  const email = String(val(erp.lead, 'EMail')).trim().toLowerCase()
  const numbers = [val(erp.lead, 'Phone'), val(erp.lead, 'Phone2')].map(String)
  ctx.lead = { name: String(val(erp.lead, 'Name')), email }
  await saveChat(file)

  const run = (async () => {
    try {
      const [mails, calls] = await Promise.all([collectMails(email, ctx), collectCalls(numbers, ctx)])
      ctx.dossier = buildDossier(erp, mails, calls)
      ctx.dossierChars = ctx.dossier.length
      ctx.sources = {
        activities: erp.activities.length, conditions: Object.keys(erp.conditions || {}).length, signing: erp.signing.length, attachments: erp.attachments.length,
        mails: mails.length, mailBodies: mails.filter((m) => m.body !== undefined).length, calls: calls.length, transcripts: calls.filter((c) => c.transcript).length
      }
      ctx.state = 'done'
    } catch (err: any) {
      const m = err?.message || String(err)
      console.warn('[lead-chat] context build failed:', leadId, m)
      ctx.state = 'failed'; ctx.error = m
    } finally {
      ctx.finishedAt = new Date().toISOString()
      await saveChat(file)
      running.delete(leadId)
    }
  })()
  running.set(leadId, run)
  return file
}

export const clearChatMessages = async (leadId: number): Promise<ChatFile> => {
  const file = await readChat(leadId)
  file.messages = []
  if (file.context) await saveChat(file)
  else { try { if (existsSync(filePath(leadId))) await unlink(filePath(leadId)) } catch { /* ignore */ } }
  return file
}

// ---- asking ---------------------------------------------------------------------------
const SYSTEM_PROMPT = `Du bist der KI-Assistent im ERP von LogYou (LogShip), einem deutschen Fulfillment-/3PL-Dienstleister (Lagerung, Kommissionierung, Versand für Online-Händler; Angebote mit Lagergeld, Pick & Pack, Versandkonditionen, Wareneingang, Retouren; Rahmenvertrag mit AGB/ADSp/AVV, digitale Signatur, SEPA-Mandat, Fragebogen Erstgespräch, individuelle Prognose).

Ein Vertriebsmitarbeiter stellt dir Fragen zu GENAU EINEM Lead. Im zweiten System-Block steht das vollständige Dossier dieses Leads: ERP-Datensatz, Aktivitäten, gespeicherte Angebots-/Vertragskonditionen, Signatur-Links, Anhänge, der komplette E-Mail-Verlauf (gesendet und empfangen) mit Inhalten, Transkripte der Telefonate und – falls vorhanden – die letzte KI-Analyse.

Regeln:
- Beantworte die Fragen NUR aus dem Dossier. Erfinde nichts. Wenn das Dossier etwas nicht hergibt, sage das klar.
- Nenne, wo es hilft, Datum und Quelle (Mail vom …, Anruf am …, Angebot vom …), damit der Nutzer es nachschlagen kann.
- Sei konkret: Daten, Beträge, Namen, Zitate. Keine Allgemeinplätze, keine Wiederholung der Frage.
- Antworte so knapp wie möglich und so ausführlich wie nötig; bei Listen von Fakten gern als Aufzählung. Einfaches Markdown (Fett, Aufzählungen, kurze Überschriften) ist erlaubt, keine Tabellen.
- Transkripte sind automatisch erstellt (Whisper) und können Fehler enthalten; Sprecher sind nicht markiert – leite den Sprecher aus dem Kontext ab und sage, wenn du unsicher bist.
- Wenn du einen E-Mail-Text oder Gesprächsleitfaden formulieren sollst: Anrede bis Grußformel, OHNE Signaturblock (wird beim Versand automatisch angehängt).
- Du beziehst dich auf frühere Fragen und Antworten dieses Gesprächs, wenn der Nutzer das tut.
- Antworte in der Sprache, die unten angegeben ist, auch wenn das Dossier deutsch ist.`

const contextBlock = async (ctx: ChatContext): Promise<string> => {
  const parts = [`DOSSIER DES LEADS (Stand ${String(ctx.finishedAt || ctx.startedAt).slice(0, 16).replace('T', ' ')}):\n\n${ctx.dossier || ''}`]
  const job = await readAssessmentJob(ctx.leadId)
  if (job?.state === 'done' && job.result) {
    ctx.assessmentAt = job.finishedAt
    const { draftMessage, ...rest } = job.result
    parts.push(`\n# LETZTE KI-ANALYSE (${String(job.finishedAt || '').slice(0, 16).replace('T', ' ')}, Modell ${job.model || ''})\n${JSON.stringify(rest, null, 1)}`)
  }
  return parts.join('\n')
}

export interface AskHandlers { onDelta: (text: string) => void }

/**
 * Answers one question. Streams text deltas through `handlers.onDelta`, persists the user
 * turn and the (possibly partial) assistant turn, and resolves with the stored assistant
 * message. Throws with a readable message when the context is missing or the API fails
 * before any text arrived.
 */
export const askChat = async (leadId: number, userId: number, question: string, language: string, handlers: AskHandlers): Promise<ChatMessage> => {
  const file = await readChat(leadId)
  const ctx = file.context
  if (!ctx || ctx.state !== 'done' || !ctx.dossier) throw new Error(ctx?.state === 'pending' ? 'Der Kontext wird noch aufgebaut' : 'Kein Kontext – bitte zuerst den Kontext aufbauen')
  const text = String(question || '').trim().slice(0, MAX_QUESTION_CHARS)
  if (!text) throw new Error('Leere Frage')

  const now = new Date().toISOString()
  const userMsg: ChatMessage = { id: `${Date.now().toString(36)}u`, role: 'user', text, at: now, userId }
  // History for the model: finished turns only (a failed answer without text is skipped).
  const history = file.messages.filter((m) => m.text && m.text.trim()).slice(-MAX_TURNS)
  const messages: any[] = history.map((m) => ({ role: m.role, content: m.text }))
  // Consecutive same-role turns are merged by the API; the first turn must be a user one.
  while (messages.length && messages[0].role !== 'user') messages.shift()
  messages.push({ role: 'user', content: `Sprache der Antwort: ${LANG_NAMES[language] || 'Deutsch'}. Heutiges Datum: ${now.slice(0, 10)}.\n\n${text}` })

  file.messages.push(userMsg)
  await saveChat(file)

  const { model } = cfg()
  const client = anthropicClient(10 * 60 * 1000)
  const system = [
    { type: 'text', text: SYSTEM_PROMPT },
    { type: 'text', text: await contextBlock(ctx), cache_control: { type: 'ephemeral', ttl: '1h' } }
  ]
  const t0 = Date.now()
  const reply: ChatMessage = { id: `${Date.now().toString(36)}a`, role: 'assistant', text: '', at: new Date().toISOString() }
  const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
  let finalMsg: any = null
  // One streamed request; when the answer hits max_tokens the partial text is handed back as an
  // assistant turn and the model is asked to continue (at most MAX_CONTINUATIONS times). The
  // dossier block stays cached, so a continuation only pays for the new turn.
  let round = 0
  let partial = ''
  try {
    while (true) {
      const turnMessages = partial
        ? [...messages, { role: 'assistant', content: partial }, { role: 'user', content: CONTINUE_PROMPT }]
        : messages
      const stream = client.beta.messages.stream({
        model,
        max_tokens: answerTokens(),
        betas: ['server-side-fallback-2026-07-01'],
        fallbacks: 'default',
        output_config: { effort: 'medium' },
        system,
        messages: turnMessages
      } as any)
      let roundText = ''
      for await (const ev of stream as any) {
        if (ev?.type === 'content_block_delta' && ev.delta?.type === 'text_delta' && ev.delta.text) {
          roundText += ev.delta.text
          reply.text += ev.delta.text
          handlers.onDelta(ev.delta.text)
        }
      }
      finalMsg = await stream.finalMessage()
      usage.input += Number(finalMsg.usage?.input_tokens || 0); usage.output += Number(finalMsg.usage?.output_tokens || 0)
      usage.cacheRead += Number(finalMsg.usage?.cache_read_input_tokens || 0); usage.cacheWrite += Number(finalMsg.usage?.cache_creation_input_tokens || 0)
      if (finalMsg.stop_reason !== 'max_tokens' || !roundText.trim() || round >= MAX_CONTINUATIONS) break
      round++
      partial = reply.text
    }
  } catch (err: any) {
    reply.error = anthropicErrorMessage(err)
    if (!reply.text) {
      // Nothing arrived: keep the question, drop the empty answer, surface the error.
      await saveChat(file)
      throw new Error(reply.error)
    }
  }
  if (finalMsg) {
    reply.model = String(finalMsg.model || model)
    reply.usage = usage
    if (finalMsg.stop_reason === 'max_tokens') reply.truncated = true
    if (finalMsg.stop_reason === 'refusal') reply.error = 'Das Modell hat die Antwort abgelehnt' + (finalMsg.stop_details?.explanation ? ': ' + finalMsg.stop_details.explanation : '')
  }
  reply.ms = Date.now() - t0
  file.messages.push(reply)
  await saveChat(file)
  return reply
}
