/**
 * Client Manager Agent — @soi96/client-manager
 *
 * Orchestrates document ingestion for client folders.
 * On each document upload:
 *   1. Calls document-classifier (Haiku) to extract raw metadata
 *   2. Uses Claude Sonnet to make the final filing decision with full client context
 *   3. PATCHes the document record in legal-service with classification results
 *
 * POST /document-uploaded  — triggered by legal-service after each upload
 * GET  /health             — liveness check
 *
 * Port: 3301 (override with PORT env var)
 */

import Anthropic from '@anthropic-ai/sdk'
import express, { Request, Response } from 'express'
import { z } from 'zod'

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

const LEGAL_SERVICE         = process.env.LEGAL_SERVICE_URL         ?? 'http://localhost:3200'
const DOCUMENT_CLASSIFIER   = process.env.DOCUMENT_CLASSIFIER_URL   ?? 'http://localhost:3300'

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })

// ─── Schemas ───────────────────────────────────────────────────────────────────

const DocumentUploadedSchema = z.object({
  documentId:       z.string(),
  clientFolderId:   z.string(),
  storageKey:       z.string(),
  mimeType:         z.string(),
  fileName:         z.string(),
  batesNumber:      z.string(),
  uploadedBy:       z.string(),
  contentBase64:    z.string().optional(),  // passed inline at upload time to avoid re-fetch
  clientName:       z.string().optional(),
  folderNumber:     z.string().optional(),
  availableFolders: z.array(z.string()).optional(),
})

type DocumentUploaded = z.infer<typeof DocumentUploadedSchema>

interface ClassifyResult {
  docType: string
  parties: string[]
  documentDate: string | null
  summary: string
  suggestedFolder: string
  tags: string[]
  confidence: number
  detectedLanguage: string
}

interface FilingDecision {
  finalFolder: string       // chosen folder name
  folderId: string | null   // resolved _id from docFolders list, or null = Inbox
  reasoning: string         // brief explanation (not stored, for logging)
  titleSuggestion: string   // better document title if classifier confidence is high
}

// ─── Step 1: Call document-classifier ─────────────────────────────────────────

