/**
 * Engine 2 — LLM analysis layer on top of Engine 1 grades.
 *
 * Engine 2 doesn't REPLACE Engine 1. It runs after Engine 1 produces a
 * grade for an A/B-tier prop, applies natural-language reasoning over the
 * full feature vector + trap signals, and either agrees or disagrees.
 * Disagreement is itself a signal — surface it in the UI so users can
 * see when our two systems diverge.
 *
 * Architecture choices:
 *   - Async + non-blocking. Engine 1 returns immediately; Engine 2 fills
 *     in 5-30 seconds later via the queue.
 *   - Queue is in-memory (Map keyed by gradeId). On process restart we
 *     lose the queue, which is acceptable — n8n can re-queue from
 *     grade_history WHERE engine2_analyzed_at IS NULL.
 *   - Only A/B-tier props qualify. C/D/F grades skip Engine 2 entirely;
 *     they're already flagged as low-confidence and don't need narrative.
 *   - Prompt is GENERIC — no 'VYNDR' brand string. The model has no idea
 *     who we are. That keeps our system prompt out of any provider's
 *     training/QA pipeline.
 */

const openRouter = require('../adapters/openRouterAdapter');
const { getSupabaseServiceClient } = require('../../utils/supabase');

const BATCH_SIZE = Number(process.env.ENGINE2_BATCH_SIZE) || 10;
const ENABLED = String(process.env.ENGINE2_ENABLED || 'true').toLowerCase() !== 'false';

// Grades that qualify for Engine 2 analysis. C/D/F skip.
const ELIGIBLE_GRADES = new Set(['A+', 'A', 'A-', 'B+', 'B', 'B-']);
const VALID_GRADES = new Set([
  'A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D', 'F', null,
]);

// In-process FIFO queue. Map preserves insertion order — values carry the
// context needed to build the prompt without re-querying upstream.
const queue = new Map();

const SYSTEM_MESSAGE = (
  "You are a sports analytics engine analyzing player prop bets. "
  + "Respond ONLY with valid JSON. No preamble, no markdown, no explanation "
  + "outside the JSON structure. If you cannot analyze this prop, respond "
  + 'with { "grade": null, "reason": "insufficient data" }.'
);

function buildPrompt(ctx) {
  const features = ctx.features || {};
  const trapSignals = ctx.trap?.signals || {};
  const recent = ctx.recentGames || [];

  const featureLines = Object.entries(features)
    .map(([k, v]) => {
      if (typeof v === 'number') {
        return `${k}: ${Number.isInteger(v) ? v : v.toFixed(2)}`;
      }
      return `${k}: ${v}`;
    })
    .join('\n');

  const activeTraps = Object.entries(trapSignals)
    .filter(([, s]) => s?.active && s?.score > 0)
    .map(([name, s]) => `- ${name}: ${s.score.toFixed(2)} (${s.explanation})`)
    .join('\n') || 'none';

  const recentLines = recent
    .map((g) => `  ${g.date}: ${g.value} vs ${g.opponent}${g.home ? ' (home)' : ''}`)
    .join('\n') || '  (no recent games)';

  return [
    `PLAYER: ${ctx.player_name} (${ctx.team || 'unknown'})`,
    `SPORT: ${ctx.sport}`,
    `PROP: ${ctx.direction} ${ctx.line} ${ctx.stat_type}`,
    `GAME: ${ctx.away_team || '?'} @ ${ctx.home_team || '?'}, ${ctx.game_date || '?'}`,
    '',
    'FEATURES:',
    featureLines || '  (no features computed)',
    '',
    `ENGINE 1 GRADE: ${ctx.engine1_grade} (${(ctx.engine1_factors || []).slice(0, 3).join(', ') || 'no factors'})`,
    '',
    'TRAP SIGNALS:',
    activeTraps,
    `Trap composite: ${(ctx.trap?.composite ?? 0).toFixed(2)} (${ctx.trap?.recommendation || 'unknown'})`,
    '',
    `CONSISTENCY: ${ctx.consistency?.consistency || 'unknown'} (cv=${(ctx.consistency?.cv ?? 0).toFixed(2)}, score=${(ctx.consistency?.score ?? 0).toFixed(2)})`,
    '',
    ...(ctx.probability && Number.isFinite(ctx.probability.p_over) ? [
      `PROBABILITY: P(Over) = ${ctx.probability.p_over.toFixed(2)} | P(Under) = ${(1 - ctx.probability.p_over).toFixed(2)}`,
      `Components: ${
        Object.entries(ctx.probability.components || {})
          .filter(([, v]) => Number.isFinite(Number(v)))
          .map(([k, v]) => `${k}=${Number(v).toFixed(2)}`)
          .join(', ') || 'none'
      }`,
      '',
    ] : []),
    'RECENT PERFORMANCE:',
    recentLines,
    '',
    'Analyze this prop and respond with:',
    '{',
    '  "grade": "A+/A/A-/B+/B/B-/C+/C/C-/D/F",',
    '  "confidence": 0.0-1.0,',
    '  "agrees_with_engine1": true/false,',
    '  "narrative": "2-3 sentence analysis",',
    '  "trap_concern": "specific trap risk if any, or null",',
    '  "key_factor": "single most important factor"',
    '}',
  ].join('\n');
}

