// lib/hail/interview.js — the HAIL Configuration Interview.
//
// A structured, resumable interview that captures the principal's operating
// reality with a SENSITIVITY CLASSIFICATION AT CAPTURE. This is the single most
// sensitive surface in HAIL, so it enforces three rules in code:
//
//   1. Every recorded answer MUST carry a valid sensitivity class. A missing or
//      unknown class is rejected (the API surface maps this to HTTP 422).
//   2. `refused` answers store NO value — only the marker that the principal
//      declined. The raw text never lands in the DB.
//   3. `restricted` answers stay local (the local-first boundary) and are NEVER
//      rendered verbatim into any artifact — the transcript redacts them.
//
// Practitioner notes (`is_practitioner_note=1`) are kept separate from principal
// answers and excluded from the principal-facing transcript.
//
// This module NEVER writes process_cases.* — interview data lives only in the
// HAIL tables, never in the engine case's intake payload.
//
// See specs/active/hail-operating-model.md + hail-development-plan.md Phase 2.

import { getDb, nowIso, newId } from '../db.js';
import { appendAudit, createArtifact } from '../runtime.js';
import { getEngagement } from './engagements.js';
import {
  SENSITIVITY_SET,
  CONFIDENCE_SET,
  INTERVIEW_SECTIONS,
  INTERVIEW_SECTION_SET,
  isLocalEngine,
} from './constants.js';

// Markers used wherever a sensitive value must not appear in cleartext.
export const RESTRICTED_MARKER = '[restricted — withheld from artifact]';
export const REFUSED_MARKER = '[declined by principal]';

function rowToInterview(row) {
  if (!row) return null;
  return { ...row, is_practitioner_note: undefined };
}

function rowToResponse(row) {
  if (!row) return null;
  return { ...row, is_practitioner_note: row.is_practitioner_note === 1 };
}

// ── Sessions ─────────────────────────────────────────────────────────────────

export function getInterview(id) {
  if (!id) return null;
  return rowToInterview(getDb().prepare('SELECT * FROM hail_interviews WHERE id = ?').get(id));
}

export function listInterviews(engagement_id) {
  if (!engagement_id) return [];
  return getDb()
    .prepare('SELECT * FROM hail_interviews WHERE engagement_id = ? ORDER BY created_at ASC')
    .all(engagement_id)
    .map(rowToInterview);
}

export function startInterview({ engagement_id, title = null, conducted_by_user_id = null }) {
  if (!engagement_id) throw new Error('startInterview: engagement_id is required');
  const engagement = getEngagement(engagement_id);
  if (!engagement) throw new Error(`startInterview: engagement "${engagement_id}" not found`);

  const db = getDb();
  const id = newId('hailiv');
  const now = nowIso();
  db.prepare(`
    INSERT INTO hail_interviews
      (id, engagement_id, title, status, section_cursor, conducted_by_user_id, created_at, updated_at)
    VALUES (?, ?, ?, 'in_progress', ?, ?, ?, ?)
  `).run(
    id,
    engagement_id,
    title || `Configuration Interview — ${engagement.principal_name}`,
    INTERVIEW_SECTIONS[0].slug,
    conducted_by_user_id,
    now,
    now
  );
  appendAudit({
    type: 'hail.interview.start',
    summary: `HAIL interview started for ${engagement.principal_name}`,
    status: 'ok',
    meta: { engagement_id, interview_id: id },
  });
  return getInterview(id);
}

export function pauseInterview(id) {
  const iv = getInterview(id);
  if (!iv) throw new Error(`pauseInterview: interview "${id}" not found`);
  getDb().prepare("UPDATE hail_interviews SET status = 'paused', updated_at = ? WHERE id = ?")
    .run(nowIso(), id);
  return getInterview(id);
}

