// server-only — never import from client components
// Classifies blank fields detected by extract-text.ts using gpt-4.1 (text-only prompt).
// Coordinates are already exact from the PDF text layer — AI only determines field type and prefill value.

import OpenAI from 'openai';
import type { BlankField } from './extract-text';
import type { SignatureFieldData, SignatureFieldType } from '@/lib/db/schema';

// Manual JSON schema for gpt-4.1 structured output.
// NOTE: Do NOT use zodResponseFormat — broken with Zod v4 (issues #1540, #1602, #1709).
const CLASSIFICATION_SCHEMA = {
  type: 'object',
  properties: {
    fields: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          index:        { type: 'integer' },
          fieldType:    { type: 'string', enum: ['text', 'initials', 'date', 'client-signature', 'agent-signature', 'agent-initials', 'checkbox'] },
          prefillValue: { type: 'string' },
        },
        required: ['index', 'fieldType', 'prefillValue'],
        additionalProperties: false,
      },
    },
  },
  required: ['fields'],
  additionalProperties: false,
} as const;

// Field height in PDF points by type (used to position the box over the underline)
const FIELD_HEIGHTS: Record<SignatureFieldType, number> = {
  'client-signature': 20,
  'agent-signature':  20,
  'initials':         14,
  'agent-initials':   14,
  'date':             12,
  'text':             12,
  'checkbox':         14,
};

// Width clamping — use the exact measured blank width but stay within these bounds
const SIZE_LIMITS: Record<SignatureFieldType, { minW: number; maxW: number }> = {
  'client-signature': { minW: 80,  maxW: 260 },
  'agent-signature':  { minW: 80,  maxW: 260 },
  'initials':         { minW: 20,  maxW: 80  },
  'agent-initials':   { minW: 20,  maxW: 80  },
  'date':             { minW: 50,  maxW: 130 },
  'text':             { minW: 30,  maxW: 280 },
  'checkbox':         { minW: 14,  maxW: 20  },
};

/**
 * Classify blank fields detected from the PDF text layer.
 * AI receives a compact text description of each blank and its surrounding context.
 * Returns SignatureFieldData[] with exact coordinates from pdfjs + AI-assigned types.
 */
