import { getDb, newId, nowIso } from './db.js';
import { resumeHeartbeatTasksForApproval } from './heartbeat-queue.js';
import { publishStateChanged } from './live-events.js';

// Synchronous decision handlers registered by other modules (e.g. the process
// engine). Each handler is called with { approval_request_id, decision, row }
// after the DB write but before emit. Handlers must never throw; this module
// catches everything to keep approvals.decide a no-failure path.
const decisionHandlers = new Set();

export function registerApprovalDecisionHandler(handler) {
  if (typeof handler !== 'function') return () => {};
  decisionHandlers.add(handler);
  return () => { decisionHandlers.delete(handler); };
}

function fireDecisionHandlers(payload) {
  for (const handler of decisionHandlers) {
    try { handler(payload); } catch { /* observation only */ }
  }
}

function emitApprovals() {
  try { publishStateChanged('approvals'); } catch { /* observation-only */ }
}

function parseJson(value, fallback = null) {
  try { return JSON.parse(value); } catch { return fallback; }
}

function rowToApproval(row) {
  if (!row) return null;
  return { ...row, payload: parseJson(row.payload, {}) };
}

export function createApprovalRequest({ action_type, risk = 'medium', summary, payload = {}, requested_by = 'system', expires_at = null, project_id = null }) {
  const db = getDb();
  const id = newId('approval');
  const created = nowIso();
  const resolvedProjectId = project_id || payload?.project_id || payload?.projectId || null;
  db.prepare(`
    INSERT INTO approval_requests (id, status, action_type, risk, summary, payload, requested_by, created_at, expires_at, project_id)
    VALUES (?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(id, action_type, risk, summary, JSON.stringify(payload), requested_by, created, expires_at, resolvedProjectId);
  db.prepare(`
    INSERT INTO audit_logs (id, at, type, summary, status, meta, project_id)
    VALUES (?, ?, 'approval.request', ?, 'pending', ?, ?)
  `).run(newId('audit'), created, summary, JSON.stringify({ approval_request_id: id, action_type, risk, project_id: resolvedProjectId }), resolvedProjectId);
  emitApprovals();
  return getApprovalRequest(id);
}

export function getApprovalRequest(id) {
  return rowToApproval(getDb().prepare('SELECT * FROM approval_requests WHERE id = ?').get(id));
}

export function listApprovalRequests({ status, project_id = null, limit = 50 } = {}) {
  const db = getDb();
  const where = [];
  const params = [];
  if (status) { where.push('status = ?'); params.push(status); }
  if (project_id) { where.push('project_id = ?'); params.push(project_id); }
  params.push(limit);
  return db.prepare(`SELECT * FROM approval_requests ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY created_at DESC LIMIT ?`)
    .all(...params).map(rowToApproval);
}

export function decideApprovalRequest(id, decision, { approved_by = 'owner' } = {}) {
  const next = decision === 'approved' ? 'approved' : 'denied';
  const db = getDb();
  const row = getApprovalRequest(id);
  if (!row) throw new Error('Approval request not found');
  if (row.status !== 'pending') throw new Error(`Approval request is ${row.status}`);
  const now = nowIso();
  db.prepare('UPDATE approval_requests SET status = ?, approved_by = ?, decided_at = ? WHERE id = ?')
    .run(next, approved_by, now, id);
  db.prepare(`
    INSERT INTO audit_logs (id, at, type, summary, status, meta)
    VALUES (?, ?, 'approval.decision', ?, ?, ?)
  `).run(newId('audit'), now, `${next} ${row.summary}`, next, JSON.stringify({ approval_request_id: id }));
  if (next === 'approved') resumeHeartbeatTasksForApproval(id);
  // Synchronous fan-out to any registered handlers (e.g. the process engine).
  // Handlers must be registered at module-init time of the consumer.
  fireDecisionHandlers({ approval_request_id: id, decision: next, row });
  emitApprovals();
  return getApprovalRequest(id);
}

// Revert a one-shot approval that was consumed but whose downstream adapter
// returned an error. Without this, every adapter failure (e.g. spec_invalid)
// forces the LLM to ask for a fresh approval on the next retry — the user
// ends up clicking Approve repeatedly while the LLM tweaks the same spec.
export function refundApprovalRequest(id) {
  if (!id) return false;
  const db = getDb();
  const row = getApprovalRequest(id);
  if (!row || row.status !== 'used') return false;
  db.prepare("UPDATE approval_requests SET status = 'approved' WHERE id = ?").run(id);
  db.prepare(`
    INSERT INTO audit_logs (id, at, type, summary, status, meta)
    VALUES (?, ?, 'approval.refund', ?, 'approved', ?)
  `).run(newId('audit'), nowIso(), `Refunded ${row.summary}`, JSON.stringify({ approval_request_id: id, reason: 'adapter_error' }));
  emitApprovals();
  return true;
}

export function consumeApprovalRequest(id, { action_type, matches } = {}) {
  if (!id) return false;
  const db = getDb();
  const row = getApprovalRequest(id);
  if (!row || row.status !== 'approved') return false;
  if (action_type && row.action_type !== action_type) return false;
  if (typeof matches === 'function' && !matches(row.payload || {})) return false;
  if (row.expires_at && row.expires_at <= nowIso()) {
    db.prepare('UPDATE approval_requests SET status = ?, decided_at = ? WHERE id = ?').run('expired', nowIso(), id);
    emitApprovals();
    return false;
  }
  db.prepare('UPDATE approval_requests SET status = ?, decided_at = COALESCE(decided_at, ?) WHERE id = ?')
    .run('used', nowIso(), id);
  emitApprovals();
  return true;
}