// Resume an interview; optionally jump the section cursor (used by autosave so a
// resumed session lands on the section the principal left off at).
export function resumeInterview(id, { section_cursor = null } = {}) {
  const iv = getInterview(id);
  if (!iv) throw new Error(`resumeInterview: interview "${id}" not found`);
  if (section_cursor != null && !INTERVIEW_SECTION_SET.has(section_cursor)) {
    throw new Error(`resumeInterview: unknown section "${section_cursor}"`);
  }
  const db = getDb();
  const cursor = section_cursor || iv.section_cursor;
  db.prepare("UPDATE hail_interviews SET status = 'in_progress', section_cursor = ?, updated_at = ? WHERE id = ?")
    .run(cursor, nowIso(), id);
  return getInterview(id);
}

export function completeInterview(id) {
  const iv = getInterview(id);
  if (!iv) throw new Error(`completeInterview: interview "${id}" not found`);
  getDb().prepare("UPDATE hail_interviews SET status = 'complete', updated_at = ? WHERE id = ?")
    .run(nowIso(), id);
  return getInterview(id);
}

// Lightweight cursor save for autosave — does not change status.
export function setSectionCursor(id, section) {
  if (!INTERVIEW_SECTION_SET.has(section)) {
    throw new Error(`setSectionCursor: unknown section "${section}"`);
  }
  const iv = getInterview(id);
  if (!iv) throw new Error(`setSectionCursor: interview "${id}" not found`);
  getDb().prepare('UPDATE hail_interviews SET section_cursor = ?, updated_at = ? WHERE id = ?')
    .run(section, nowIso(), id);
  return getInterview(id);
}

// ── Responses ────────────────────────────────────────────────────────────────

