// server-mock.js — in-memory mock of the Mysterion crypto service verify API.
//
// Models exactly what /crypto/verify/hash, /crypto/verify, and
// /crypto/verify/batch return for a given fixture database. Does not implement
// ML-DSA, TSA, RFC3161 — only the STRING WIRE CONTRACT, which is what the
// extension bug hinges on.

'use strict';

// A "signing record" in the fake DB — what routers/sign.py:finalize writes
// into the signing_records table.
//
//   { steg_id: 32-hex, creator_id: dashed-uuid,
//     post_sign_hash: 64-hex BLAKE3,
//     pHash:          16-hex dhash,
//     altered: bool,                // flag set when an alteration is reported
//   }

class ServerMock {
  constructor(records = []) {
    this.records = records;
    this.thresholdT = 10;  // Hamming distance threshold for pHash similarity
  }

  /** POST /crypto/verify/hash { content_hash } → { found, steg_id } */
  fastPath(contentHashHex) {
    if (!contentHashHex) return null;
    const hit = this.records.find(r => r.post_sign_hash === contentHashHex);
    if (hit) return { found: true, steg_id: hit.steg_id };
    return { found: false };
  }

  /** POST /crypto/verify { steg_id, phash, content_hash, extracted_creator_id } */
  verify({ steg_id, phash, content_hash, extracted_creator_id }) {
    if (!steg_id) return null;
    // STRICT match on canonical 32-hex — this IS the wire contract.
    // If the extension sends anything other than 32-hex lowercase, the
    // backend row lookup misses and the extension sees `{verified:false}`
    // with no steg_id — which the extension maps to UNKNOWN (terminal 8).
    const expected = /^[0-9a-f]{32}$/;
    if (!expected.test(steg_id)) {
      // Backend would reject as 422 → extension falls to pHash
      return null;
    }
    const row = this.records.find(r => r.steg_id === steg_id);
    if (!row) {
      // No row for that steg_id → backend returns verified:false, no steg_id
      return { verdict: 'unknown' };
    }

    // Creator mismatch — extension sees different creator in LSB vs row
    if (extracted_creator_id &&
        extracted_creator_id.toLowerCase() !== row.creator_id.toLowerCase()) {
      // Authentic row but creator mismatch → resigned or CREATOR_MISMATCH
      // If hash+phash still match, this is RESIGNED (terminal 3), else CM(4).
      if (content_hash === row.post_sign_hash) {
        return { verdict: 'resigned' };
      }
      return { verdict: 'creator_mismatch' };
    }

    // Alteration flag set by upstream
    if (row.altered) {
      return {
        verdict: 'altered',
        creator_mismatch: extracted_creator_id &&
          extracted_creator_id.toLowerCase() !== row.creator_id.toLowerCase(),
      };
    }

    // Exact byte-match → AUTHENTIC
    if (content_hash === row.post_sign_hash) {
      return { verdict: 'authentic' };
    }

    // pHash close but hash different → TRANSFORMED
    if (phash && row.pHash) {
      const d = hammingDistance(phash, row.pHash);
      if (d >= 0 && d <= this.thresholdT) {
        return { verdict: 'transformed' };
      }
    }

    // Hash mismatch, pHash far → probably bit-level alteration
    return { verdict: 'altered' };
  }

  /** pHash-only lookup across all records when extraction failed. */
  pHashLookup(phash) {
    if (!phash) return null;
    let best = null;
    for (const r of this.records) {
      const d = hammingDistance(phash, r.pHash);
      if (d < 0) continue;
      if (d <= this.thresholdT) {
        if (!best || d < best.d) {
          best = { d, kind: r.altered ? 'altered' : 'authentic', record: r };
        }
      }
    }
    return best;
  }
}

function hammingDistance(aHex, bHex) {
  if (!aHex || !bHex || aHex.length !== bHex.length) return -1;
  let d = 0;
  for (let i = 0; i < aHex.length; i += 2) {
    const a = parseInt(aHex.substr(i, 2), 16);
    const b = parseInt(bHex.substr(i, 2), 16);
    let x = a ^ b;
    while (x) { d += x & 1; x >>>= 1; }
  }
  return d;
}

module.exports = { ServerMock, hammingDistance };