async function callClassifier(event: DocumentUploaded): Promise<ClassifyResult> {
  // Fetch content from legal-service if not passed inline
  let base64 = event.contentBase64
  if (!base64) {
    const res = await fetch(`${LEGAL_SERVICE}/documents/${event.documentId}/content`)
    if (!res.ok) throw new Error(`Failed to fetch document content: ${res.status}`)
    const buf = await res.arrayBuffer()
    base64 = Buffer.from(buf).toString('base64')
  }

  const res = await fetch(`${DOCUMENT_CLASSIFIER}/classify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      documentId:       event.documentId,
      contentBase64:    base64,
      mimeType:         event.mimeType,
      fileName:         event.fileName,
      clientName:       event.clientName,
      folderNumber:     event.folderNumber,
      availableFolders: event.availableFolders,
    }),
  })

  if (!res.ok) {
    const body = await res.text()
    throw new Error(`Classifier error ${res.status}: ${body}`)
  }

  const json = await res.json() as { ok: boolean; result: ClassifyResult }
  if (!json.ok) throw new Error('Classifier returned ok=false')
  return json.result
}

// ─── Step 2: Fetch available doc-folders from legal-service ───────────────────

async function fetchDocFolders(clientFolderId: string): Promise<{ _id: string; name: string }[]> {
  try {
    const res = await fetch(`${LEGAL_SERVICE}/client-folders/${clientFolderId}`)
    if (!res.ok) return []
    const folder = await res.json() as { docFolders?: { _id: string; name: string }[] }
    return folder.docFolders ?? []
  } catch {
    return []
  }
}

// ─── Step 3: Sonnet makes the final filing decision ───────────────────────────

async function makeFilingDecision(
  event: DocumentUploaded,
  classification: ClassifyResult,
  docFolders: { _id: string; name: string }[],
): Promise<FilingDecision> {
  const folderList = docFolders.map(f => `- "${f.name}" (id: ${f._id})`).join('\n')

  const prompt = `You are the Client Manager for ${event.clientName ?? 'a law firm client'} (folder ${event.folderNumber ?? 'unknown'}).

A new document has been uploaded and pre-classified by the Document Classifier. Review the classification and make the final filing decision.

## Document
- File: ${event.fileName}
- Bates: ${event.batesNumber}
- MIME type: ${event.mimeType}

## Classifier output (confidence: ${classification.confidence.toFixed(2)})
- Document type: ${classification.docType}
- Detected language: ${classification.detectedLanguage}
- Parties: ${classification.parties.join(', ') || 'none detected'}
- Document date: ${classification.documentDate ?? 'unknown'}
- Summary: ${classification.summary}
- Tags: ${classification.tags.join(', ') || 'none'}
- Suggested folder: ${classification.suggestedFolder}

## Available folders
${folderList || '(no folders configured — use Inbox)'}

## Your task
Return a JSON object with:
{
  "finalFolder": "<exact folder name>",
  "folderId": "<folder _id from the list above, or null for Inbox>",
  "reasoning": "<one sentence explaining your decision>",
  "titleSuggestion": "<a clean, descriptive title for this document, e.g. 'Employment Contract — John Smith — 2025-03-15'>"
}

If confidence is below 0.5, use Inbox (folderId: null). Otherwise file in the most appropriate folder. Respond with JSON only.`

  const message = await anthropic.messages.create({
    model:      'claude-sonnet-4-6',
    max_tokens: 512,
    messages:   [{ role: 'user', content: prompt }],
  })

  const rawText = message.content.find(b => b.type === 'text')?.text ?? ''
  const jsonMatch = rawText.match(/\{[\s\S]*\}/)
  if (!jsonMatch) throw new Error(`No JSON in Sonnet response: ${rawText.slice(0, 200)}`)

  return JSON.parse(jsonMatch[0]) as FilingDecision
}

// ─── Step 4: Patch document in legal-service ──────────────────────────────────

async function patchDocument(documentId: string, updates: Record<string, unknown>): Promise<void> {
  const res = await fetch(`${LEGAL_SERVICE}/documents/${documentId}`, {
    method:  'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(updates),
  })
  if (!res.ok) {
    const body = await res.text()
    console.error(`[client-manager] PATCH /documents/${documentId} failed ${res.status}: ${body}`)
  }
}

// ─── Main pipeline ─────────────────────────────────────────────────────────────

async function handleDocumentUploaded(event: DocumentUploaded): Promise<void> {
  const { documentId, clientFolderId, batesNumber } = event

  console.log(`[client-manager] processing ${batesNumber} (${event.fileName})`)

  // Mark processing
  await patchDocument(documentId, { aiStatus: 'processing' })

  // Classify
  let classification: ClassifyResult
  try {
    classification = await callClassifier(event)
  } catch (err) {
    console.error(`[client-manager] classifier failed for ${batesNumber}:`, err)
    await patchDocument(documentId, { aiStatus: 'failed' })
    return
  }

  console.log(`[client-manager] ${batesNumber} classified as ${classification.docType} (confidence ${classification.confidence.toFixed(2)})`)

  // Fetch folder list for context
  const docFolders = await fetchDocFolders(clientFolderId)

  // Sonnet makes final filing decision
  let decision: FilingDecision
  try {
    decision = await makeFilingDecision(event, classification, docFolders)
    console.log(`[client-manager] ${batesNumber} → "${decision.finalFolder}" | ${decision.reasoning}`)
  } catch (err) {
    // If Sonnet fails, fall back to classifier's suggestion
    console.error(`[client-manager] Sonnet decision failed for ${batesNumber}, using classifier suggestion:`, err)
    const matched = docFolders.find(f => f.name === classification.suggestedFolder)
    decision = {
      finalFolder:     classification.suggestedFolder,
      folderId:        matched?._id ?? null,
      reasoning:       'Fallback to classifier suggestion due to Sonnet error',
      titleSuggestion: event.fileName,
    }
  }

  // Persist classification results
  await patchDocument(documentId, {
    aiStatus:         'classified',
    classified:       true,
    documentType:     classification.docType,
    parties:          classification.parties,
    documentDate:     classification.documentDate ?? undefined,
    summary:          classification.summary,
    tags:             classification.tags,
    aiConfidence:     classification.confidence,
    detectedLanguage: classification.detectedLanguage,
    folderId:         decision.folderId ?? undefined,
    title:            decision.titleSuggestion || event.fileName,
  })

  console.log(`[client-manager] ${batesNumber} done — filed in "${decision.finalFolder}"`)
}

// ─── Express server ───────────────────────────────────────────────────────────

const app = express()
app.use(express.json({ limit: '50mb' }))

app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'ok', service: 'client-manager', model: 'claude-sonnet-4-6' })
})

app.post('/document-uploaded', (req: Request, res: Response) => {
  const parseResult = DocumentUploadedSchema.safeParse(req.body)
  if (!parseResult.success) {
    res.status(400).json({ ok: false, error: parseResult.error.message })
    return
  }

  // Respond 202 immediately so legal-service isn't blocked waiting for AI
  res.status(202).json({ ok: true, message: 'Document queued for classification' })

  // Process asynchronously
  handleDocumentUploaded(parseResult.data).catch(err => {
    console.error(`[client-manager] unhandled error for ${parseResult.data.batesNumber}:`, err)
  })
})

const PORT = parseInt(process.env.PORT ?? '3301', 10)
app.listen(PORT, () => {
  console.log(`[client-manager] listening on :${PORT}`)
})