// Record (or update) one answer. Classification is REQUIRED and validated.
// Update-or-insert keyed on (interview_id, question_key, is_practitioner_note)
// so a resumed session can revise an answer in place without duplicating rows,
// and a practitioner note never overwrites the principal's own answer.
export function recordResponse({
  interview_id,
  section,
  question_key,
  answer = null,
  sensitivity,
  confidence = null,
  evidence = null,
  is_practitioner_note = false,
}) {
  if (!interview_id) throw badRequest('interview_id is required');
  const iv = getInterview(interview_id);
  if (!iv) throw notFound(`interview "${interview_id}" not found`);
  if (!section || !INTERVIEW_SECTION_SET.has(section)) {
    throw badRequest(`unknown section "${section}"`);
  }
  if (!question_key || typeof question_key !== 'string') {
    throw badRequest('question_key is required');
  }
  // Rule 1: classification at capture is mandatory.
  if (!sensitivity || !SENSITIVITY_SET.has(sensitivity)) {
    throw badRequest(
      `sensitivity is required and must be one of ${[...SENSITIVITY_SET].join(', ')}`,
      'sensitivity_required'
    );
  }
  if (confidence != null && !CONFIDENCE_SET.has(confidence)) {
    throw badRequest(`unknown confidence "${confidence}"`);
  }

  // Rule 2: a refused answer stores NO value — neither the answer nor evidence.
  const storedAnswer = sensitivity === 'refused' ? null : (answer == null ? null : String(answer));
  const storedEvidence = sensitivity === 'refused' ? null : evidence;
  const noteFlag = is_practitioner_note ? 1 : 0;

  const db = getDb();
  const now = nowIso();
  const existing = db.prepare(
    'SELECT id FROM hail_interview_responses WHERE interview_id = ? AND question_key = ? AND is_practitioner_note = ?'
  ).get(interview_id, question_key, noteFlag);

  let responseId;
  if (existing) {
    responseId = existing.id;
    db.prepare(`
      UPDATE hail_interview_responses
         SET section = ?, answer = ?, sensitivity = ?, confidence = ?, evidence = ?, updated_at = ?
       WHERE id = ?
    `).run(section, storedAnswer, sensitivity, confidence, storedEvidence, now, responseId);
  } else {
    responseId = newId('hailresp');
    db.prepare(`
      INSERT INTO hail_interview_responses
        (id, interview_id, engagement_id, section, question_key, answer, sensitivity,
         confidence, evidence, is_practitioner_note, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      responseId, interview_id, iv.engagement_id, section, question_key,
      storedAnswer, sensitivity, confidence, storedEvidence, noteFlag, now, now
    );
  }

  // Audit records the classification + key, NEVER the answer value.
  appendAudit({
    type: 'hail.interview.response',
    summary: `HAIL interview response (${sensitivity}) for ${question_key}`,
    status: 'ok',
    meta: {
      engagement_id: iv.engagement_id,
      interview_id,
      section,
      question_key,
      sensitivity,
      is_practitioner_note: noteFlag === 1,
    },
  });

  return rowToResponse(db.prepare('SELECT * FROM hail_interview_responses WHERE id = ?').get(responseId));
}

// Raw responses for an interview. By default practitioner notes are INCLUDED
// (for the practitioner UI). The transcript path filters them out.
export function listResponses(interview_id, { include_practitioner_notes = true } = {}) {
  if (!interview_id) return [];
  const db = getDb();
  const rows = include_practitioner_notes
    ? db.prepare('SELECT * FROM hail_interview_responses WHERE interview_id = ? ORDER BY created_at ASC').all(interview_id)
    : db.prepare('SELECT * FROM hail_interview_responses WHERE interview_id = ? AND is_practitioner_note = 0 ORDER BY created_at ASC').all(interview_id);
  return rows.map(rowToResponse);
}

// The display value for a response, applying the redaction policy. `restricted`
// answers are NEVER returned in cleartext here; `refused` answers show the
// declined marker. Used by the transcript renderer and any artifact path.
export function displayValueFor(response) {
  if (!response) return '';
  if (response.sensitivity === 'refused') return REFUSED_MARKER;
  if (response.sensitivity === 'restricted') return RESTRICTED_MARKER;
  return response.answer == null ? '' : String(response.answer);
}

// Client-safe projection of a response: the raw `answer` is dropped entirely and
// replaced with the redaction-policy `display` value, so a restricted value
// NEVER crosses into a client bundle / browser payload. Use this anywhere a
// response is serialized to the browser (e.g. the interview capture UI).
export function toDisplayResponse(r) {
  if (!r) return null;
  return {
    id: r.id,
    section: r.section,
    question_key: r.question_key,
    sensitivity: r.sensitivity,
    confidence: r.confidence,
    is_practitioner_note: r.is_practitioner_note === true || r.is_practitioner_note === 1,
    redacted: r.sensitivity === 'restricted' || r.sensitivity === 'refused',
    display: displayValueFor(r),
  };
}

export function listDisplayResponses(interview_id, { include_practitioner_notes = true } = {}) {
  return listResponses(interview_id, { include_practitioner_notes }).map(toDisplayResponse);
}

// Project a stored response for consumption by an inference ENGINE. `restricted`
// answers are masked for any non-local engine (the local-first boundary —
// matches lib/disk.js / lib/agent-memory.js, which only trust `ollama`).
// `refused` answers have no value. Local engines (ollama) may see restricted
// content because it never leaves the device.
export function responseForEngine(r, engine) {
  if (!r) return null;
  let answer = r.answer;
  let masked = false;
  if (r.sensitivity === 'refused') {
    answer = null;
  } else if (r.sensitivity === 'restricted' && !isLocalEngine(engine)) {
    answer = '[restricted: withheld from non-local engine]';
    masked = true;
  }
  return {
    section: r.section,
    question_key: r.question_key,
    sensitivity: r.sensitivity,
    confidence: r.confidence,
    is_practitioner_note: r.is_practitioner_note === true || r.is_practitioner_note === 1,
    answer,
    masked,
  };
}

// All responses for an engagement (across interview sessions), projected for an
// engine. This is the diagnostic agent's input — restricted content is masked
// for cloud engines so it never crosses the local-first boundary.
export function listResponsesForEngagementEngine(engagement_id, engine, { include_practitioner_notes = true } = {}) {
  if (!engagement_id) return [];
  if (!engine) throw new Error('listResponsesForEngagementEngine: engine is required');
  const db = getDb();
  const rows = include_practitioner_notes
    ? db.prepare('SELECT * FROM hail_interview_responses WHERE engagement_id = ? ORDER BY created_at ASC').all(engagement_id)
    : db.prepare('SELECT * FROM hail_interview_responses WHERE engagement_id = ? AND is_practitioner_note = 0 ORDER BY created_at ASC').all(engagement_id);
  return rows.map(rowToResponse).map((r) => responseForEngine(r, engine));
}

// ── Transcript ───────────────────────────────────────────────────────────────

// Build the principal-facing transcript model: sections in canonical order,
// principal answers only (practitioner notes excluded), restricted redacted,
// refused marked declined. No raw restricted value ever appears.
export function getTranscript(interview_id) {
  const iv = getInterview(interview_id);
  if (!iv) throw notFound(`interview "${interview_id}" not found`);
  const responses = listResponses(interview_id, { include_practitioner_notes: false });
  const bySection = new Map(INTERVIEW_SECTIONS.map((s) => [s.slug, []]));
  for (const r of responses) {
    if (!bySection.has(r.section)) bySection.set(r.section, []);
    bySection.get(r.section).push({
      question_key: r.question_key,
      sensitivity: r.sensitivity,
      confidence: r.confidence,
      value: displayValueFor(r),
      redacted: r.sensitivity === 'restricted' || r.sensitivity === 'refused',
    });
  }
  const sections = INTERVIEW_SECTIONS
    .map((s) => ({ slug: s.slug, label: s.label, items: bySection.get(s.slug) || [] }))
    .filter((s) => s.items.length > 0);
  return {
    interview_id: iv.id,
    engagement_id: iv.engagement_id,
    status: iv.status,
    section_cursor: iv.section_cursor,
    sections,
  };
}

function escapeMd(s) {
  return String(s == null ? '' : s).replace(/[\\`*_{}\[\]<>]/g, (c) => `\\${c}`);
}