// Four-strategy parser. The model is supposed to return raw JSON, but
// "supposed to" is doing a lot of work — we layer fallbacks so a chatty
// model doesn't make us drop the whole analysis. Strategy 4 (regex field
// extraction) is the last-ditch — at least we capture the grade.
function parseResponse(raw) {
  if (!raw || typeof raw !== 'string') return null;

  // 1. Direct parse.
  try {
    const j = JSON.parse(raw.trim());
    if (j && typeof j === 'object') return j;
  } catch { /* fall through */ }

  // 2. Markdown fenced block.
  const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
  if (fence?.[1]) {
    try {
      const j = JSON.parse(fence[1].trim());
      if (j && typeof j === 'object') return j;
    } catch { /* fall through */ }
  }

  // 3. First {...} block.
  const obj = raw.match(/\{[\s\S]*\}/);
  if (obj) {
    try {
      const j = JSON.parse(obj[0]);
      if (j && typeof j === 'object') return j;
    } catch { /* fall through */ }
  }

  // 4. Field-level regex extraction — last resort. We at least want the
  // grade letter; the narrative becomes a flag string so the row is
  // distinguishable from a model that returned valid JSON.
  const gradeMatch = raw.match(/["']?grade["']?\s*[:=]\s*["']?([A-F][+-]?)/i);
  if (gradeMatch) {
    const confMatch = raw.match(/["']?confidence["']?\s*[:=]\s*([\d.]+)/i);
    const conf = confMatch ? parseFloat(confMatch[1]) : NaN;
    return {
      grade: gradeMatch[1].toUpperCase(),
      confidence: Number.isFinite(conf) && conf >= 0 && conf <= 1 ? conf : 0.5,
      narrative: 'Extracted from malformed response',
      agrees_with_engine1: null,
      key_factor: null,
      trap_concern: null,
    };
  }
  return null;
}

function validateAnalysis(parsed) {
  if (!parsed) return null;
  // Allow the explicit "I can't" response.
  if (parsed.grade === null) return { grade: null, reason: parsed.reason || 'insufficient data' };

  if (!VALID_GRADES.has(parsed.grade)) return null;
  const confidence = Number(parsed.confidence);
  if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) return null;
  const narrative = typeof parsed.narrative === 'string' ? parsed.narrative.slice(0, 500) : null;
  if (!narrative || narrative.length === 0) return null;
  return {
    grade: parsed.grade,
    confidence,
    narrative,
    agrees_with_engine1: !!parsed.agrees_with_engine1,
    trap_concern: typeof parsed.trap_concern === 'string' ? parsed.trap_concern.slice(0, 300) : null,
    key_factor: typeof parsed.key_factor === 'string' ? parsed.key_factor.slice(0, 200) : null,
  };
}

function queueAnalysis(gradeId, propContext) {
  if (!ENABLED) return;
  if (!gradeId || !propContext) return;
  if (!ELIGIBLE_GRADES.has(propContext.engine1_grade)) return;
  // De-dupe by gradeId — re-queuing on retry is fine; we just overwrite.
  queue.set(gradeId, propContext);
}

function getQueueSize() {
  return queue.size;
}

function clearQueue() {
  queue.clear();
}

async function persistResult(gradeId, analysis, modelUsed, latencyMs) {
  const supabase = getSupabaseServiceClient();
  const patch = {
    engine2_grade: analysis.grade,
    engine2_confidence: analysis.confidence,
    engine2_narrative: analysis.narrative,
    engine2_agrees: analysis.agrees_with_engine1,
    engine2_key_factor: analysis.key_factor,
    engine2_trap_concern: analysis.trap_concern,
    engine2_model: modelUsed,
    engine2_latency_ms: latencyMs,
    engine2_analyzed_at: new Date().toISOString(),
  };
  const { error } = await supabase.from('grade_history').update(patch).eq('id', gradeId);
  if (error) {
    console.warn('[engine2] persist failed for', gradeId, error.message);
  }
}

async function analyzeOne(gradeId, propContext) {
  const userPrompt = buildPrompt(propContext);
  const result = await openRouter.analyze(SYSTEM_MESSAGE, userPrompt);
  if (!result) return { ok: false, reason: 'openrouter unavailable' };

  const parsed = parseResponse(result.response);
  const analysis = validateAnalysis(parsed);
  if (!analysis) return { ok: false, reason: 'parse/validate failed' };
  if (analysis.grade === null) return { ok: false, reason: analysis.reason };

  await persistResult(gradeId, analysis, result.modelUsed, result.latencyMs);
  return { ok: true, analysis, modelUsed: result.modelUsed, latencyMs: result.latencyMs };
}

async function processQueue() {
  if (!ENABLED) return { processed: 0, succeeded: 0, failed: 0 };
  let processed = 0;
  let succeeded = 0;
  let failed = 0;
  for (const [gradeId, ctx] of queue.entries()) {
    if (processed >= BATCH_SIZE) break;
    queue.delete(gradeId);
    processed += 1;
    try {
      const res = await analyzeOne(gradeId, ctx);
      if (res.ok) succeeded += 1; else failed += 1;
    } catch (err) {
      console.warn('[engine2] analyze threw for', gradeId, err.message);
      failed += 1;
    }
  }
  return { processed, succeeded, failed, remaining: queue.size };
}

module.exports = {
  queueAnalysis,
  processQueue,
  getQueueSize,
  clearQueue,
  __internals: {
    buildPrompt,
    parseResponse,
    validateAnalysis,
    analyzeOne,
    persistResult,
    queue,
    SYSTEM_MESSAGE,
    ELIGIBLE_GRADES,
    VALID_GRADES,
    BATCH_SIZE,
  },
};
