import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { getDb, newId, nowIso } from './db.js';
import { ARTIFACTS_DIR, ROOT, PKG_ROOT, WORKSPACE, ensureDirs, relToRoot, pickExistingDir } from './paths.js';
import { ensureArtifactDir } from './artifact-paths.js';
import { DEFAULT_PROJECT_ID } from './projects.js';
import { appendAudit, recordArtifact, recordRun, updateRun } from './runtime.js';
import { renderDocumentHtml, stripEmoji } from './artifact-format.js';

const FINAL_EVENT_TYPES = new Set(['complete', 'failed', 'cancelled', 'stale']);

function slugify(input) {
  return String(input || 'hermes-job').toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 80) || 'hermes-job';
}

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

function allowedCwdRoots() {
  const configured = String(process.env.HERMES_ALLOWED_ROOTS || '')
    .split(path.delimiter)
    .map(p => p.trim())
    .filter(Boolean);
  return [
    ROOT,
    WORKSPACE,
    path.dirname(ROOT),
    ...configured,
  ].map(p => {
    const resolved = path.resolve(p);
    return fs.existsSync(resolved) ? fs.realpathSync(resolved) : resolved;
  });
}

export function normalizeHermesCwd(cwd) {
  if (!cwd) return null;
  const resolved = path.resolve(String(cwd));
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
    throw new Error(`Working directory not found: ${resolved}`);
  }
  const checked = fs.realpathSync(resolved);
  const allowed = allowedCwdRoots();
  if (!allowed.some(root => checked === root || checked.startsWith(`${root}${path.sep}`))) {
    throw new Error(`Working directory is outside allowed roots: ${resolved}`);
  }
  return resolved;
}

function rowToJob(row) {
  return row ? { ...row } : null;
}

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

function writeArtifactFile(filePath, content) {
  const abs = path.resolve(ROOT, filePath);
  const workspace = fs.realpathSync(WORKSPACE);
  const parent = fs.existsSync(path.dirname(abs)) ? fs.realpathSync(path.dirname(abs)) : path.resolve(path.dirname(abs));
  const checked = path.join(parent, path.basename(abs));
  if (!(checked === workspace || checked.startsWith(`${workspace}${path.sep}`))) {
    throw new Error(`Refusing to write artifact outside workspace: ${filePath}`);
  }
  fs.mkdirSync(path.dirname(abs), { recursive: true });
  fs.writeFileSync(abs, content, 'utf8');
}

function truncate(value, max = 6000) {
  const text = String(value || '');
  return text.length > max ? `${text.slice(0, max)}\n\n[truncated ${text.length - max} chars]` : text;
}

export function listHermesJobs(limit = 20) {
  const db = getDb();
  return db.prepare('SELECT * FROM hermes_jobs ORDER BY updated_at DESC LIMIT ?').all(limit).map(rowToJob);
}

export function getHermesJob(id) {
  const db = getDb();
  return rowToJob(db.prepare('SELECT * FROM hermes_jobs WHERE id = ?').get(id));
}

export function listHermesJobEvents(jobId, limit = 200) {
  const db = getDb();
  return db.prepare('SELECT * FROM hermes_job_events WHERE job_id = ? ORDER BY at ASC LIMIT ?')
    .all(jobId, limit)
    .map(rowToEvent);
}

export function getHermesJobWithEvents(id) {
  const job = getHermesJob(id);
  if (!job) return null;
  return { ...job, events: listHermesJobEvents(id, 500) };
}

export function renderHermesJobArtifact(job, events = []) {
  const status = job.status || 'queued';
  const lines = [];

  const finalEvent = [...events].reverse().find(event => event.type === 'final');
  const errorEvent = [...events].reverse().find(event => event.type === 'error');
  if (finalEvent?.message) {
    lines.push(finalEvent.message, '');
  } else if (job.error || errorEvent?.message) {
    lines.push('## Error', '', job.error || errorEvent.message, '');
  }

  const progress = events.filter(event => !['final'].includes(event.type));
  if (progress.length) {
    lines.push('## Progress', '');
    for (const event of progress.slice(-80)) {
      const stamp = event.at ? event.at.replace('T', ' ').replace(/\.\d+Z$/, 'Z') : '';
      const message = truncate(event.message || event.type, 1200).replace(/\n/g, '\n  ');
      lines.push(`- ${stamp} ${event.type}: ${message}`.trim());
    }
    lines.push('');
  }

  if (status === 'running') {
    lines.push('## Current state', '', 'Hermes is still running. This artifact updates as new progress arrives.', '');
  } else if (status === 'queued') {
    lines.push('## Current state', '', 'Hermes is queued and will start shortly.', '');
  }

  return `${lines.join('\n')}\n`;
}