export async function classifyFieldsWithAI(
  blanks: BlankField[],
  client: { name: string | null; propertyAddress: string | null } | null,
): Promise<{ fields: SignatureFieldData[]; textFillData: Record<string, string> }> {
  if (!process.env.OPENAI_API_KEY) {
    throw new Error('OPENAI_API_KEY not configured');
  }

  if (blanks.length === 0) {
    return { fields: [], textFillData: {} };
  }

  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  const clientName      = client?.name            ?? 'Unknown';
  const propertyAddress = client?.propertyAddress ?? 'Unknown';

  // Build a compact text description of every blank for the AI
  const blankDescriptions = blanks.map((b, i) => {
    const parts = [`[${i}] page${b.page}`];
    if (b.rowTotal !== undefined && b.rowTotal > 1) parts.push(`row=${b.rowIndex}/${b.rowTotal}`);
    if (b.contextBefore) parts.push(`before="${b.contextBefore}"`);
    if (b.contextAfter)  parts.push(`after="${b.contextAfter}"`);
    if (b.contextAbove)  parts.push(`above="${b.contextAbove}"`);
    if (b.contextBelow)  parts.push(`below="${b.contextBelow}"`);
    return parts.join(' ');
  }).join('\n');

  const response = await openai.chat.completions.create({
    model: 'gpt-4.1',
    messages: [
      {
        role: 'system',
        content: `You are classifying blank lines in a Utah real estate document.
Each blank is a sequence of underscores (or a bracket box) found in the PDF. You are given the text immediately before and after it on the same line, plus the lines above and below.

Client name: ${clientName}
Property address: ${propertyAddress}

━━━ FIELD TYPE RULES ━━━

"text" — any fill-in blank for written information: names, company, address, price, MLS#, term,
  percentage, phone, or any other data field.
  → Use for ALL inline blanks surrounded by sentence text.
  → Use for Address/Phone blanks even when they appear on the same row as signature blanks.

"date" — a date blank.
  → *** HIGHEST PRIORITY RULE: if the last word(s) in contextBefore are "Date" or "date",
    classify as "date". This overrides ALL other rules including "initials". ***
  → Also use when contextBefore ends with a date-like label ("day of", month/year).
  → A footer like "Seller's Initials [  ] Date ___" has TWO blanks: the bracket = "initials",
    the underscore AFTER "Date" = "date". Never let "Seller's Initials" earlier in the line
    override the immediate "Date" label right before the underscore blank.

"client-signature" — ONLY for a blank on its OWN DEDICATED LINE where a buyer or seller physically signs.
  The line will have NO sentence text before or after the blank — just a long underline.
  The label "(Seller's Signature)", "(Buyer's Signature)", or similar appears on the line BELOW the blank
  (in contextBelow) or immediately beside it.
  → NEVER use for inline blanks inside a sentence.
  → NEVER use for Address/Phone blanks even when they are on the same row as a signature.
  → When a signature block has TWO rows of blanks (two sellers), BOTH rows get "client-signature"
    for their signature blank. Look for "(Seller's Signature)" in contextAbove OR contextBelow.

"agent-signature" — ONLY for a blank on its OWN DEDICATED LINE for the listing agent or broker.
  Look for "(Agent's Signature)", "(Broker)", "Seller's Agent", or "by:" in contextAbove/contextBelow.
  → NEVER use for inline blanks inside a sentence.

"initials" — a short blank or bracket box labeled "Seller's Initials" or "Buyer's Initials".
  These appear at the bottom margin of each page.
  → "Seller's Initials" → "initials" (this is the CLIENT/seller, not the agent)
  → "Buyer's Initials" → "initials"
  → The blank immediately after the word "Date" on the same line → "date", not "initials"

"agent-initials" — ONLY for blanks explicitly labeled for the AGENT's initials.

"checkbox" — a small tick-box [ ] embedded inside sentence text.
  These appear as bracket items mid-sentence: "[ ] ARE [ ] ARE NOT", "[ ] IS [ ] IS NOT",
  "check applicable box: [ ] County Records [ ] Appraisal", etc.
  → contextBefore and contextAfter BOTH contain real sentence words (not blank labels).
  → These are NOT form-fill blanks — they are selection checkboxes and must be ignored.
  → ALWAYS classify inline bracket blanks surrounded by sentence text as "checkbox".

━━━ SIGNATURE BLOCK ROWS ━━━
Real estate signature blocks have multiple blanks on the same line, e.g.:
  [sig blank]   [address/phone blank]   [date blank]
  (Seller's Signature)  (Address/Phone)  (Date)

When a blank is part of a multi-blank row its description includes row=N/T,
where N is its 1-indexed position and T is the total blanks on that line.
  row=1/3 = first of three   row=2/3 = middle   row=3/3 = last of three

Use row position + contextBelow to classify each blank:
- row=1/T (first) + contextBelow has "(Seller's Signature)" or "(Buyer's Signature)" → "client-signature"
- row=1/T (first) + contextBelow has agent/broker label → "agent-signature"
- row=N/T where 1 < N < T (middle) → "text" (Address/Phone)
- row=T/T (last) + contextBelow contains "(Date)" → "date"
- row=1/1 (only blank on row) → use contextBefore / contextBelow label rules below

━━━ QUICK DECISION TREE ━━━
1. contextBefore ends with "Date" or "date"? → "date"
   *** This is the ONLY way a blank becomes "date" via contextBefore. ***

2. contextBefore ends with "Initials" (e.g. "Seller's Initials", "Buyer's Initials")? → "initials"
   *** This fires BEFORE the text/checkbox rules. Do NOT reclassify as "text" just because
       contextAfter also contains the word "Date". The Date label is AFTER this blank — it labels
       the NEXT blank, not this one. ***

3. Blank is a bracket [ ] with real sentence words both before AND after (not "Initials")? → "checkbox"
4. Blank is inside a sentence (real label words both before AND after)? → "text"
5. contextBefore ends with "Address" or "Phone" or "Address/Phone"? → "text"
6. row=T/T (last on row, T>1) AND contextBelow contains "(Date)"? → "date"
7. row=1/T (first on row, T>1) AND contextBelow contains "(Seller's Signature)" or "(Buyer's Signature)"? → "client-signature"
8. row=1/T (first on row, T>1) AND contextBelow contains agent/broker label? → "agent-signature"
9. row=N/T where 1 < N < T (middle on row)? → "text"
10. Everything else? → "text"

━━━ FOOTER INITIALS/DATE PATTERN ━━━
Page footers contain: "Seller's Initials [initials-blank] Date [date-blank]"
Two blanks, two rules:
- contextBefore ends with "Initials" → "initials"  ← rule 2 above already handles this
- contextBefore ends with "Date"    → "date"        ← rule 1 above already handles this
The word "Date" in contextAfter does NOT make a blank a date field.
The word "Date" in contextBefore DOES make a blank a date field.

━━━ PREFILL ━━━
- Blank clearly for client/buyer/seller NAME (not address) → prefillValue = "${clientName}"
- Blank clearly for PROPERTY address (listing address, not a signature block Address/Phone field) → prefillValue = "${propertyAddress}"
- "Address/Phone" blanks in signature blocks are for the signer's personal contact info → prefillValue = ""
- Everything else → prefillValue = ""

Return a classification for EVERY blank index. Do not skip any.`,
      },
      {
        role: 'user',
        content: blankDescriptions,
      },
    ],
    response_format: {
      type: 'json_schema',
      json_schema: {
        name: 'field_classification',
        strict: true,
        schema: CLASSIFICATION_SCHEMA,
      },
    },
  });

  const raw = JSON.parse(response.choices[0].message.content!) as {
    fields: Array<{ index: number; fieldType: string; prefillValue: string }>;
  };
  // Deterministic post-processing: override AI classifications that are
  // unambiguously wrong based on structural context, regardless of AI output.
  for (const result of raw.fields) {
    const blank = blanks[result.index];
    if (!blank) continue;
    const before    = blank.contextBefore.trim();
    const lastWord  = before.split(/\s+/).pop()?.toLowerCase() ?? '';
    const lastTwo   = before.split(/\s+/).slice(-2).join(' ').toLowerCase();
    const rowIdx    = blank.rowIndex ?? 1;
    const rowTot    = blank.rowTotal ?? 1;

    // Rule A — footer: "Date ___" → always date
    if (lastWord === 'date') {
      result.fieldType = 'date';

    // Rule B — footer: "Seller's/Buyer's Initials [  ]" → always initials
    } else if (lastWord === 'initials' || lastTwo === "seller's initials" || lastTwo === "buyer's initials") {
      result.fieldType = 'initials';

    // Rule C — signature rows: only the FIRST blank on a multi-blank row can be a
    // signature. Middle and last blanks are address/phone (text) or date — never signatures.
    } else if (rowTot > 1 && rowIdx > 1 &&
               (result.fieldType === 'client-signature' || result.fieldType === 'agent-signature')) {
      // Last blank on the row → date if contextBelow labels it "(Date)", else text
      if (rowIdx === rowTot && blank.contextBelow.includes('(Date)')) {
        result.fieldType = 'date';
      } else {
        result.fieldType = 'text';
      }

    // Rule D — addr/phone blank separated onto its own detected line (rowTotal=2, rowIndex=1):
    // When y-position drift splits a 3-blank signature row, the sig blank ends up row=1/1
    // and addr+date end up as row=1/2 and row=2/2. The row=1/2 blank has contextBelow
    // containing "(Address/Phone)" and "(Date)" but no sig blank before it — it's the
    // address/phone field, not a signature.
    } else if (rowTot === 2 && rowIdx === 1 &&
               (result.fieldType === 'client-signature' || result.fieldType === 'agent-signature') &&
               blank.contextBelow.includes('(Address/Phone)') &&
               blank.contextBelow.includes('(Date)') &&
               !blank.contextBelow.includes('(Seller') &&
               !blank.contextBelow.includes('(Buyer')) {
      result.fieldType = 'text';
    }
  }

  const fields: SignatureFieldData[] = [];
  const textFillData: Record<string, string> = {};

  for (const result of raw.fields) {
    if (result.fieldType === 'checkbox') continue;
    const fieldType = result.fieldType as SignatureFieldType;
    const blank = blanks[result.index];
    if (!blank) continue;

    const height = FIELD_HEIGHTS[fieldType] ?? 12;
    const lim    = SIZE_LIMITS[fieldType] ?? SIZE_LIMITS['text'];
    const width  = Math.max(lim.minW, Math.min(blank.width, lim.maxW));

    // Position the field box so it sits ON the underscore line.
    // blank.y is the text baseline (PDF bottom-left origin). Underscores descend just below
    // the baseline, so anchor the field bottom 2pt below baseline and extend upward by height.
    // This keeps the box tight to the underline without climbing into the text above.
    const y = Math.max(0, blank.y - 2);

    const id = crypto.randomUUID();
    fields.push({ id, page: blank.page, x: blank.x, y, width, height, type: fieldType });

    if (fieldType === 'text' && result.prefillValue) {
      textFillData[id] = result.prefillValue;
    }
  }

  return { fields, textFillData };
}
