// AI-assisted email classifier — augments the existing heuristic ingest
// pipeline with Claude Haiku 4.5 for emails the regex/sender-domain
// heuristic can't link on its own.
//
// Design:
//   1. Heuristic runs first (regex booking_ref, PNR, sender domain). If it
//      finds a high-confidence match, AI is skipped — saves cost on the
//      easy 80% of emails.
//   2. If heuristic returns nothing or low confidence, build a candidate
//      list of recent bookings + supplier/customer hints and ask Claude
//      to pick the best match. Cap candidates at ~50 most recent.
//   3. Claude returns structured JSON: {booking_code, confidence,
//      classification, reason}. Confidence is its own self-assessment 0-1.
//   4. Caller decides what to do with the result based on env-configured
//      thresholds (auto-link / suggest / ignore).
//
// Cost control:
//   - Daily quota tracked in `ai_classification_log` table (per UTC day).
//   - Once exhausted, classifier short-circuits to disabled state.
//   - Prompt caching is enabled on the system prompt (re-used across
//     calls; ~80% input-token savings on the long instructions block).
//
// Privacy: only the email's subject + first 800 chars of body + sender
// addresses are sent. No attachments, no headers, no full body. Anthropic
// retains zero data by default on the API and doesn't train on it.

const Anthropic = require('@anthropic-ai/sdk').default || require('@anthropic-ai/sdk');

const MODEL = 'claude-haiku-4-5';
const MAX_CANDIDATES = 50;
const MAX_BODY_CHARS = 800;
const SYSTEM_PROMPT = `You are an email classification assistant for a travel operations console. Your job: given an email and a list of candidate bookings, decide which booking the email refers to AND who sent it (the customer, the supplier, or someone else).

Output rules:
- Reply with ONE line of valid JSON. No prose before/after. No markdown fences.
- Schema: {"booking_code": string|null, "confidence": number, "classification": "customer"|"supplier"|"internal"|"unknown", "reason": string}
- "booking_code" must EXACTLY match a code from the candidate list, or be null if no candidate fits.
- "confidence" is your own confidence in BOTH the booking match AND the classification, on a 0-1 scale where:
    1.0 = explicit booking ref + sender clearly identifies role
    0.85+ = strong implicit match (supplier name, hotel name, customer name + dates align)
    0.5-0.85 = plausible match but ambiguous, would benefit from human confirmation
    <0.5 = guessing, don't risk auto-link
- If the email mentions multiple bookings, pick the one most central to the message.
- "reason" is one short sentence explaining what evidence led to your match (cite specifics, e.g. "Sender domain matches PARKROYAL Kuala Lumpur, the supplier on MHH003675").
- If no candidate is a real match, set booking_code=null, confidence=0.0, classification="unknown".

Travel context cues:
- Suppliers send: confirmation numbers (HCN), PNRs, voucher attachments, room change notices, payment receipts to ops.
- Customers send: questions, cancellation requests, complaints, photos. Often from gmail/hotmail/yahoo.
- Senders matching app domains (e.g. @tripovo.com, @easygds.com) are typically internal — use "internal".`;

/**
 * Build the user prompt — the email summary + candidate list.
 * Kept short to minimise input tokens.
 */
function buildUserPrompt({ email, candidates }) {
  const subject = (email.subject || '').slice(0, 200);
  const snippet = (email.body_text || email.snippet || '').slice(0, MAX_BODY_CHARS);
  const fromEmail = email.from_email || '';
  const fromName = email.from_name || '';
  const toEmails = Array.isArray(email.to_emails) ? email.to_emails.slice(0, 5).join(', ') : (email.to_emails || '');

  const candidateLines = candidates.slice(0, MAX_CANDIDATES).map(b => {
    const ccode = b.code || b.id;
    const cust = [b.contact_first_name, b.contact_last_name].filter(Boolean).join(' ') || b.contact_name || b.contact_email || '';
    const supplier = b.supplier_name || '';
    const date = b.travel_date ? String(b.travel_date).slice(0, 10) : '';
    const status = b.booking_status || '';
    return `${ccode} | ${cust} | ${supplier} | ${date} | ${status}`;
  });

  return `EMAIL TO CLASSIFY
From: ${fromName ? fromName + ' <' + fromEmail + '>' : fromEmail}
To: ${toEmails}
Subject: ${subject}
Body (first ${MAX_BODY_CHARS} chars): ${snippet}

CANDIDATE BOOKINGS (code | customer | supplier | travel_date | status):
${candidateLines.join('\n') || '(none — DB cache empty)'}

Classify and respond with JSON only.`;
}

