/**
 * FormCalibrationAgent
 *
 * Autonomous agent that takes every stock form template, fetches its PDF from the
 * official government source, and uses Claude to produce verified field mappings.
 *
 * Pipeline per form:
 *   1. Download PDF from sourceUrl (cache it locally)
 *   2. Try AcroForm extraction (pdf-lib) → if fields found:
 *        • Build field inventory: name, position, type, page
 *        • Ask Claude to map each field name → our dataSource schema
 *        • Store fillMode:'acroform', acroFieldName per mapping, needsCalibration:false
 *   3. If no AcroForm fields (flat/scanned PDF):
 *        • Extract visible text + rough positions with pdf-parse
 *        • Ask Claude to estimate x/y coordinates for our standard fields
 *        • Store fillMode:'coordinate', coordinates, needsCalibration:true (human verify)
 *        • Mark needsCalibration:true because coordinates are AI-estimated, not exact
 *
 * Trigger:
 *   POST /form-templates/calibrate/run         — calibrate all un-calibrated stock forms
 *   POST /form-templates/calibrate/:id         — calibrate one specific template
 *   GET  /form-templates/calibrate/status      — last run report
 */

import { Injectable, Logger } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { ConfigService } from '@nestjs/config'
import Anthropic from '@anthropic-ai/sdk'
import { PDFDocument, PDFTextField, PDFCheckBox, PDFRadioGroup, PDFDropdown } from 'pdf-lib'
import { FormTemplate, FormTemplateDocument } from '../common/schemas'
import * as fs from 'fs'
import * as path from 'path'

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

export interface AcroFieldInfo {
  name: string
  type: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'unknown'
  page: number        // 1-based
  x: number
  y: number
  width: number
  height: number
}

export interface MappedField {
  fieldId: string
  label: string
  dataSource: string
  staticValue?: string
  dateFormat?: string
  maxLength?: number
  // AcroForm mode
  acroFieldName?: string
  // Coordinate mode
  page?: number
  x?: number
  y?: number
  fontSize?: number
}

export interface CalibrationResult {
  templateId: string
  stockId: string
  name: string
  status: 'success_acroform' | 'success_coordinate' | 'skipped' | 'error'
  fieldsFound: number
  fieldsMapped: number
  fillMode: string
  message: string
  durationMs: number
}

export interface CalibrationReport {
  startedAt: string
  finishedAt: string
  results: CalibrationResult[]
  summary: { total: number; acroform: number; coordinate: number; skipped: number; errors: number }
}

// Available data sources — fed to Claude so it knows the vocabulary
const DATA_SOURCES = `
INDIVIDUAL CLIENT FIELDS:
  client.firstName        — first name of the individual client
  client.lastName         — last name / family name
  client.nationality      — nationality (e.g. "US", "JP", "British")
  client.passportNumber   — passport number or national ID number
  client.dateOfBirth      — date of birth (Date — apply dateFormat if needed)
  client.visaType         — visa category (e.g. "Non-B", "Non-O", "Tourist")
  client.visaNumber       — visa / permit reference number
  client.visaExpiry       — visa / permit expiry date (Date)
  client.address          — current address in Thailand (full string)
  client.email            — primary email address
  client.phone            — primary phone number
  client.idType           — type of ID document
  client.lineId           — LINE messenger ID

CORPORATE CLIENT FIELDS:
  client.companyName      — registered company name
  client.registrationNumber — company registration number (DBD)
  client.boiStatus        — BOI promotion status / project number
  client.directors        — array of director names (use index 0 for first director)

MATTER FIELDS:
  matter.title            — matter title / description
  matter.matterNumber     — internal reference number (e.g. TM.7/2026/0041)
  matter.practiceArea     — practice area (immigration / corporate / etc.)
  matter.subType          — sub-type or job title (for work permit matters)
  matter.openedDate       — date the matter was opened (Date)
  matter.responsibleAttorney — attorney name / ID

SPECIAL:
  static                  — fixed/static text that never changes; provide staticValue
`.trim()

// ─── Agent ────────────────────────────────────────────────────────────────────

@Injectable()
export class FormCalibrationAgent {
  private readonly logger = new Logger(FormCalibrationAgent.name)
  private readonly anthropic: Anthropic
  private readonly MODEL = 'claude-sonnet-4-6'
  private lastReport: CalibrationReport | null = null

  constructor(
    @InjectModel(FormTemplate.name) private templateModel: Model<FormTemplateDocument>,
    private readonly config: ConfigService,
  ) {
    this.anthropic = new Anthropic({
      apiKey: config.get<string>('ANTHROPIC_API_KEY'),
    })
  }

