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

function emitHeartbeat() {
  try { publishStateChanged('heartbeat'); } catch { /* bus failure is observation-only */ }
}

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

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

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

export function createHeartbeatTask(payload = {}) {
  const db = getDb();
  const now = nowIso();
  const id = payload.id || newId('hbtask');
  const type = payload.type || payload.payload?.type || 'agent_tick';
  const body = payload.payload || payload;
  db.prepare(`
    INSERT OR IGNORE INTO heartbeat_tasks
      (id, agent_id, schedule_id, type, status, priority, payload, attempts, max_attempts, run_after, created_at, updated_at)
    VALUES (?, ?, ?, ?, 'queued', ?, ?, 0, ?, ?, ?, ?)
  `).run(
    id,
    payload.agent_id || body.agent_id || null,
    payload.schedule_id || body.schedule_id || null,
    type,
    Number(payload.priority || 0),
    JSON.stringify(body),
    Number(payload.max_attempts || process.env.MADARX_HEARTBEAT_MAX_ATTEMPTS || 3),
    payload.run_after || now,
    now,
    now
  );
  appendHeartbeatTaskEvent(id, { type: 'created', message: `Created ${type} task` });
  emitHeartbeat();
  return getHeartbeatTask(id);
}

export function getHeartbeatTask(id) {
  return rowToTask(getDb().prepare('SELECT * FROM heartbeat_tasks WHERE id = ?').get(id));
}

export function listHeartbeatTasks(limit = 50) {
  return getDb().prepare('SELECT * FROM heartbeat_tasks ORDER BY updated_at DESC LIMIT ?').all(limit).map(rowToTask);
}

export function listHeartbeatTaskEvents({ task_id = null, limit = 100 } = {}) {
  const db = getDb();
  if (task_id) {
    return db.prepare('SELECT * FROM heartbeat_task_events WHERE task_id = ? ORDER BY at ASC LIMIT ?')
      .all(task_id, limit).map(rowToEvent);
  }
  return db.prepare('SELECT * FROM heartbeat_task_events ORDER BY at DESC LIMIT ?')
    .all(limit).map(rowToEvent);
}

export function claimHeartbeatTask({ workerId, leaseMs = Number(process.env.MADARX_HEARTBEAT_LEASE_MS || 120000) } = {}) {
  const db = getDb();
  const now = nowIso();
  const task = db.prepare(`
    SELECT * FROM heartbeat_tasks
    WHERE status = 'queued' AND run_after <= ?
    ORDER BY priority DESC, run_after ASC
    LIMIT 1
  `).get(now);
  if (!task) return null;
  const leaseUntil = new Date(Date.now() + leaseMs).toISOString();
  const result = db.prepare(`
    UPDATE heartbeat_tasks
    SET status = 'leased', lease_owner = ?, lease_expires_at = ?, updated_at = ?
    WHERE id = ? AND status = 'queued'
  `).run(workerId, leaseUntil, now, task.id);
  if (!result.changes) return null;
  appendHeartbeatTaskEvent(task.id, { type: 'claimed', message: `Claimed by ${workerId}`, payload: { lease_expires_at: leaseUntil } });
  emitHeartbeat();
  return getHeartbeatTask(task.id);
}

export function markHeartbeatTaskRunning(taskId, workerId) {
  const now = nowIso();
  getDb().prepare("UPDATE heartbeat_tasks SET status='running', lease_owner=COALESCE(lease_owner, ?), updated_at=? WHERE id=?")
    .run(workerId, now, taskId);
  appendHeartbeatTaskEvent(taskId, { type: 'running', message: 'Task started' });
  emitHeartbeat();
  return getHeartbeatTask(taskId);
}

export function renewHeartbeatLease({ taskId, workerId, leaseMs = 120000 }) {
  const leaseUntil = new Date(Date.now() + leaseMs).toISOString();
  getDb().prepare('UPDATE heartbeat_tasks SET lease_expires_at=?, updated_at=? WHERE id=? AND lease_owner=?')
    .run(leaseUntil, nowIso(), taskId, workerId);
  return getHeartbeatTask(taskId);
}

