// engine/pull-requests-store.js — SQL-backed implementation of the
// per-file pull-requests array. Same external contract as the legacy
// JSON-file reader, modeled directly on work-items-store.js.
//
//   readPullRequestsForScope(scope)        -> [pr, pr, ...]
//   applyPullRequestsMutation(scope, fn)   -> diff-then-apply,
//                                             returns { wrote, result }
//
// `scope` is 'central' for the top-level pull-requests.json or a project
// name for projects/<name>/pull-requests.json.
//
// External-edit detection mirrors work-items-store: per-scope last-mirror
// mtime is recorded after every write; if the JSON file's mtime advances
// outside our knowledge (test re-seed, operator hand-edit), we treat the
// JSON as canonical and rebuild SQL before the next read or mutation.

const path = require('path');
const fs = require('fs');

function _toMs(v) {
  if (v == null) return null;
  if (typeof v === 'number') return Number.isFinite(v) ? v : null;
  const parsed = Date.parse(v);
  return Number.isFinite(parsed) ? parsed : null;
}

function scopeForFilePath(filePath) {
  const norm = String(filePath).replace(/\\/g, '/');
  const m = norm.match(/projects\/([^/]+)\/pull-requests\.json$/);
  if (m) return m[1];
  return 'central';
}

function _filePathForScope(scope) {
  const shared = require('./shared');
  if (scope === 'central') {
    return path.join(shared.MINIONS_DIR, 'pull-requests.json');
  }
  return path.join(shared.MINIONS_DIR, 'projects', scope, 'pull-requests.json');
}

function _parseRow(row) {
  if (!row || !row.data) return null;
  try { return JSON.parse(row.data); }
  catch { return null; }
}

function _readJsonArrayFallback(scope) {
  const fp = _filePathForScope(scope);
  let raw;
  try { raw = fs.readFileSync(fp, 'utf8'); }
  catch { return []; }
  try {
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    try {
      // eslint-disable-next-line no-console
      console.warn(`[pull-requests-store] corrupt JSON in ${fp}: ${e.message}`);
    } catch { /* console may be wrapped in tests */ }
    return [];
  }
}

// Content-hash fingerprint per scope: same-length swaps (timestamps,
// reviewStatus enums) collide on (mtime, size) but never on SHA-1.
const _lastMirrorHashByScope = new Map();

const crypto = require('crypto');

function _fileContentHash(filePath) {
  try {
    const buf = fs.readFileSync(filePath);
    return crypto.createHash('sha1').update(buf).digest('hex');
  }
  catch { return null; }
}

function _hydrateScopeFromJson(db, scope) {
  const jsonItems = _readJsonArrayFallback(scope);
  db.prepare('DELETE FROM pull_requests WHERE scope = ?').run(scope);
  if (jsonItems.length === 0) return;
  const now = Date.now();
  const ins = db.prepare(`
    INSERT INTO pull_requests (id, scope, status, review_status, agent, branch, pr_number, created_at, last_polled_at, data, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(scope, id) DO NOTHING
  `);
  for (const pr of jsonItems) {
    if (!pr || !pr.id) continue;
    ins.run(
      String(pr.id),
      scope,
      String(pr.status || 'active'),
      pr.reviewStatus || null,
      pr.agent || null,
      pr.branch || null,
      Number.isFinite(pr.prNumber) ? pr.prNumber : null,
      _toMs(pr.created),
      _toMs(pr.lastPolledAt || pr.lastPolled),
      JSON.stringify(pr),
      now,
    );
  }
}

function _resyncScopeIfJsonDiverged(db, scope) {
  const jsonPath = _filePathForScope(scope);
  const currentHash = _fileContentHash(jsonPath);
  const lastHash = _lastMirrorHashByScope.get(scope);
  if (currentHash == null) return;
  if (lastHash != null && currentHash === lastHash) return;
  if (lastHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM pull_requests WHERE scope = ? LIMIT 1').get(scope);
    if (sqlHas) {
      _lastMirrorHashByScope.set(scope, currentHash);
      return;
    }
  }
  _hydrateScopeFromJson(db, scope);
  _lastMirrorHashByScope.set(scope, currentHash);
}

function readPullRequestsForScope(scope) {
  const { getDb } = require('./db');
  const db = getDb();

  _resyncScopeIfJsonDiverged(db, scope);

  const rows = db.prepare(`
    SELECT data FROM pull_requests
    WHERE scope = ?
    ORDER BY rowid
  `).all(scope);

  if (rows.length === 0) {
    const fallback = _readJsonArrayFallback(scope);
    if (fallback.length > 0) return fallback;
    return [];
  }

  const out = [];
  for (const row of rows) {
    const pr = _parseRow(row);
    if (pr) out.push(pr);
  }
  return out;
}

// Enumerate scopes that have a JSON file on disk — used as the
// SQL-unavailable fallback for `readAllPullRequests`. Mirrors
// work-items-store's `_enumerateJsonScopes` shape.
function _enumerateJsonScopes() {
  const shared = require('./shared');
  const scopes = [];
  const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
  if (fs.existsSync(centralPath)) scopes.push('central');
  const projectsDir = path.join(shared.MINIONS_DIR, 'projects');
  try {
    for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
      if (!d.isDirectory()) continue;
      const fp = path.join(projectsDir, d.name, 'pull-requests.json');
      if (fs.existsSync(fp)) scopes.push(d.name);
    }
  } catch { /* projects dir missing on fresh install */ }
  return scopes;
}

