/**
 * routes/scan.js
 * Run a Google search (or image-hash) scan against a case.
 * Uses planGate('scan') to enforce monthly limit.
 */

'use strict';

const express  = require('express');
const multer   = require('multer');
const validator = require('validator');
const sharp    = require('sharp');
const { requireLogin } = require('../middleware/auth');
const { sendUserError } = require('../middleware/userErrors');
const alertBus = require('../services/alertBus');
const planGate = require('../middleware/planGate');
const Case    = require('../models/case');
const Finding = require('../models/finding');
const Scan    = (() => {
  // we don't have a Scan model file; add it inline (or move to models/scan.js later)
  const { db } = require('../config/db');
  const insert = db.prepare(`
    INSERT INTO scans (case_id, user_id, query, engine, result_count, tier1_hits, tier2_hits, tier3_hits, duration_ms, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  return { insert: (data) => insert.run(
    data.case_id, data.user_id, data.query, data.engine,
    data.result_count, data.tier1_hits, data.tier2_hits, data.tier3_hits,
    data.duration_ms, Date.now()
  )};
})();
const Audit  = require('../models/auditLog');
const scrape = require('../services/scrape');
const detect = require('../services/detect');
const hash   = require('../services/hash');

const router = express.Router();
const sec   = require('../config/security');
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: sec.UPLOAD_MAX_BYTES },
  fileFilter: (req, file, cb) => {
    if (sec.UPLOAD_ALLOWED_MIME.includes(file.mimetype)) cb(null, true);
    else cb(new Error('UNSUPPORTED_IMAGE_TYPE'));
  },
});

/**
 * GET /scan/run?case_id=X&q=...
 * Search Google for the victim's name + optional extra keywords, classify each
 * result via the 3-tier detection pipeline.
 */
router.get('/run', requireLogin, planGate('scan'), async (req, res) => {
  try {
    const caseId = parseInt(req.query.case_id, 10);
    const extra  = (req.query.q || '').trim();
    const c = Case.getById(caseId);
    if (!c) return sendUserError(res, 'NOT_FOUND', { publicMessage: "We couldn't find that case.", adminDetail: { case_id: caseId } });
    if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
      return sendUserError(res, 'FORBIDDEN');
    }
    if (!c.victim_name) return sendUserError(res, 'INVALID_INPUT', { publicMessage: "This case doesn't have a victim name yet." });

    const query = `"${c.victim_name}" ${extra}`.trim();
    const start = Date.now();
    let result = [];
    try {
      result = await scrape.googleSearch(query, { num: 20 });
    } catch (e) {
      Audit.log(req.user, 'scan.error', `case:${c.id}`, { error: e.message });
      alertBus.raise({
        severity: 'warn',
        category: 'api_error',
        service: 'scrape_do',
        title: 'Scrape.do failed during scan',
        detail: { case_id: c.id, query, error: e.message },
        ref_url: `case:${c.id}`,
        dedupe_key: `scan_scrape_fail:${c.id}`,
      });
      return sendUserError(res, 'DISPATCH_FAILED', {
        publicMessage: "We couldn't run that scan right now. Our team has been notified.",
        adminDetail: { case_id: c.id, query, error: e.message, hint: 'Check Scrape.do key / quota in /admin → API keys' },
        alertService: 'scrape_do',
        alertCategory: 'api_error',
      });
    }

    let tier1 = 0, tier2 = 0, tier3 = 0, confirmed = 0;
    const findings = [];
    for (const r of result) {
      if (!validator.isURL(r.url, { require_protocol: true })) continue;
      const out = await detect.detectAndPersist({
        case_id: c.id, user_id: req.user.id,
        url: r.url, page_title: r.title, page_snippet: r.snippet,
      });
      findings.push(out.finding);
      if (out.result.tier === 1) tier1++;
      if (out.result.tier === 2) tier2++;
      if (out.result.tier === 3) tier3++;
      if (out.finding.is_ncii) confirmed++;
    }

    Scan.insert({
      case_id: c.id, user_id: req.user.id, query,
      engine: 'google',
      result_count: result.length, tier1_hits: tier1, tier2_hits: tier2, tier3_hits: tier3,
      duration_ms: Date.now() - start,
    });
    Audit.log(req.user, 'scan.run', `case:${c.id}`, { count: result.length, confirmed });
    res.json({ ok: true, scanned: result.length, confirmed, findings, tier_breakdown: { tier1, tier2, tier3 } });
  } catch (err) {
    console.error('[scan] route error:', err);
    return sendUserError(res, 'INTERNAL', {
      adminDetail: { message: err.message, path: req.path },
      alertTitle: `Unhandled /scan error`,
    });
  }
});

/**
 * POST /scan/image  (multipart: image, case_id)
 * Hash the image, find similar hashes in the case, optionally ask AI.
 */
router.post('/image', requireLogin, planGate('scan'), upload.single('image'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'NO_IMAGE' });
  if (!req.file.buffer || req.file.buffer.length === 0) {
    return res.status(400).json({ error: 'EMPTY_FILE' });
  }
  const caseId = parseInt(req.body.case_id, 10);
  const c = Case.getById(caseId);
  if (!c) return res.status(404).json({ error: 'CASE_NOT_FOUND' });
  if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }

  // ---- HARDENING: verify magic bytes, strip EXIF, cap dimensions ----
  let metadata;
  try {
    metadata = await sharp(req.file.buffer, { failOn: 'truncated' }).metadata();
  } catch (e) {
    return res.status(400).json({ error: 'INVALID_IMAGE', message: 'File is not a valid image (magic-byte check failed).' });
  }
  // Reject unsupported formats even if MIME lied
  const format = (metadata.format || '').toLowerCase();
  if (!['jpeg', 'png', 'webp'].includes(format)) {
    return res.status(400).json({ error: 'UNSUPPORTED_FORMAT', format });
  }
  // Reject overly large pixel dimensions (pHash unreliable / DOS)
  const pixels = (metadata.width || 0) * (metadata.height || 0);
  if (pixels > sec.UPLOAD_MAX_PIXELS) {
    return res.status(413).json({ error: 'IMAGE_TOO_LARGE', width: metadata.width, height: metadata.height, max_pixels: sec.UPLOAD_MAX_PIXELS });
  }
  // Re-process the image to strip all EXIF / metadata before any further use
  let safeBuffer;
  try {
    safeBuffer = await sharp(req.file.buffer)
      .rotate()                              // apply EXIF orientation then strip
      .withMetadata({})                      // remove all metadata
      .toFormat(format, { quality: 92 })
      .toBuffer();
  } catch (e) {
    return res.status(400).json({ error: 'IMAGE_PROCESSING_FAILED', message: e.message });
  }
  // Wipe the original (which may have EXIF GPS coordinates etc.)
  req.file.buffer = null;

  // ---- Hash the sanitised buffer ----
  let pHash;
  try { pHash = await hash.phash(safeBuffer); }
  catch (e) { return res.status(400).json({ error: 'HASH_FAILED', message: e.message }); }

  const matches = Finding.findSimilarHash(req.user.id, pHash, 10);
  let finding = null;
  if (matches.length > 0) {
    const c2 = Case.getById(matches[0].case_id);
    finding = Finding.create({
      case_id: c2.id, user_id: req.user.id,
      url: c2.reference_url || `case:${c2.id}`,
      page_title: 'Image hash match', page_snippet: `Matched known case #${c2.id} (distance ${matches[0].distance})`,
      image_hashes: [pHash],
      tier: 1, confidence: 0.95, is_ncii: true, status: 'confirmed',
    });
  } else {
    const out = await detect.detectAndPersist({
      case_id: c.id, user_id: req.user.id,
      url: c.reference_url || 'image-upload',
      page_title: 'Image upload', page_snippet: c.context || '',
      image_hashes: [pHash],
    });
    finding = out.finding;
  }
  Audit.log(req.user, 'scan.image', `case:${c.id}`, { hash: pHash, matches: matches.length, pixels, format });
  res.json({ ok: true, finding, hash_matches: matches.length, image_meta: { width: metadata.width, height: metadata.height, format } });
});

