/**
 * services/ai.js
 * MiniMax AI — fallback classification. Every call is logged to ai_calls.
 */

'use strict';

const keyRotator = require('./keyRotator');
const alertBus   = require('./alertBus');
const { db }     = require('../config/db');

// MiniMax_API_URL is the BASE URL — we append /chat/completions ourselves.
// We defensively strip a trailing /chat/completions if the operator pasted
// a full endpoint into .env, so the final URL is always correct.
function buildEndpoint() {
  const raw = (process.env.MiniMax_API_URL || 'https://api.minimax.io/v1')
    .replace(/\/+$/, '')
    .replace(/\/chat\/?(completions)?$/i, '');
  return `${raw}/chat/completions`;
}
const MODEL   = process.env.MiniMax_MODEL   || 'MiniMax-M2.7';

async function classifyNcii({ url, title, snippet, imageB64 = null }) {
  const start = Date.now();
  let keyId = null;
  let aiCallId = null;
  let tokensIn = 0;
  let tokensOut = 0;
  let costEstimate = 0;

  const systemPrompt = 'You are an NSFW / NCII triage classifier. Reply only with strict JSON like {"isNcii":boolean,"confidence":0.0-1.0,"reason":"short"}.';
  const userPrompt = buildUserPrompt({ url, title, snippet, hasImage: !!imageB64 });

  const messages = [{ role: 'system', content: systemPrompt }];
  if (imageB64) {
    messages.push({
      role: 'user',
      content: [
        { type: 'text', text: userPrompt },
        { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${imageB64}` } },
      ],
    });
  } else {
    messages.push({ role: 'user', content: userPrompt });
  }

  try {
    const { result, key } = await keyRotator.runWithRotation('MiniMax', async (k) => {
      keyId = k.id;
      // API_URL is the BASE — we append /chat/completions ourselves.
      const res = await fetch(buildEndpoint(), {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${k.key_value}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: MODEL,
          messages,
          temperature: 0.1,
          max_tokens: 800,
          response_format: { type: 'json_object' },
        }),
      });
      if (!res.ok) {
        const t = await res.text();
        throw new Error(`MiniMax ${res.status}: ${t.slice(0, 200)}`);
      }
      return res.json();
    });
    tokensIn   = result.usage?.prompt_tokens     || 0;
    tokensOut  = result.usage?.completion_tokens || 0;
    costEstimate = estimateCost(tokensIn, tokensOut, MODEL);

    const content = result.choices?.[0]?.message?.content || '{}';
    let parsed;
    try { parsed = JSON.parse(content); }
    catch {
      const m = content.match(/\{[\s\S]*\}/);
      parsed = m ? JSON.parse(m[0]) : { isNcii: false, confidence: 0 };
    }
    aiCallId = logAiCall({
      service: 'MiniMax', keyId, endpoint: 'chat/completions',
      tokensIn, tokensOut, costEstimate,
      tier3Reason: 'text', success: 1, responseMs: Date.now() - start,
    });
    return {
      isNcii: !!parsed.isNcii,
      confidence: clamp(parseFloat(parsed.confidence) || 0, 0, 1),
      reason: parsed.reason || '',
      ai_call_id: aiCallId,
    };
  } catch (err) {
    logAiCall({
      service: 'MiniMax', keyId, endpoint: 'chat/completions',
      tokensIn, tokensOut, costEstimate,
      tier3Reason: 'text', success: 0, responseMs: Date.now() - start, error: err.message,
    });
    throw err;
  }
}

function buildUserPrompt({ url, title, snippet, hasImage }) {
  return `Does the following URL/page indicate non-consensual intimate imagery (NCII) — i.e. real, identifiable people in sexual contexts shared without consent, including revenge porn, leaked OnlyFans, or non-consensual recording?
${hasImage ? 'An image is attached for visual analysis.' : ''}
URL: ${url}
Title: ${title || '(no title)'}
Snippet: ${snippet || '(no snippet)'}

Reply only JSON: {"isNcii": boolean, "confidence": 0.0-1.0, "reason": "≤15 words"}`;
}

/**
 * Categorize a finding as one of the operator-visible categories.
 * Used by the manual "Classify with AI" button and by the auto-classify
 * hook on tier-3 confirms. Re-uses the same keyRotator + ai_calls log
 * as classifyNcii so quota accounting stays consistent.
 *
 * Categories (mirrored in public/js/findings.js for badges):
 *   name_only             — page mentions the name in non-intimate context
 *   unauthorized_video    — NCII video, no consent
 *   unauthorized_image    — NCII image, no consent
 *   unauthorized_other    — NCII, type not clear (text post, gallery of thumbnails, etc.)
 *   not_ncii              — no NCII content, can be ignored
 *   unknown               — model is not sure
 */
async function categorize({ url, title, snippet, bodyText = null, victimName = null }) {
  const start = Date.now();
  let keyId = null;
  let aiCallId = null;
  let tokensIn = 0, tokensOut = 0, costEstimate = 0;

  const systemPrompt = 'You are an NCII triage classifier for a takedown tool. Read the page summary below and decide the content category. Reply only with strict JSON. No prose.';
  const userPrompt = `Categorize this page.

URL: ${url}
Title: ${title || '(no title)'}
Snippet: ${snippet || '(no snippet)'}
${victimName ? `Depicted person / name of interest: ${victimName}` : ''}
${bodyText ? `\nBody excerpt (first 800 chars):\n${bodyText.slice(0, 800)}` : ''}

Choose exactly one of:
  - name_only            (page uses the name but contains no NCII)
  - unauthorized_video   (NCII video, no consent)
  - unauthorized_image   (NCII image, no consent)
  - unauthorized_other   (NCII, type not clear — text, gallery, etc.)
  - not_ncii             (no NCII; false positive)
  - unknown              (cannot tell from the inputs)

Reply only JSON: {"category":"<one of the above>","confidence":0.0-1.0,"evidence":"≤120-char quoted phrase from the snippet/body that supports your decision","summary":"≤25 words plain-English why"}`;

  const messages = [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userPrompt },
  ];

  const allowed = new Set(['name_only','unauthorized_video','unauthorized_image','unauthorized_other','not_ncii','unknown']);

  try {
    const { result, key } = await keyRotator.runWithRotation('MiniMax', async (k) => {
      keyId = k.id;
      // API_URL is the BASE — we append /chat/completions ourselves.
      const res = await fetch(buildEndpoint(), {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${k.key_value}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: MODEL,
          messages,
          temperature: 0.1,
          max_tokens: 800,
          response_format: { type: 'json_object' },
        }),
      });
      if (!res.ok) {
        const t = await res.text();
        throw new Error(`MiniMax ${res.status}: ${t.slice(0, 200)}`);
      }
      return res.json();
    });
    tokensIn   = result.usage?.prompt_tokens     || 0;
    tokensOut  = result.usage?.completion_tokens || 0;
    costEstimate = estimateCost(tokensIn, tokensOut, MODEL);

    const content = result.choices?.[0]?.message?.content || '{}';
    let parsed;
    try { parsed = JSON.parse(content); }
    catch {
      const m = content.match(/\{[\s\S]*\}/);
      parsed = m ? JSON.parse(m[0]) : { category: 'unknown', confidence: 0 };
    }
    const category = allowed.has(parsed.category) ? parsed.category : 'unknown';
    aiCallId = logAiCall({
      service: 'MiniMax', keyId, endpoint: 'chat/completions',
      tokensIn, tokensOut, costEstimate,
      tier3Reason: 'categorize', success: 1, responseMs: Date.now() - start,
    });
    return {
      category,
      confidence: clamp(parseFloat(parsed.confidence) || 0, 0, 1),
      evidence: String(parsed.evidence || '').slice(0, 200),
      summary:  String(parsed.summary  || '').slice(0, 200),
      model: MODEL,
      aiCallId,
    };
  } catch (err) {
    logAiCall({
      service: 'MiniMax', keyId, endpoint: 'chat/completions',
      tokensIn, tokensOut, costEstimate,
      tier3Reason: 'categorize', success: 0, responseMs: Date.now() - start, error: err.message,
    });
    throw err;
  }
}

function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }

function estimateCost(inT, outT, model) {
  // Crude per-1k-token cost estimates (USD). Update as pricing changes.
  const table = {
    'MiniMax-2.0':         { in: 0.0010, out: 0.0020 },
    'MiniMax-2.0-vision': { in: 0.0050, out: 0.0050 },
    'MiniMax-M3':          { in: 0.0030, out: 0.0150 },
    'MiniMax-M2.7':        { in: 0.0015, out: 0.0080 },
    'MiniMax-M2.7-highspeed': { in: 0.0010, out: 0.0050 },
  };
  const p = table[model] || table['MiniMax-M2.7'] || table['MiniMax-2.0'];
  return ((inT / 1000) * p.in) + ((outT / 1000) * p.out);
}

function logAiCall({ service, keyId, endpoint, tokensIn, tokensOut, costEstimate,
                     tier3Reason, success, responseMs, error = null }) {
  const stmt = db.prepare(`
    INSERT INTO ai_calls
      (service, key_id, endpoint, tokens_in, tokens_out, cost_estimate,
       tier3_reason, success, response_ms, error, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  const info = stmt.run(
    service, keyId, endpoint,
    tokensIn, tokensOut, costEstimate,
    tier3Reason, success ? 1 : 0, responseMs, error, Date.now()
  );
  return info.lastInsertRowid;
}

module.exports = { classifyNcii, categorize };