export function appendHeartbeatTaskEvent(taskId, event = {}) {
  getDb().prepare(`
    INSERT INTO heartbeat_task_events (id, task_id, type, message, payload, at)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(newId('hbevent'), taskId, event.type || 'info', event.message || null, event.payload ? JSON.stringify(event.payload) : null, nowIso());
}

export function completeHeartbeatTask(taskId, result = {}) {
  const now = nowIso();
  getDb().prepare(`
    UPDATE heartbeat_tasks SET status='complete', completed_at=?, updated_at=?, error=NULL WHERE id=?
  `).run(now, now, taskId);
  appendHeartbeatTaskEvent(taskId, { type: 'complete', message: 'Task complete', payload: result });
  emitHeartbeat();
  return getHeartbeatTask(taskId);
}

export function failHeartbeatTask(taskId, error) {
  const task = getHeartbeatTask(taskId);
  const attempts = Number(task?.attempts || 0) + 1;
  const maxAttempts = Number(task?.max_attempts || 3);
  const now = nowIso();
  if (attempts >= maxAttempts) {
    getDb().prepare(`
      UPDATE heartbeat_tasks SET status='failed', attempts=?, error=?, completed_at=?, updated_at=? WHERE id=?
    `).run(attempts, String(error?.message || error), now, now, taskId);
    appendHeartbeatTaskEvent(taskId, { type: 'failed', message: String(error?.message || error) });
    emitHeartbeat();
  } else {
    const backoffMs = Math.min(300000, 1000 * 2 ** attempts);
    rescheduleHeartbeatTask(taskId, { backoffMs, error, attempts });
  }
  return getHeartbeatTask(taskId);
}

export function rescheduleHeartbeatTask(taskId, { backoffMs = 1000, error, attempts } = {}) {
  const runAfter = new Date(Date.now() + backoffMs).toISOString();
  const task = getHeartbeatTask(taskId);
  const nextAttempts = attempts ?? Number(task?.attempts || 0) + 1;
  getDb().prepare(`
    UPDATE heartbeat_tasks
    SET status='queued', attempts=?, run_after=?, lease_owner=NULL, lease_expires_at=NULL, error=?, updated_at=?
    WHERE id=?
  `).run(nextAttempts, runAfter, String(error?.message || error || ''), nowIso(), taskId);
  appendHeartbeatTaskEvent(taskId, { type: 'retry', message: `Retry after ${backoffMs}ms` });
  emitHeartbeat();
  return getHeartbeatTask(taskId);
}

export function pauseHeartbeatTaskForApproval(taskId, approvalRequestId, message = 'Approval required') {
  getDb().prepare(`
    UPDATE heartbeat_tasks SET status='approval_required', approval_request_id=?, error=?, updated_at=? WHERE id=?
  `).run(approvalRequestId, message, nowIso(), taskId);
  appendHeartbeatTaskEvent(taskId, { type: 'approval_required', message, payload: { approval_request_id: approvalRequestId } });
  emitHeartbeat();
  try { publishStateChanged('approvals'); } catch { /* observation-only */ }
  return getHeartbeatTask(taskId);
}

export function resumeHeartbeatTasksForApproval(approvalRequestId) {
  const db = getDb();
  const now = nowIso();
  const rows = db.prepare(`
    SELECT id FROM heartbeat_tasks
    WHERE status = 'approval_required' AND approval_request_id = ?
  `).all(approvalRequestId);
  for (const row of rows) {
    db.prepare(`
      UPDATE heartbeat_tasks
      SET status='queued', run_after=?, lease_owner=NULL, lease_expires_at=NULL, error=NULL, updated_at=?
      WHERE id=?
    `).run(now, now, row.id);
    appendHeartbeatTaskEvent(row.id, {
      type: 'approval_resumed',
      message: 'Approval granted; task requeued',
      payload: { approval_request_id: approvalRequestId },
    });
  }
  if (rows.length) emitHeartbeat();
  return rows.length;
}

export function recoverStaleHeartbeatTasks(now = nowIso()) {
  const rows = getDb().prepare(`
    SELECT id FROM heartbeat_tasks
    WHERE status IN ('leased', 'running') AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?
  `).all(now);
  for (const row of rows) {
    getDb().prepare(`
      UPDATE heartbeat_tasks
      SET status='queued', lease_owner=NULL, lease_expires_at=NULL, updated_at=?, error='Recovered stale lease'
      WHERE id=?
    `).run(nowIso(), row.id);
    appendHeartbeatTaskEvent(row.id, { type: 'stale_recovered', message: 'Recovered stale lease' });
  }
  if (rows.length) emitHeartbeat();
  return rows.length;
}