export function refreshHermesJobArtifact(jobId) {
  const job = getHermesJob(jobId);
  if (!job?.artifact_id) return null;
  const events = listHermesJobEvents(jobId, 500);
  const markdown = stripEmoji(renderHermesJobArtifact(job, events));
  const html = renderDocumentHtml({ title: job.title || 'Hermes Job', markdown, meta: `Status: ${job.status || 'queued'}` });
  const db = getDb();
  const artifact = db.prepare('SELECT * FROM artifacts WHERE id = ?').get(job.artifact_id);
  if (!artifact) return null;
  db.prepare('UPDATE artifacts SET content = ?, source_md = ?, kind = ? WHERE id = ?')
    .run(html, markdown, 'document', job.artifact_id);
  writeArtifactFile(artifact.path, markdown);
  return { artifact_id: job.artifact_id, content: html };
}

export function appendHermesJobEvent(jobId, { type, message, payload } = {}) {
  const db = getDb();
  const id = newId('hermes-event');
  const at = nowIso();
  db.prepare(`
    INSERT INTO hermes_job_events (id, job_id, type, message, payload, at)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(id, jobId, type || 'status', message || null, payload ? JSON.stringify(payload) : null, at);
  db.prepare('UPDATE hermes_jobs SET updated_at = ? WHERE id = ?').run(at, jobId);
  refreshHermesJobArtifact(jobId);
  return { id, job_id: jobId, type: type || 'status', message, payload, at };
}

export function createHermesJob({ prompt, title, cwd, session_id, model, timeout_ms, project_id } = {}) {
  if (!prompt || typeof prompt !== 'string') throw new Error('prompt is required');
  ensureDirs();
  const safeCwd = normalizeHermesCwd(cwd);
  const id = newId('hermes-job');
  const now = nowIso();
  const jobTitle = title || `Hermes job: ${prompt.slice(0, 60)}`;
  const scopedProjectId = project_id || DEFAULT_PROJECT_ID;
  const artifactDir = ensureArtifactDir(scopedProjectId);
  const file = path.join(artifactDir, `${slugify(jobTitle)}-${Date.now().toString(36)}.md`);
  const filePath = relToRoot(file);

  const run = recordRun({
    kind: 'hermes-job',
    title: jobTitle,
    agent: 'Hermes Agent',
    agent_id: 'hermes',
    status: 'running',
    artifact: filePath,
    preview: `Hermes job queued: ${prompt.slice(0, 160)}`,
    triggered_by: 'claude',
  });

  const placeholderJob = {
    id,
    title: jobTitle,
    prompt,
    cwd: safeCwd,
    status: 'queued',
    created_at: now,
    updated_at: now,
  };
  const markdown = stripEmoji(renderHermesJobArtifact(placeholderJob, []));
  const html = renderDocumentHtml({ title: jobTitle, markdown, meta: 'Status: queued' });
  writeArtifactFile(filePath, markdown);
  const artifactId = recordArtifact({
    run_id: run.id,
    agent_id: 'hermes',
    file_path: filePath,
    kind: 'document',
    title: jobTitle,
    content: html,
    source_md: markdown,
    project_id: scopedProjectId,
  });

  const db = getDb();
  db.prepare(`
    INSERT INTO hermes_jobs (
      id, artifact_id, run_id, status, title, prompt, cwd, hermes_session_id, model,
      timeout_ms, created_at, updated_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    id,
    artifactId,
    run.id,
    'queued',
    jobTitle,
    prompt,
    safeCwd,
    session_id || null,
    model || null,
    timeout_ms ? Number(timeout_ms) : null,
    now,
    now
  );

  appendHermesJobEvent(id, { type: 'queued', message: 'Hermes job queued.' });
  appendAudit({
    type: 'hermes.job.create',
    summary: `Queued Hermes job: ${jobTitle}`,
    status: 'queued',
    meta: { job_id: id, run_id: run.id, artifact_id: artifactId },
  });
  return { ...getHermesJob(id), file_path: filePath };
}

export function startHermesJobWorker(jobId) {
  // PKG_ROOT is the real install dir at runtime (MADARX_PKG_ROOT). ROOT is the
  // build-time-baked path and is wrong in the packaged web bundle. Resolve the
  // runner script absolutely against PKG_ROOT so it is found regardless of cwd,
  // and spawn from a cwd that actually exists.
  const runnerScript = path.join(PKG_ROOT, 'scripts', 'hermes-job-runner.mjs');
  const child = spawn(process.execPath, [runnerScript, '--job-id', jobId], {
    cwd: pickExistingDir(PKG_ROOT, WORKSPACE),
    env: process.env,
    stdio: 'ignore',
    detached: true,
  });
  child.unref();
  appendHermesJobEvent(jobId, { type: 'status', message: `Background runner spawned with pid ${child.pid}.` });
  return { pid: child.pid };
}

export function markHermesJobRunning(id, { pid } = {}) {
  const db = getDb();
  const now = nowIso();
  db.prepare(`
    UPDATE hermes_jobs
    SET status = 'running', pid = ?, started_at = COALESCE(started_at, ?), updated_at = ?
    WHERE id = ?
  `).run(pid || null, now, now, id);
  appendHermesJobEvent(id, { type: 'started', message: pid ? `Hermes runner started with pid ${pid}.` : 'Hermes runner started.' });
  return getHermesJob(id);
}

export function completeHermesJob(id, { response, hermes_session_id, model, provider, exit_code = 0, format_source, format_violations } = {}) {
  const db = getDb();
  const now = nowIso();
  db.prepare(`
    UPDATE hermes_jobs
    SET status = 'complete', hermes_session_id = COALESCE(?, hermes_session_id),
        model = COALESCE(?, model), provider = COALESCE(?, provider),
        exit_code = ?, completed_at = ?, updated_at = ?
    WHERE id = ?
  `).run(hermes_session_id || null, model || null, provider || null, exit_code, now, now, id);
  if (format_source || format_violations?.length) {
    appendHermesJobEvent(id, {
      type: 'format',
      message: format_violations?.length ? `Format source: ${format_source}; violations: ${format_violations.join(', ')}` : `Format source: ${format_source}`,
      payload: { source: format_source, violations: format_violations || [] },
    });
  }
  appendHermesJobEvent(id, { type: 'final', message: response || '(no response)' });
  const job = getHermesJob(id);
  if (job?.run_id) {
    updateRun(job.run_id, {
      status: 'complete',
      preview: String(response || '').slice(0, 300),
      output: response || '',
    });
  }
  appendAudit({ type: 'hermes.job.complete', summary: `Completed Hermes job: ${job?.title || id}`, status: 'complete', meta: { job_id: id } });
  return getHermesJob(id);
}

export function failHermesJob(id, { error, exit_code = null, status = 'failed' } = {}) {
  const finalStatus = FINAL_EVENT_TYPES.has(status) ? status : 'failed';
  const db = getDb();
  const now = nowIso();
  db.prepare(`
    UPDATE hermes_jobs
    SET status = ?, error = ?, exit_code = ?, completed_at = ?, updated_at = ?
    WHERE id = ?
  `).run(finalStatus, error || 'Hermes job failed', exit_code, now, now, id);
  appendHermesJobEvent(id, { type: 'error', message: error || 'Hermes job failed.' });
  const job = getHermesJob(id);
  if (job?.run_id) {
    updateRun(job.run_id, {
      status: finalStatus === 'cancelled' ? 'cancelled' : 'failed',
      preview: error || 'Hermes job failed.',
      output: error || '',
    });
  }
  appendAudit({ type: 'hermes.job.fail', summary: `Hermes job failed: ${job?.title || id}`, status: finalStatus, meta: { job_id: id, error } });
  return getHermesJob(id);
}

export function markStaleHermesJobs({ olderThanMs = 30 * 60 * 1000 } = {}) {
  const db = getDb();
  const cutoff = new Date(Date.now() - olderThanMs).toISOString();
  const rows = db.prepare(`
    SELECT id FROM hermes_jobs
    WHERE status IN ('queued', 'running') AND updated_at < ?
  `).all(cutoff);
  for (const row of rows) {
    failHermesJob(row.id, { status: 'stale', error: 'Hermes job became stale before completion.' });
  }
  return rows.length;
}