function readAllPullRequests() {
  const { getDb } = require('./db');
  const db = getDb();

  try { _resyncScopeIfJsonDiverged(db, 'central'); } catch {}
  const knownScopes = db.prepare('SELECT DISTINCT scope FROM pull_requests').all().map(r => r.scope);
  for (const s of knownScopes) {
    if (s === 'central') continue;
    try { _resyncScopeIfJsonDiverged(db, s); } catch {}
  }

  const rows = db.prepare('SELECT data, scope FROM pull_requests ORDER BY rowid').all();
  const out = [];
  for (const row of rows) {
    const pr = _parseRow(row);
    if (!pr) continue;
    pr._scope = row.scope;
    out.push(pr);
  }
  return out;
}

function _indexById(arr) {
  const out = new Map();
  for (const pr of arr) {
    if (!pr || !pr.id) continue;
    out.set(String(pr.id), pr);
  }
  return out;
}

function _computePullRequestsDiff(scope, before, after) {
  const beforeMap = _indexById(before);
  const afterMap = _indexById(after);
  const toUpsert = [];
  const toDelete = [];
  for (const [id, pr] of afterMap) {
    const prev = beforeMap.get(id);
    if (!prev) { toUpsert.push(pr); continue; }
    if (JSON.stringify(prev) !== JSON.stringify(pr)) toUpsert.push(pr);
  }
  for (const [id] of beforeMap) {
    if (!afterMap.has(id)) toDelete.push(id);
  }
  return { scope, toUpsert, toDelete };
}

function _applyPullRequestsDiff(db, diff) {
  if (diff.toUpsert.length === 0 && diff.toDelete.length === 0) return false;
  const now = Date.now();
  const upsertStmt = db.prepare(`
    INSERT INTO pull_requests (id, scope, status, review_status, agent, branch, pr_number, created_at, last_polled_at, data, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(scope, id) DO UPDATE SET
      status         = excluded.status,
      review_status  = excluded.review_status,
      agent          = excluded.agent,
      branch         = excluded.branch,
      pr_number      = excluded.pr_number,
      created_at     = excluded.created_at,
      last_polled_at = excluded.last_polled_at,
      data           = excluded.data,
      updated_at     = excluded.updated_at
  `);
  const deleteStmt = db.prepare('DELETE FROM pull_requests WHERE scope = ? AND id = ?');

  for (const pr of diff.toUpsert) {
    upsertStmt.run(
      String(pr.id),
      diff.scope,
      String(pr.status || 'active'),
      pr.reviewStatus || null,
      pr.agent || null,
      pr.branch || null,
      Number.isFinite(pr.prNumber) ? pr.prNumber : null,
      _toMs(pr.created),
      _toMs(pr.lastPolledAt || pr.lastPolled),
      JSON.stringify(pr),
      now,
    );
  }
  for (const id of diff.toDelete) {
    deleteStmt.run(diff.scope, id);
  }
  return true;
}

function applyPullRequestsMutation(scope, mutator) {
  const { getDb, withTransaction } = require('./db');
  const db = getDb();

  return withTransaction(db, () => {
    _resyncScopeIfJsonDiverged(db, scope);
    const before = readPullRequestsForScope(scope);
    // Strip the _scope decoration that readAllPullRequests adds, in case
    // a caller round-trips records from there back through here.
    for (const pr of before) { if (pr && pr._scope) delete pr._scope; }
    const beforeSnapshot = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null)
      ? before
      : (Array.isArray(next) ? next : before);
    const diff = _computePullRequestsDiff(scope, beforeSnapshot, after);
    const wrote = _applyPullRequestsDiff(db, diff);
    return { wrote, result: after };
  });
}

function _mirrorJsonFromSql(scope, filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    // Read SQL directly for this scope — bypass JSON fallback so a
    // mutation that empties SQL for the scope doesn't resurrect stale
    // JSON content.
    const db = getDb();
    const rows = db.prepare('SELECT data FROM pull_requests WHERE scope = ? ORDER BY rowid').all(scope);
    const items = [];
    for (const row of rows) {
      const pr = _parseRow(row);
      if (!pr) continue;
      if (pr._scope) delete pr._scope;
      items.push(pr);
    }
    const target = filePath || _filePathForScope(scope);
    shared.safeWrite(target, items);
    const h = _fileContentHash(target);
    if (h != null) _lastMirrorHashByScope.set(scope, h);
  } catch {
    // Mirror failures are non-fatal — SQL has already committed.
  }
}

function _resetPullRequestsTableForTest() {
  const { getDb } = require('./db');
  try { getDb().exec('DELETE FROM pull_requests'); } catch { /* not initialized */ }
}

function dropScope(scope) {
  try {
    const { getDb } = require('./db');
    getDb().prepare('DELETE FROM pull_requests WHERE scope = ?').run(scope);
    _lastMirrorHashByScope.delete(scope);
  } catch { /* db unavailable */ }
}

module.exports = {
  scopeForFilePath,
  readPullRequestsForScope,
  readAllPullRequests,
  applyPullRequestsMutation,
  dropScope,
  _filePathForScope,
  _mirrorJsonFromSql,
  _resetPullRequestsTableForTest,
};