  // ── Public API ─────────────────────────────────────────────────────────────

  getLastReport(): CalibrationReport | null {
    return this.lastReport
  }

  async calibrateAll(forceRecalibrate = false): Promise<CalibrationReport> {
    const filter: any = { isSystem: true }
    if (!forceRecalibrate) filter.needsCalibration = true
    const templates = await this.templateModel.find(filter).lean()

    this.logger.log(`Starting calibration of ${templates.length} stock form(s)`)
    const startedAt = new Date().toISOString()
    const results: CalibrationResult[] = []

    for (const tpl of templates) {
      const result = await this.calibrateOne(String((tpl as any)._id))
      results.push(result)
    }

    const report: CalibrationReport = {
      startedAt,
      finishedAt: new Date().toISOString(),
      results,
      summary: {
        total:      results.length,
        acroform:   results.filter(r => r.status === 'success_acroform').length,
        coordinate: results.filter(r => r.status === 'success_coordinate').length,
        skipped:    results.filter(r => r.status === 'skipped').length,
        errors:     results.filter(r => r.status === 'error').length,
      },
    }
    this.lastReport = report
    this.logger.log(`Calibration complete — ${JSON.stringify(report.summary)}`)
    return report
  }

  async calibrateOne(templateId: string): Promise<CalibrationResult> {
    const t0 = Date.now()
    const template = await this.templateModel.findById(templateId).lean()
    if (!template) {
      return this.result(templateId, '', 'Unknown', 'error', 0, 0, 'coordinate', 'Template not found', t0)
    }
    const tpl = template as any
    const name = tpl.name
    const stockId = tpl.stockId ?? templateId

    if (!tpl.sourceUrl) {
      return this.result(templateId, stockId, name, 'skipped', 0, 0, tpl.fillMode, 'No sourceUrl — manual upload only', t0)
    }

    this.logger.log(`[${stockId}] Calibrating "${name}"`)

    try {
      // Step 1 — Download PDF
      const pdfBytes = await this.downloadPdf(tpl, templateId)
      this.logger.log(`[${stockId}] PDF fetched (${pdfBytes.length} bytes)`)

      // Step 2 — Try AcroForm extraction
      const acroFields = await this.extractAcroFields(pdfBytes)

      if (acroFields.length > 0) {
        this.logger.log(`[${stockId}] AcroForm detected — ${acroFields.length} field(s)`)
        const mapped = await this.mapAcroFieldsWithClaude(name, tpl.description ?? '', acroFields)
        await this.saveAcroMappings(templateId, mapped)
        return this.result(templateId, stockId, name, 'success_acroform', acroFields.length, mapped.length, 'acroform', `Mapped ${mapped.length}/${acroFields.length} AcroForm fields`, t0)
      }

      // Step 3 — Flat PDF: extract text and ask Claude to estimate positions
      this.logger.log(`[${stockId}] No AcroForm fields — falling back to text layout analysis`)
      const textLayout = await this.extractTextLayout(pdfBytes)
      const mapped = await this.mapFlatPdfWithClaude(name, tpl.description ?? '', textLayout, tpl.practiceArea)
      await this.saveCoordinateMappings(templateId, mapped)
      return this.result(templateId, stockId, name, 'success_coordinate', 0, mapped.length, 'coordinate', `Estimated ${mapped.length} coordinate field(s) from text layout (needs verification)`, t0)

    } catch (err: any) {
      this.logger.error(`[${stockId}] Calibration failed: ${err.message}`)
      return this.result(templateId, stockId, name, 'error', 0, 0, 'coordinate', err.message, t0)
    }
  }

  // ── PDF download ──────────────────────────────────────────────────────────

