// lib/process/definitions.js — load, list, validate, upsert, archive process
// definitions.
//
// All structural validation now flows through lib/process/spec-validator.js so
// internal seeds and external (API / agent) authoring share one rulebook.
// upsertDefinition() is the SOLE writer of process_definitions +
// process_stages; the engine never writes back.

import { getDb, nowIso, newId } from '../db.js';
import { validateWorkflowSpec, VALID_STAGE_KINDS } from './spec-validator.js';
import { safeJson, loadKnownAgentTemplates } from './util.js';

function rowToDefinition(row) {
  if (!row) return null;
  return {
    ...row,
    intake_form_schema: safeJson(row.intake_form_schema_json, null),
    source_spec: safeJson(row.source_spec_json, null),
  };
}

function rowToStage(row) {
  if (!row) return null;
  return {
    ...row,
    required: row.required === 1,
    config: safeJson(row.config_json, {}),
  };
}

export function listDefinitions({ status = 'published' } = {}) {
  const db = getDb();
  const rows = status === 'all'
    ? db.prepare('SELECT * FROM process_definitions ORDER BY created_at ASC').all()
    : db.prepare('SELECT * FROM process_definitions WHERE status = ? ORDER BY created_at ASC').all(status);
  return rows.map(rowToDefinition);
}

export function getDefinition(slugOrId) {
  if (!slugOrId) return null;
  const db = getDb();
  const row = db.prepare('SELECT * FROM process_definitions WHERE slug = ? OR id = ? LIMIT 1').get(slugOrId, slugOrId);
  return rowToDefinition(row);
}

export function listStages(definitionId) {
  if (!definitionId) return [];
  return getDb()
    .prepare('SELECT * FROM process_stages WHERE definition_id = ? ORDER BY position ASC')
    .all(definitionId)
    .map(rowToStage);
}

export function getStage(stageId) {
  if (!stageId) return null;
  return rowToStage(getDb().prepare('SELECT * FROM process_stages WHERE id = ?').get(stageId));
}

export function getStageBySlug(definitionId, slug) {
  if (!definitionId || !slug) return null;
  return rowToStage(
    getDb().prepare('SELECT * FROM process_stages WHERE definition_id = ? AND slug = ?').get(definitionId, slug)
  );
}

export function getFirstStage(definitionId) {
  if (!definitionId) return null;
  return rowToStage(
    getDb()
      .prepare('SELECT * FROM process_stages WHERE definition_id = ? ORDER BY position ASC LIMIT 1')
      .get(definitionId)
  );
}

// Resolve the next stage from a given stage. Honors on_pass_stage_slug; falls
// back to position+1 in the same definition. Returns null at the end.
export function nextStageAfter(stage) {
  if (!stage) return null;
  if (stage.on_pass_stage_slug) {
    return getStageBySlug(stage.definition_id, stage.on_pass_stage_slug);
  }
  const db = getDb();
  const row = db
    .prepare('SELECT * FROM process_stages WHERE definition_id = ? AND position > ? ORDER BY position ASC LIMIT 1')
    .get(stage.definition_id, stage.position);
  return rowToStage(row);
}

export function failureStageAfter(stage) {
  if (!stage || !stage.on_fail_stage_slug) return null;
  return getStageBySlug(stage.definition_id, stage.on_fail_stage_slug);
}

// Compares the persisted stage shape against an incoming spec to decide
// whether a version bump is required when overwriting a published definition.
function stagesDifferStructurally(existingStages, incomingStages) {
  if (existingStages.length !== incomingStages.length) return true;
  for (let i = 0; i < existingStages.length; i++) {
    const a = existingStages[i];
    const b = incomingStages[i];
    if (a.slug !== b.slug) return true;
    if (a.kind !== b.kind) return true;
    if ((a.on_pass_stage_slug || null) !== (b.on_pass_stage_slug || null)) return true;
    if ((a.on_fail_stage_slug || null) !== (b.on_fail_stage_slug || null)) return true;
  }
  return false;
}

// Back-compat: throws on bad shape. Now a thin wrapper around the strict
// validator. Used by older callers; new callers should call validateWorkflowSpec
// directly and inspect the structured result.
export function validateDefinitionSpec(spec) {
  const db = getDb();
  const result = validateWorkflowSpec(spec, { knownAgentTemplates: loadKnownAgentTemplates(db) });
  if (!result.ok) {
    const err = new Error(`process definition: ${result.errors.join('; ')}`);
    err.code = 'spec_invalid';
    err.errors = result.errors;
    throw err;
  }
  return true;
}