/**
 * POST /scan/video  (JSON: { case_id, frames: [base64, base64, ...] })
 * Hash each frame, look for matches, run AI on first ambiguous frame.
 */
router.post('/video', requireLogin, planGate('scan'), async (req, res) => {
  const ref = require('crypto').randomBytes(4).toString('hex');
  const { case_id, frames } = req.body || {};
  if (!Array.isArray(frames) || frames.length === 0) {
    return res.status(400).json({ error: 'NO_FRAMES', ref });
  }
  if (frames.length > 30) return res.status(400).json({ error: 'TOO_MANY_FRAMES', ref });
  const c = Case.getById(parseInt(case_id, 10));
  if (!c) return res.status(404).json({ error: 'CASE_NOT_FOUND', ref });
  if (req.user.role !== 'superadmin' && c.user_id !== req.user.id && c.org_id !== req.user.org_id) {
    return res.status(403).json({ error: 'FORBIDDEN', ref });
  }

  try {
    const hashes = await hash.phashFrames(frames);
    let finding = null;
    for (const h of hashes) {
      const matches = Finding.findSimilarHash(req.user.id, h, 10);
      if (matches.length > 0) {
        const c2 = Case.getById(matches[0].case_id);
        finding = Finding.create({
          case_id: c2.id, user_id: req.user.id,
          url: c2.reference_url || `case:${c2.id}`,
          page_title: 'Video frame hash match',
          page_snippet: `Matched case #${c2.id} (distance ${matches[0].distance})`,
          image_hashes: [h], tier: 1, confidence: 0.95, is_ncii: true, status: 'confirmed',
        });
        break;
      }
    }
    if (!finding) {
      // No hash match. Take the middle frame and ask AI.
      const middle = frames[Math.floor(frames.length / 2)];
      const out = await detect.detectAndPersist({
        case_id: c.id, user_id: req.user.id,
        url: c.reference_url || 'video-upload',
        page_title: 'Video upload', page_snippet: c.context || '',
        image_hashes: hashes, imageB64: middle,
      });
      finding = out.finding;
    }
    Audit.log(req.user, 'scan.video', `case:${c.id}`, { frames: frames.length, hashes: hashes.length, ref });
    res.json({ ok: true, finding, frames_hashed: hashes.length, ref });
  } catch (e) {
    const alertBus = require('../services/alertBus');
    alertBus.raise({
      severity: 'error', category: 'other', service: 'scan_video',
      title: `Video scan failed (case #${c.id})`,
      detail: { ref, case_id: c.id, user_id: req.user.id, frames: frames.length, error: e.message, stack: e.stack && e.stack.slice(0, 800) },
      ref_url: `case:${c.id}`,
      dedupe_key: `scan_video_fail:${ref}`,
    });
    console.error(`[scan/video] ref=${ref} case=${c.id} user=${req.user.id} error=${e.message}`);
    res.status(500).json({ error: 'INTERNAL', ref, message: 'Video scan failed. Reference this short code to support.' });
  }
});

module.exports = router;