/** Strict JSON parser tolerant of fenced code blocks. */
function parseClaudeJson(text) {
  if (!text) return null;
  let s = String(text).trim();
  // Strip markdown code fences if Claude added them despite instructions
  s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
  // Take the first {...} block to be safe
  const m = s.match(/\{[\s\S]*\}/);
  if (!m) return null;
  try { return JSON.parse(m[0]); } catch { return null; }
}

/**
 * Classify a single email. Returns:
 *   { skipped: true, reason }   — when AI is disabled, budget hit, or no key
 *   { skipped: false, booking_code, confidence, classification, reason,
 *     usage: { input_tokens, output_tokens, cache_read_input_tokens } }
 */
async function classifyEmailWithAI({ email, candidates, pool }) {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) return { skipped: true, reason: 'ANTHROPIC_API_KEY not set' };

  const dailyBudget = parseInt(process.env.AI_DAILY_BUDGET || '500', 10);
  if (!Number.isFinite(dailyBudget) || dailyBudget <= 0) {
    return { skipped: true, reason: 'AI_DAILY_BUDGET set to 0' };
  }

  // Budget check — atomic-ish: read counter for today, deny if exceeded.
  // The increment happens after a successful API call so a hung call
  // doesn't burn budget. UTC day boundary.
  if (pool) {
    try {
      const today = new Date().toISOString().slice(0, 10);
      const [[row]] = await pool.query(
        'SELECT calls FROM ai_classification_log WHERE day = ?',
        [today]
      );
      if (row && row.calls >= dailyBudget) {
        return { skipped: true, reason: `daily budget ${dailyBudget} exhausted` };
      }
    } catch (e) {
      console.warn('AI budget check failed (proceeding):', e.message);
    }
  }

  const client = new Anthropic({ apiKey });

  // Build the request. System prompt is marked for prompt caching so
  // subsequent calls re-use the cached tokens (Anthropic charges 10% of
  // normal input rate for cache reads). The user message stays
  // per-request because it includes the unique email + candidates.
  let resp;
  try {
    resp = await client.messages.create({
      model: MODEL,
      max_tokens: 256,
      system: [{
        type: 'text',
        text: SYSTEM_PROMPT,
        cache_control: { type: 'ephemeral' },
      }],
      messages: [{
        role: 'user',
        content: buildUserPrompt({ email, candidates }),
      }],
    });
  } catch (e) {
    return { skipped: true, reason: `Claude API error: ${e.message}`, error: e };
  }

  // Extract the text response
  const block = (resp.content || []).find(b => b.type === 'text');
  const raw = block?.text || '';
  const parsed = parseClaudeJson(raw);

  // Increment the daily counter regardless of parse success — we paid
  // for the call either way.
  if (pool) {
    try {
      const today = new Date().toISOString().slice(0, 10);
      await pool.query(
        `INSERT INTO ai_classification_log (day, calls, input_tokens, output_tokens, cache_read_tokens)
         VALUES (?, 1, ?, ?, ?)
         ON DUPLICATE KEY UPDATE
           calls = calls + 1,
           input_tokens = input_tokens + VALUES(input_tokens),
           output_tokens = output_tokens + VALUES(output_tokens),
           cache_read_tokens = cache_read_tokens + VALUES(cache_read_tokens)`,
        [today,
         resp.usage?.input_tokens || 0,
         resp.usage?.output_tokens || 0,
         resp.usage?.cache_read_input_tokens || 0]
      );
    } catch (e) {
      console.warn('AI budget increment failed:', e.message);
    }
  }

  if (!parsed) {
    return {
      skipped: false,
      booking_code: null,
      confidence: 0,
      classification: 'unknown',
      reason: `Claude returned unparseable response: ${raw.slice(0, 200)}`,
      usage: resp.usage || {},
    };
  }

  // Normalise the result
  return {
    skipped: false,
    booking_code: parsed.booking_code || null,
    confidence: Math.max(0, Math.min(1, Number(parsed.confidence) || 0)),
    classification: ['customer', 'supplier', 'internal', 'unknown'].includes(parsed.classification)
      ? parsed.classification
      : 'unknown',
    reason: String(parsed.reason || '').slice(0, 500),
    usage: resp.usage || {},
  };
}

module.exports = {
  classifyEmailWithAI,
  // Exported for tests / inspection
  buildUserPrompt,
  parseClaudeJson,
  SYSTEM_PROMPT,
  MODEL,
};