// Idempotent upsert of a definition + its stages. Returns the upserted row.
//
// opts:
//   source_spec_json — original spec as a string (preserves agent/API
//     authoring intent across re-seeds). When omitted, defaults to
//     JSON.stringify(spec) so we never persist a headless row.
//   created_by_user_id, created_by_agent_id — provenance.
//   skipValidation — internal escape hatch (NEVER set this from API code).
export function upsertDefinition(spec, opts = {}) {
  const db = getDb();
  if (!opts.skipValidation) {
    const result = validateWorkflowSpec(spec, { knownAgentTemplates: loadKnownAgentTemplates(db) });
    if (!result.ok) {
      const err = new Error(`process definition: ${result.errors.join('; ')}`);
      err.code = 'spec_invalid';
      err.errors = result.errors;
      throw err;
    }
  }
  const now = nowIso();
  const id = `procdef:${spec.slug}`;
  const intakeStage = spec.stages.find(s => s.kind === 'intake_form');
  const intakeSchemaJson = intakeStage?.config?.form_schema
    ? JSON.stringify(intakeStage.config.form_schema)
    : null;
  const sourceSpecJson = opts.source_spec_json
    ? (typeof opts.source_spec_json === 'string' ? opts.source_spec_json : JSON.stringify(opts.source_spec_json))
    : JSON.stringify(spec);

  const existing = db.prepare('SELECT * FROM process_definitions WHERE id = ?').get(id);

  // Version-bump enforcement: refuse to overwrite a published definition with
  // a structurally different stage list at the same version number.
  if (existing && existing.status === 'published') {
    const existingStages = db
      .prepare('SELECT slug, kind, on_pass_stage_slug, on_fail_stage_slug FROM process_stages WHERE definition_id = ? ORDER BY position ASC')
      .all(id);
    const sameVersion = (spec.version || 1) === existing.version;
    if (sameVersion && stagesDifferStructurally(existingStages, spec.stages)) {
      const err = new Error(`process definition ${spec.slug}: stage list changed but version not bumped (currently v${existing.version})`);
      err.code = 'version_bump_required';
      throw err;
    }
  }

  // schema_version is intentionally not written here. The column exists with
  // DEFAULT 1 in lib/schema.js so legacy callers reading the column still see
  // a value, but the engine treats schema_version as reserved-for-future-use
  // until the first migration ships. See spec-validator.js for the matching
  // accept-but-don't-enforce stance.
  if (existing) {
    db.prepare(`
      UPDATE process_definitions
         SET name = ?, version = ?, description = ?, category = ?,
             intake_form_schema_json = ?, source = ?, status = ?,
             source_spec_json = ?,
             created_by_user_id = COALESCE(?, created_by_user_id),
             created_by_agent_id = COALESCE(?, created_by_agent_id),
             updated_at = ?
       WHERE id = ?
    `).run(
      spec.name,
      spec.version || existing.version || 1,
      spec.description || null,
      spec.category || null,
      intakeSchemaJson,
      spec.source || existing.source || 'seed',
      spec.status || existing.status || 'published',
      sourceSpecJson,
      opts.created_by_user_id || null,
      opts.created_by_agent_id || null,
      now,
      id
    );
  } else {
    db.prepare(`
      INSERT INTO process_definitions
        (id, slug, name, version, description, category, intake_form_schema_json,
         source, status, source_spec_json,
         created_by_user_id, created_by_agent_id,
         created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      id,
      spec.slug,
      spec.name,
      spec.version || 1,
      spec.description || null,
      spec.category || null,
      intakeSchemaJson,
      spec.source || 'seed',
      spec.status || 'published',
      sourceSpecJson,
      opts.created_by_user_id || null,
      opts.created_by_agent_id || null,
      now,
      now
    );
  }

  // Replace stage rows in a transaction. Stages are append-only externally,
  // but seeds re-author the canonical shape on every boot.
  db.exec('BEGIN');
  try {
    db.prepare('DELETE FROM process_stages WHERE definition_id = ?').run(id);
    spec.stages.forEach((stage, position) => {
      db.prepare(`
        INSERT INTO process_stages
          (id, definition_id, position, slug, name, kind, config_json, on_pass_stage_slug, on_fail_stage_slug, required)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        `procstage:${spec.slug}:${stage.slug}`,
        id,
        position,
        stage.slug,
        stage.name,
        stage.kind,
        JSON.stringify(stage.config || {}),
        stage.on_pass_stage_slug || null,
        stage.on_fail_stage_slug || null,
        stage.required === false ? 0 : 1
      );
    });
    db.exec('COMMIT');
  } catch (err) {
    try { db.exec('ROLLBACK'); } catch { /* observation only */ }
    throw err;
  }

  return getDefinition(spec.slug);
}

// Soft-archive a definition. Rows are kept (FK from process_cases.definition_id
// is RESTRICT). Returns { ok, archived, in_flight_cases }.
export function archiveDefinition(slug) {
  if (!slug) throw new Error('archiveDefinition: slug required');
  const db = getDb();
  const def = db.prepare('SELECT * FROM process_definitions WHERE slug = ?').get(slug);
  if (!def) return { ok: false, error: 'not_found' };

  const inFlight = db
    .prepare("SELECT COUNT(*) AS n FROM process_cases WHERE definition_id = ? AND status NOT IN ('complete','cancelled','rejected','failed')")
    .get(def.id).n;
  if (inFlight > 0) {
    return { ok: false, error: 'in_flight_cases', in_flight_cases: inFlight };
  }

  db.prepare("UPDATE process_definitions SET status = 'archived', updated_at = ? WHERE id = ?").run(nowIso(), def.id);
  return { ok: true, archived: true, in_flight_cases: 0 };
}

export { VALID_STAGE_KINDS, validateWorkflowSpec };

// newId/nowIso re-exports for seed modules that want consistent IDs without
// having to dual-import from lib/db.js.
export { newId, nowIso };