// Render the transcript to a markdown artifact via the standard createArtifact
// path (so it lands in the engagement's project library with ACL/encryption).
// Restricted values are redacted; practitioner notes are excluded.
export function renderTranscriptArtifact(interview_id) {
  const transcript = getTranscript(interview_id);
  const iv = getInterview(interview_id);
  const engagement = getEngagement(iv.engagement_id);

  const lines = [];
  lines.push(`# HAIL Configuration Interview — ${escapeMd(engagement?.principal_name || 'Principal')}`);
  lines.push('');
  lines.push('> Principal-facing transcript. Restricted answers are withheld; declined answers are marked. Practitioner notes are excluded.');
  lines.push('');
  for (const section of transcript.sections) {
    lines.push(`## ${escapeMd(section.label)}`);
    lines.push('');
    for (const item of section.items) {
      // item.value is already the redaction-policy display (marker for restricted/
      // refused), so escaping only applies to non-redacted cleartext.
      const value = item.redacted ? item.value : escapeMd(item.value);
      lines.push(`- **${escapeMd(item.question_key)}** _(${item.sensitivity})_: ${value}`);
    }
    lines.push('');
  }
  const content = lines.join('\n');

  const result = createArtifact({
    title: `HAIL Interview transcript — ${engagement?.principal_name || iv.engagement_id}`,
    kind: 'markdown',
    content,
    project_id: engagement?.project_id || null,
    agent_id: 'hail-interview',
  });

  appendAudit({
    type: 'hail.interview.transcript',
    summary: `HAIL interview transcript rendered for ${engagement?.principal_name || iv.engagement_id}`,
    status: 'ok',
    meta: { engagement_id: iv.engagement_id, interview_id, artifact_id: result.artifact_id },
  });
  return result;
}

// ── error helpers (carry a status so API routes can map cleanly) ─────────────

function badRequest(message, code = 'bad_request') {
  const err = new Error(message);
  err.status = 422;
  err.code = code;
  return err;
}
function notFound(message) {
  const err = new Error(message);
  err.status = 404;
  err.code = 'not_found';
  return err;
}