  private async downloadPdf(tpl: any, templateId: string): Promise<Buffer> {
    const stockDir = path.join(process.cwd(), 'uploads', 'form-templates', 'stock')
    if (!fs.existsSync(stockDir)) fs.mkdirSync(stockDir, { recursive: true })

    const safeId = (tpl.stockId ?? templateId).replace(/[^a-zA-Z0-9_-]/g, '_')
    const cachedPath = path.join(stockDir, `${safeId}.pdf`)

    if (fs.existsSync(cachedPath) && fs.statSync(cachedPath).size > 1024) {
      return fs.readFileSync(cachedPath)
    }

    const urls: string[] = [tpl.sourceUrl, ...(tpl.fallbackUrls ?? [])].filter(Boolean)
    let lastErr: any
    for (const url of urls) {
      try {
        this.logger.log(`Fetching: ${url}`)
        const res = await fetch(url, {
          signal: AbortSignal.timeout(20_000),
          headers: { 'User-Agent': 'Mozilla/5.0 (compatible; GPS-Legal-FormBot/1.0)' },
        })
        if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`)
        const bytes = Buffer.from(await res.arrayBuffer())
        if (bytes.length < 1024) throw new Error(`Response too small (${bytes.length} bytes) — likely not a PDF`)
        fs.writeFileSync(cachedPath, bytes)
        await this.templateModel.findByIdAndUpdate(templateId, { storageKey: `stock/${safeId}.pdf` })
        return bytes
      } catch (e) {
        lastErr = e
        this.logger.warn(`Failed to fetch ${url}: ${(e as any).message}`)
      }
    }
    throw new Error(`Could not download PDF: ${lastErr?.message}`)
  }

  // ── AcroForm extraction ───────────────────────────────────────────────────

  private async extractAcroFields(pdfBytes: Buffer): Promise<AcroFieldInfo[]> {
    let pdfDoc: PDFDocument
    try {
      pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true })
    } catch {
      return []
    }

    const pages = pdfDoc.getPages()
    let form: ReturnType<typeof pdfDoc.getForm>
    try {
      form = pdfDoc.getForm()
    } catch {
      return []
    }

    let fields: any[]
    try {
      fields = form.getFields()
    } catch {
      return []
    }

    if (!fields.length) return []

    // Build a page-index lookup: page dict ref → page number (1-based)
    const pageRefToIndex = new Map<string, number>()
    for (let i = 0; i < pages.length; i++) {
      const ref = (pages[i] as any).ref
      if (ref) pageRefToIndex.set(`${ref.objectNumber}:${ref.generationNumber}`, i + 1)
    }

    const result: AcroFieldInfo[] = []
    for (const field of fields) {
      const name = field.getName()
      let type: AcroFieldInfo['type'] = 'unknown'
      if (field instanceof PDFTextField)   type = 'text'
      else if (field instanceof PDFCheckBox) type = 'checkbox'
      else if (field instanceof PDFRadioGroup) type = 'radio'
      else if (field instanceof PDFDropdown)  type = 'dropdown'

      const widgets = (field as any).acroField?.getWidgets?.() ?? []
      for (const widget of widgets) {
        try {
          const rect = widget.getRectangle()
          // Determine page
          const pageRef = widget.P?.()
          let pageNum = 1
          if (pageRef) {
            const key = `${pageRef.objectNumber}:${pageRef.generationNumber}`
            pageNum = pageRefToIndex.get(key) ?? 1
          }
          result.push({ name, type, page: pageNum, x: rect.x, y: rect.y, width: rect.width, height: rect.height })
        } catch {
          // Widget without valid rectangle — skip
        }
      }
    }

    return result
  }

  // ── Text layout extraction (flat PDFs) ───────────────────────────────────

  private async extractTextLayout(pdfBytes: Buffer): Promise<string> {
    try {
      const pdfParse = (await import('pdf-parse')).default
      const data = await pdfParse(pdfBytes, { max: 5 })
      // Return raw text — Claude will reason about structure from this
      return data.text.slice(0, 8000)  // cap to avoid token overflow
    } catch {
      return '[Could not extract text from PDF]'
    }
  }

  // ── Claude: AcroForm field mapping ────────────────────────────────────────

  private readonly ACRO_BATCH_SIZE = 30

  private async mapAcroFieldsWithClaude(
    formName: string,
    description: string,
    acroFields: AcroFieldInfo[],
  ): Promise<MappedField[]> {
    // Batch into chunks to avoid output token limits on forms with many fields
    const chunks: AcroFieldInfo[][] = []
    for (let i = 0; i < acroFields.length; i += this.ACRO_BATCH_SIZE) {
      chunks.push(acroFields.slice(i, i + this.ACRO_BATCH_SIZE))
    }

    const allMapped: MappedField[] = []
    for (let c = 0; c < chunks.length; c++) {
      const chunk = chunks[c]
      const batchNote = chunks.length > 1 ? ` (batch ${c + 1}/${chunks.length})` : ''
      const fieldList = chunk
        .map(f => `  - "${f.name}" (type:${f.type}, page:${f.page}, x:${Math.round(f.x)}, y:${Math.round(f.y)})`)
        .join('\n')

      const prompt = `You are mapping PDF form fields to a legal practice management system's data model.

FORM: ${formName}${batchNote}
CONTEXT: ${description}

PDF ACROFORM FIELDS FOUND:
${fieldList}

AVAILABLE DATA SOURCES:
${DATA_SOURCES}

TASK:
Map each PDF field to the best-matching data source.
- For date fields: include "dateFormat": "DD/MM/YYYY" (Thai government standard)
- For fields with no matching data source: use "static" with an empty staticValue (leave blank for user to fill)
- Ignore button, submit, reset, and purely decorative fields
- "label" should be a clear English description of what the field contains

Return ONLY a valid JSON array, no explanation, no markdown fences:
[
  {
    "fieldId": "unique_snake_case_id",
    "acroFieldName": "<exact name from PDF field list above>",
    "label": "Human-readable field label",
    "dataSource": "client.firstName",
    "dateFormat": "DD/MM/YYYY"  // only for date fields
  },
  ...
]`

      const msg = await this.anthropic.messages.create({
        model: this.MODEL,
        max_tokens: 8096,
        messages: [{ role: 'user', content: prompt }],
      })

      const batch = this.parseClaudeJson(msg.content, `${formName} batch ${c + 1}`)
      allMapped.push(...batch)
    }

    return allMapped
  }

  // ── Claude: flat PDF coordinate estimation ────────────────────────────────

  private async mapFlatPdfWithClaude(
    formName: string,
    description: string,
    textLayout: string,
    practiceArea: string,
  ): Promise<MappedField[]> {
    const prompt = `You are analyzing a flat (non-fillable) government PDF form to estimate where auto-fill text should be placed.

FORM: ${formName}
PRACTICE AREA: ${practiceArea}
CONTEXT: ${description}

PDF TEXT CONTENT (extracted, may be garbled):
${textLayout}

AVAILABLE DATA SOURCES:
${DATA_SOURCES}

TASK:
Based on the form name, context, and extracted text, identify the standard fields this form contains
and estimate their approximate x/y coordinates in PDF coordinate space (bottom-left origin, A4 page = 595x842 pts).

Rules:
- Focus on fillable fields: name, passport, nationality, date of birth, address, visa type/number, etc.
- x typically ranges 140–500 for label-then-field layouts; y depends on vertical position
- Page 1 fields are most important
- For date fields: include "dateFormat": "DD/MM/YYYY"
- Use fontSize 10 as default
- Estimate y positions: top of A4 page ≈ y=800, middle ≈ y=500, bottom ≈ y=100

Return ONLY a valid JSON array, no explanation, no markdown fences:
[
  {
    "fieldId": "unique_snake_case_id",
    "label": "Field label",
    "dataSource": "client.firstName",
    "page": 1,
    "x": 165,
    "y": 640,
    "fontSize": 10,
    "dateFormat": "DD/MM/YYYY"  // only for date fields
  },
  ...
]`

    const msg = await this.anthropic.messages.create({
      model: this.MODEL,
      max_tokens: 8096,
      messages: [{ role: 'user', content: prompt }],
    })

    return this.parseClaudeJson(msg.content, formName)
  }

  // ── DB writes ─────────────────────────────────────────────────────────────

  private async saveAcroMappings(templateId: string, fields: MappedField[]): Promise<void> {
    await this.templateModel.findByIdAndUpdate(templateId, {
      fillMode: 'acroform',
      fields,
      needsCalibration: false,
    })
    this.logger.log(`[${templateId}] Saved ${fields.length} AcroForm mappings`)
  }

  private async saveCoordinateMappings(templateId: string, fields: MappedField[]): Promise<void> {
    await this.templateModel.findByIdAndUpdate(templateId, {
      fillMode: 'coordinate',
      fields,
      needsCalibration: true,   // coordinate estimates always need human sign-off
    })
    this.logger.log(`[${templateId}] Saved ${fields.length} coordinate estimates (needs verification)`)
  }

  // ── Helpers ───────────────────────────────────────────────────────────────

  private parseClaudeJson(content: any[], formName: string): MappedField[] {
    const text = content
      .filter((b: any) => b.type === 'text')
      .map((b: any) => b.text)
      .join('')
      .trim()

    // Strip any accidental markdown fences
    const clean = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim()

    try {
      const parsed = JSON.parse(clean)
      if (!Array.isArray(parsed)) throw new Error('Not an array')
      return parsed
    } catch (e) {
      this.logger.error(`Could not parse Claude response for "${formName}": ${(e as any).message}`)
      this.logger.debug(`Raw response: ${text.slice(0, 500)}`)
      return []
    }
  }

  private result(
    id: string, stockId: string, name: string,
    status: CalibrationResult['status'],
    fieldsFound: number, fieldsMapped: number, fillMode: string,
    message: string, t0: number,
  ): CalibrationResult {
    return { templateId: id, stockId, name, status, fieldsFound, fieldsMapped, fillMode, message, durationMs: Date.now() - t0 }
  }
}
