/**
 * services/platformContactsRepo.js
 *
 * The "learned contact map" — backed by a SQLite table. The system
 * itself populates this as takedowns are sent / responded / failed.
 * Operators can also override via /admin/contacts.
 *
 * Lookup is by domain. The highest-confidence row wins. Rows have
 * provenance: curated (hand-written bootstrap), security_txt,
 * sitemap, whois, admin_override, observed_success, attempted, failed.
 *
 * Bootstrap: on first lookup of a domain that has no row, this module
 * falls back to the curated KNOWN list (services/platformContacts.js
 * `KNOWN`) and inserts it as `source='curated', confidence=0.7`. The
 * curated entries are NOT removed — they're the seed.
 *
 * The hard-coded KNOWN map stays as the **bootstrap-only** fallback.
 * Once a row exists in the DB (even a curated one), the DB is the
 * source of truth. Admins can edit, disable, or replace any row.
 */

'use strict';

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

// IMPORTANT: prepared statements are bound to the db handle that
// was active at module load. In production this is the singleton
// in config/db.js, which is the only one. In tests, the test setup
// creates a fresh DB and the test file requires this module AFTER
// the test DB is in place — so the handles match. We expose a tiny
// `db` accessor that returns whatever handle the singleton currently
// has. If you swap the DB at runtime, call `repo._refresh()` to
// re-bind the prepared statements.
let db = dbModule;
let _upsert, _update, _disable, _findByDomain, _findById, _listAll, _findByDomainContact, _bumpContact, _insertRow, _disableRow, _selectRow;

function bind() {
  _upsert = db.prepare(`
    INSERT INTO platform_contacts
      (domain, contact, form_url, security_url, name, source, confidence, verified_at, notes, created_at, updated_at)
    VALUES
      (@domain, @contact, @form_url, @security_url, @name, @source, @confidence, @verified_at, @notes, @now, @now)
    ON CONFLICT(domain, contact, form_url, security_url) DO UPDATE SET
      confidence = MAX(confidence, excluded.confidence),
      verified_at = CASE WHEN excluded.verified_at IS NULL THEN verified_at ELSE excluded.verified_at END,
      updated_at = excluded.updated_at,
      name = COALESCE(excluded.name, name),
      source = CASE
        WHEN excluded.confidence > confidence THEN excluded.source
        ELSE source
      END
  `);
  _update = db.prepare(`
    UPDATE platform_contacts
       SET contact = @contact,
           form_url = @form_url,
           security_url = @security_url,
           name = @name,
           confidence = @confidence,
           verified_at = @verified_at,
           notes = COALESCE(@notes, notes),
           updated_at = @now
     WHERE id = @id
  `);
  _disable = db.prepare(`UPDATE platform_contacts SET confidence = 0, updated_at = ? WHERE id = ?`);
  _findByDomain = db.prepare(`
    SELECT * FROM platform_contacts
     WHERE domain = ? AND confidence >= ?
     ORDER BY confidence DESC, COALESCE(verified_at, 0) DESC
     LIMIT 10
  `);
  _findById = db.prepare(`SELECT * FROM platform_contacts WHERE id = ?`);
  _listAll = db.prepare(`
    SELECT * FROM platform_contacts ORDER BY domain, confidence DESC
  `);
  _findByDomainContact = db.prepare(`SELECT id, source, confidence, notes FROM platform_contacts WHERE domain = ? AND contact = ?`);
  _bumpContact = db.prepare(`UPDATE platform_contacts SET source = 'observed_success', confidence = 1.0, verified_at = ?, updated_at = ? WHERE id = ?`);
  _disableRow = db.prepare(`UPDATE platform_contacts SET confidence = 0, updated_at = ? WHERE id = ?`);
}
bind();

// Minimum confidence to trust a contact. Lower-confidence rows are
// stored for analysis but never used to dispatch a real takedown.
// A guessed-from-RDAP address starts at 0.5; an admin override or
// observed_success is 1.0. So 0.5 is the floor for "we have at
// least one real signal that this address is correct."
const MIN_CONFIDENCE_TO_USE = 0.5;

// Lazy require to avoid circular dep at module load.
let _KNOWN = null;
function getKnownMap() {
  if (_KNOWN) return _KNOWN;
  _KNOWN = require('./platformContacts').KNOWN || {};
  return _KNOWN;
}

function _refresh() { bind(); }   // for tests that swap DB handles

/**
 * Look up the best contact for a domain. Returns the highest-confidence
 * row, or null if nothing is known.
 *
 * If the DB has no row for this domain, seeds the curated KNOWN map
 * (if any) so the system self-populates on first use.
 */
function getBest(domain) {
  if (!domain) return null;
  const d = String(domain).toLowerCase().replace(/^www\./, '');

  const rows = _findByDomain.all(d, MIN_CONFIDENCE_TO_USE);
  if (rows.length) return rows[0];

  // Seed from the curated map on first lookup of an unknown domain.
  // NOTE: in v1.3.x the curated map is reduced to the test fixture only;
  // this branch exists for forward-compat (admins can add a curated
  // entry in /admin/contacts and it will land here).
  const seed = getKnownMap()[d];
  if (seed) {
    _upsert.run({
      domain: d,
      contact: seed.contact || null,
      form_url: seed.form_url || null,
      security_url: seed.security || null,
      name: seed.name || null,
      source: 'curated',
      confidence: 0.7,
      verified_at: seed.verified_at || Date.now(),
      notes: seed.notes || null,
      now: Date.now(),
    });
    return _findByDomain.all(d, MIN_CONFIDENCE_TO_USE)[0] || null;
  }
  return null;
}

