/**
 * models/finding.js
 */

'use strict';

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

const insert = db.prepare(`
  INSERT INTO findings
    (case_id, user_id, url, domain, page_title, page_snippet, image_hashes,
     tier, tier3_reason, ai_call_id, confidence, is_ncii, status, created_at)
  VALUES
    (@case_id, @user_id, @url, @domain, @page_title, @page_snippet, @image_hashes,
     @tier, @tier3_reason, @ai_call_id, @confidence, @is_ncii, @status, @created_at)
`);

const findById = db.prepare('SELECT * FROM findings WHERE id = ?');
const findByCaseAndUrl = db.prepare('SELECT * FROM findings WHERE case_id = ? AND url = ? ORDER BY id ASC LIMIT 1');
const listByCase = db.prepare('SELECT * FROM findings WHERE case_id = ? ORDER BY created_at DESC');
const listPendingForCase = db.prepare(`
  SELECT * FROM findings WHERE case_id = ? AND status IN ('confirmed','takedown_queued')
`);
const searchByHash = db.prepare(`
  SELECT case_id, image_hashes FROM findings
   WHERE image_hashes IS NOT NULL
`);

const updateStatus = db.prepare(`
  UPDATE findings SET status = ?, updated_at = ? WHERE id = ?
`);

function domainOf(url) {
  try { return new URL(url).hostname.replace(/^www\./, ''); }
  catch { return ''; }
}

function create({ case_id, user_id, url, page_title = null, page_snippet = null,
                  image_hashes = null, tier, tier3_reason = null, ai_call_id = null,
                  confidence = null, is_ncii = false, status = 'pending' }) {
  // DEDUP: a single (case_id, url) pair should produce at most one
  // finding. Multiple scrape sources (Google + DuckDuckGo + a deep
  // rescanner) routinely rediscover the same URL — without this
  // check, the dashboard shows the same leak 3-5 times and we'd
  // queue 3-5 duplicate takedowns.
  //
  // Return the existing row with a `__deduped: true` flag so the
  // caller can decide whether to log a "merged" audit event.
  // If the caller explicitly wants to force a new row (e.g. the
  // operator edited the URL), they pass {force: true}.
  // (No force flag right now — the system always dedups.)
  const existing = findByCaseAndUrl.get(case_id, url);
  if (existing) return { ...existing, __deduped: true };
  const info = insert.run({
    case_id,
    user_id,
    url,
    domain: domainOf(url),
    page_title,
    page_snippet,
    image_hashes: image_hashes ? JSON.stringify(image_hashes) : null,
    tier,
    tier3_reason,
    ai_call_id,
    confidence,
    is_ncii: is_ncii ? 1 : 0,
    status,
    created_at: Date.now(),
  });
  return findById.get(info.lastInsertRowid);
}

function getById(id) { return findById.get(id); }
function getByCaseAndUrl(caseId, url) { return findByCaseAndUrl.get(caseId, url) || null; }
function listForCase(caseId) { return listByCase.all(caseId); }
function setStatus(id, status) { updateStatus.run(status, Date.now(), id); }

/**
 * Hamming distance over two equal-length hex strings.
 * Returns Infinity for unequal lengths.
 */
function hamming(a, b) {
  if (!a || !b || a.length !== b.length) return Infinity;
  let dist = 0;
  for (let i = 0; i < a.length; i += 2) {
    const x = parseInt(a.slice(i, i + 2), 16) ^ parseInt(b.slice(i, i + 2), 16);
    while (x) { dist += x & 1; x >>= 1; }
  }
  return dist;
}

/**
 * Compare an image hash against all known hashes for a user.
 * Threshold: distance <= 10
 */
function findSimilarHash(userId, hash, threshold = 10) {
  const rows = searchByHash.all();
  const matches = [];
  for (const row of rows) {
    if (!row.image_hashes) continue;
    let hashes;
    try { hashes = JSON.parse(row.image_hashes); } catch { continue; }
    if (!Array.isArray(hashes)) continue;
    for (const h of hashes) {
      const d = hamming(h, hash);
      if (d <= threshold) {
        matches.push({ case_id: row.case_id, hash: h, distance: d });
      }
    }
  }
  return matches;
}

module.exports = { create, getById, getByCaseAndUrl, listForCase, setStatus, hamming, findSimilarHash, domainOf };