/**
 * Record an address we discovered (security.txt, footer, WHOIS, etc.).
 * The system calls this whenever contactDiscovery or RDAP produces
 * a candidate address — no human in the loop.
 */
function recordDiscovery({ domain, contact, form_url = null, security_url = null, name = null, source, confidence = 0.4, notes = null }) {
  if (!domain || !source) return null;
  if (!contact && !form_url && !security_url) return null;   // need at least one channel
  const d = String(domain).toLowerCase().replace(/^www\./, '');
  const now = Date.now();
  _upsert.run({
    domain: d, contact: contact || null, form_url, security_url,
    name, source, confidence, verified_at: null, notes, now,
  });
  return _findByDomain.all(d, MIN_CONFIDENCE_TO_USE)[0] || null;
}

/**
 * An admin manually set this contact. Marks confidence=1.0 + verified.
 * Used by /admin/contacts POST + when an operator edits a takedown
 * row's contact_email before sending.
 */
function recordAdminOverride({ domain, contact, form_url = null, security_url = null, name = null, notes = null }) {
  if (!domain || !contact) return null;
  const d = String(domain).toLowerCase().replace(/^www\./, '');
  const now = Date.now();
  _upsert.run({
    domain: d, contact, form_url, security_url, name,
    source: 'admin_override', confidence: 1.0,
    verified_at: now, notes: notes || 'Set manually by an operator',
    now,
  });
  return _findByDomain.all(d, MIN_CONFIDENCE_TO_USE)[0] || null;
}

/**
 * The system sent a takedown to this address and the content was
 * later removed / responded. Upgrade the row to source=observed_success
 * confidence=1.0.
 */
function recordObservedSuccess({ domain, contact }) {
  if (!domain || !contact) return null;
  const d = String(domain).toLowerCase().replace(/^www\./, '');
  const now = Date.now();
  const existing = _findByDomainContact.get(d, contact);
  if (existing) {
    _bumpContact.run(now, now, existing.id);
  } else {
    _upsert.run({
      domain: d, contact, form_url: null, security_url: null,
      name: null, source: 'observed_success', confidence: 1.0,
      verified_at: now, notes: 'Promoted from a successful takedown outcome.',
      now,
    });
  }
  return _findByDomain.all(d, MIN_CONFIDENCE_TO_USE)[0] || null;
}

/**
 * The system sent to this address and it failed (bounce, 4xx).
 * Downgrade confidence so future lookups prefer alternatives.
 */
function recordFailure({ domain, contact, reason }) {
  if (!domain || !contact) return null;
  const d = String(domain).toLowerCase().replace(/^www\./, '');
  const now = Date.now();
  const existing = _findByDomainContact.get(d, contact);
  if (existing) {
    // Don't drop below 0.1 — a hard-bounce might be transient.
    const newConf = Math.max(0.1, existing.confidence - 0.2);
    const note = existing.notes ? existing.notes + '\n' : '';
    db.prepare(`UPDATE platform_contacts SET confidence = ?, source = CASE WHEN ? = 'observed_success' THEN source ELSE 'failed' END, notes = ? || ?, updated_at = ? WHERE id = ?`)
      .run(newConf, existing.source, note, 'Last failure: ' + (reason || 'unknown'), now, existing.id);
  } else {
    _upsert.run({
      domain: d, contact, form_url: null, security_url: null,
      name: null, source: 'failed', confidence: 0.1,
      verified_at: null, notes: 'Recorded as failed: ' + (reason || 'unknown'),
      now,
    });
  }
  return _findByDomain.all(d, MIN_CONFIDENCE_TO_USE)[0] || null;
}

function list(filter = {}) {
  let rows = _listAll.all();
  if (filter.domain) rows = rows.filter(r => r.domain === String(filter.domain).toLowerCase());
  if (filter.source) rows = rows.filter(r => r.source === filter.source);
  if (filter.minConfidence !== undefined) rows = rows.filter(r => r.confidence >= filter.minConfidence);
  return rows;
}

function getById(id) { return _findById.get(id); }

function adminUpdate(id, fields) {
  const cur = _findById.get(id);
  if (!cur) return null;
  _update.run({
    id,
    contact:    fields.contact    ?? cur.contact,
    form_url:   fields.form_url   ?? cur.form_url,
    security_url: fields.security_url ?? cur.security_url,
    name:       fields.name       ?? cur.name,
    confidence: fields.confidence ?? cur.confidence,
    verified_at: fields.verified_at ?? cur.verified_at,
    notes:      fields.notes      ?? cur.notes,
    now: Date.now(),
  });
  return _findById.get(id);
}

function adminDisable(id) { _disableRow.run(Date.now(), id); return _findById.get(id); }

module.exports = {
  getBest, recordDiscovery, recordAdminOverride,
  recordObservedSuccess, recordFailure,
  list, getById, adminUpdate, adminDisable, _refresh,
  MIN_CONFIDENCE_TO_USE,
};
