'use strict';

const path = require('path');
const crypto = require('crypto');

const VALID_TYPES = new Set(['bug', 'confusing_ux', 'wrong_answer', 'feature_request', 'performance', 'crash', 'other']);
const VALID_SEVERITIES = new Set(['low', 'medium', 'high', 'blocking']);
const VALID_CONSENT = new Set(['maintainer_report', 'diagnostic_bundle', 'local_only', 'local_walle', 'anonymous_aggregate']);
const VALID_STATUS = new Set(['draft', 'triaging', 'needs_clarification', 'actionable', 'evidence_ready', 'submitted', 'closed']);
const VALID_INCLUDE = new Set(['suggested', 'included', 'excluded']);

function nowIso() {
  return new Date().toISOString();
}

function id(prefix) {
  return `${prefix}_${crypto.randomUUID()}`;
}

function cleanText(value, max = 12000) {
  return String(value || '').replace(/\r\n/g, '\n').replace(/\0/g, '').slice(0, max);
}

function cleanHtml(value, max = 24000) {
  return String(value || '').replace(/\0/g, '').slice(0, max);
}

function cleanEnum(value, allowed, fallback) {
  const v = String(value || '').trim();
  return allowed.has(v) ? v : fallback;
}

function normalizeConsent(value, fallback = 'maintainer_report') {
  const raw = cleanEnum(value, VALID_CONSENT, fallback);
  return raw === 'anonymous_aggregate' ? 'maintainer_report' : raw;
}

function json(value, fallback) {
  try {
    if (value == null || value === '') return fallback;
    return typeof value === 'string' ? JSON.parse(value) : value;
  } catch {
    return fallback;
  }
}

function stringify(value) {
  return JSON.stringify(value == null ? {} : value);
}

function hashText(value) {
  return crypto.createHash('sha256').update(String(value || '')).digest('hex');
}

function ensureFeedbackSchema(db) {
  db.exec(`
    CREATE TABLE IF NOT EXISTS feedback_items (
      id TEXT PRIMARY KEY,
      source TEXT DEFAULT 'ctm',
      surface TEXT DEFAULT '',
      type TEXT NOT NULL DEFAULT 'bug',
      status TEXT NOT NULL DEFAULT 'draft',
      severity TEXT NOT NULL DEFAULT 'medium',
      title TEXT DEFAULT '',
      description_text TEXT DEFAULT '',
      description_html TEXT DEFAULT '',
      ctm_session_id TEXT,
      agent_session_id TEXT,
      project_path TEXT,
      git_branch TEXT,
      model_provider TEXT,
      model_id TEXT,
      consent_level TEXT NOT NULL DEFAULT 'maintainer_report',
      actionable_score REAL DEFAULT 0,
      triage_summary TEXT DEFAULT '',
      triage_result TEXT DEFAULT '{}',
      clarification_questions TEXT DEFAULT '[]',
      evidence_manifest TEXT DEFAULT '{}',
      remote_feedback_id TEXT,
      remote_status TEXT DEFAULT '',
      remote_error TEXT DEFAULT '',
      submitted_at TEXT,
      walle_memory_id TEXT,
      walle_status TEXT DEFAULT '',
      created_at TEXT DEFAULT (datetime('now')),
      updated_at TEXT DEFAULT (datetime('now'))
    );

    CREATE TABLE IF NOT EXISTS feedback_attachments (
      id TEXT PRIMARY KEY,
      feedback_id TEXT NOT NULL,
      image_id INTEGER,
      kind TEXT DEFAULT 'screenshot',
      filename TEXT DEFAULT '',
      mime_type TEXT DEFAULT 'image/png',
      file_path TEXT DEFAULT '',
      redacted INTEGER DEFAULT 0,
      created_at TEXT DEFAULT (datetime('now')),
      FOREIGN KEY (feedback_id) REFERENCES feedback_items(id) ON DELETE CASCADE
    );

    CREATE TABLE IF NOT EXISTS feedback_context_items (
      id TEXT PRIMARY KEY,
      feedback_id TEXT NOT NULL,
      kind TEXT NOT NULL,
      label TEXT NOT NULL,
      source_table TEXT DEFAULT '',
      source_id TEXT DEFAULT '',
      content_hash TEXT NOT NULL,
      content_preview TEXT DEFAULT '',
      include_state TEXT DEFAULT 'suggested',
      sensitivity TEXT DEFAULT 'medium',
      token_estimate INTEGER DEFAULT 0,
      metadata TEXT DEFAULT '{}',
      created_at TEXT DEFAULT (datetime('now')),
      updated_at TEXT DEFAULT (datetime('now')),
      UNIQUE(feedback_id, content_hash),
      FOREIGN KEY (feedback_id) REFERENCES feedback_items(id) ON DELETE CASCADE
    );

    CREATE TABLE IF NOT EXISTS feedback_agent_runs (
      id TEXT PRIMARY KEY,
      feedback_id TEXT NOT NULL,
      status TEXT DEFAULT 'queued',
      task_type TEXT DEFAULT 'triage',
      agent_session_id TEXT DEFAULT '',
      prompt_hash TEXT DEFAULT '',
      result_json TEXT DEFAULT '{}',
      created_at TEXT DEFAULT (datetime('now')),
      completed_at TEXT,
      FOREIGN KEY (feedback_id) REFERENCES feedback_items(id) ON DELETE CASCADE
    );

    CREATE TABLE IF NOT EXISTS feedback_events (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feedback_id TEXT NOT NULL,
      event TEXT NOT NULL,
      details TEXT DEFAULT '{}',
      created_at TEXT DEFAULT (datetime('now')),
      FOREIGN KEY (feedback_id) REFERENCES feedback_items(id) ON DELETE CASCADE
    );

    CREATE INDEX IF NOT EXISTS idx_feedback_items_session ON feedback_items(ctm_session_id);
    CREATE INDEX IF NOT EXISTS idx_feedback_items_status ON feedback_items(status, updated_at);
    CREATE INDEX IF NOT EXISTS idx_feedback_context_feedback ON feedback_context_items(feedback_id);
    CREATE INDEX IF NOT EXISTS idx_feedback_events_feedback ON feedback_events(feedback_id, created_at);
  `);
  try { db.prepare('ALTER TABLE feedback_items ADD COLUMN remote_feedback_id TEXT').run(); } catch {}
  try { db.prepare("ALTER TABLE feedback_items ADD COLUMN remote_status TEXT DEFAULT ''").run(); } catch {}
  try { db.prepare("ALTER TABLE feedback_items ADD COLUMN remote_error TEXT DEFAULT ''").run(); } catch {}
}

function addEvent(db, feedbackId, event, details = {}) {
  db.prepare('INSERT INTO feedback_events (feedback_id, event, details) VALUES (?, ?, ?)')
    .run(feedbackId, event, stringify(details));
}

function tableExists(db, name) {
  return !!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name);
}

function resolveSessionIdentity(db, sessionId, input = {}) {
  const requestedId = cleanText(sessionId || input.ctm_session_id || input.session_id || '', 256);
  const explicitAgentId = cleanText(input.agent_session_id || '', 256);
  const identity = {
    requested_id: requestedId,
    ctm_session_id: requestedId,
    agent_session_id: explicitAgentId,
    agents: [],
  };
  if (!requestedId) return identity;

  try {
    if (!tableExists(db, 'agent_sessions')) return identity;
    const byCtm = db.prepare(`
      SELECT *
      FROM agent_sessions
      WHERE ctm_session_id = ?
      ORDER BY COALESCE(NULLIF(modified_at, ''), updated_at, created_at) DESC,
        updated_at DESC,
        created_at DESC
    `).all(requestedId);
    if (byCtm.length) {
      identity.agents = byCtm;
      identity.agent_session_id ||= cleanText(byCtm[0].agent_session_id || '', 256);
      return identity;
    }

    const byAgent = db.prepare('SELECT * FROM agent_sessions WHERE agent_session_id = ?').get(requestedId);
    if (byAgent?.ctm_session_id) {
      identity.ctm_session_id = cleanText(byAgent.ctm_session_id, 256);
      identity.agent_session_id ||= cleanText(byAgent.agent_session_id || '', 256);
      identity.agents = db.prepare(`
        SELECT *
        FROM agent_sessions
        WHERE ctm_session_id = ?
        ORDER BY COALESCE(NULLIF(modified_at, ''), updated_at, created_at) DESC,
          updated_at DESC,
          created_at DESC
      `).all(byAgent.ctm_session_id);
    }
  } catch {}
  return identity;
}

function sessionConversationSourceIds(db, sessionId, input = {}) {
  const identity = resolveSessionIdentity(db, sessionId, input);
  const ids = [];
  const add = (value) => {
    const s = cleanText(value || '', 256);
    if (s && !ids.includes(s)) ids.push(s);
  };
  add(input.agent_session_id);
  for (const agent of identity.agents || []) add(agent.agent_session_id);
  add(identity.ctm_session_id);
  if (ids.length === 0) add(sessionId);
  return ids;
}

function placeholders(count) {
  return Array.from({ length: count }, () => '?').join(', ');
}

function pickSessionFacts(db, sessionId, input = {}) {
  const identity = resolveSessionIdentity(db, sessionId, input);
  const facts = {
    ctm_session_id: cleanText(identity.ctm_session_id || '', 256),
    agent_session_id: cleanText(identity.agent_session_id || '', 256),
    project_path: cleanText(input.project_path || '', 1024),
    git_branch: cleanText(input.git_branch || '', 256),
    model_provider: cleanText(input.model_provider || '', 120),
    model_id: cleanText(input.model_id || '', 256),
  };
  if (!facts.ctm_session_id) return facts;

  try {
    if (tableExists(db, 'ctm_sessions')) {
      const ctm = db.prepare('SELECT * FROM ctm_sessions WHERE id = ?').get(facts.ctm_session_id);
      if (ctm) {
        facts.project_path ||= cleanText(ctm.cwd || ctm.project_path || '', 1024);
      }
    }
  } catch {}

  try {
    if (tableExists(db, 'agent_sessions')) {
      const agent = identity.agents?.[0] || null;
      if (agent) {
        facts.agent_session_id ||= cleanText(agent.agent_session_id || '', 256);
        facts.project_path ||= cleanText(agent.project_path || '', 1024);
        facts.git_branch ||= cleanText(agent.git_branch || '', 256);
        facts.model_id ||= cleanText(agent.model || '', 256);
        facts.model_provider ||= cleanText(agent.provider || '', 120);
      }
    }
  } catch {}

  try {
    if (tableExists(db, 'session_conversations')) {
      const sourceIds = sessionConversationSourceIds(db, facts.ctm_session_id, facts);
      const conv = sourceIds.length
        ? db.prepare(`SELECT * FROM session_conversations WHERE ctm_session_id IN (${placeholders(sourceIds.length)}) ORDER BY imported_at DESC LIMIT 1`).get(...sourceIds)
        : null;
      if (conv) {
        facts.project_path ||= cleanText(conv.project_path || '', 1024);
        facts.git_branch ||= cleanText(conv.git_branch || '', 256);
        facts.model_provider ||= cleanText(conv.model_provider || '', 120);
        facts.model_id ||= cleanText(conv.model_id || '', 256);
      }
    }
  } catch {}

  return facts;
}

function normalizeInput(db, input = {}, existing = null) {
  const sessionId = input.ctm_session_id ?? input.session_id ?? existing?.ctm_session_id ?? '';
  const facts = pickSessionFacts(db, sessionId, { ...existing, ...input });
  return {
    source: cleanText(input.source ?? existing?.source ?? 'ctm', 80) || 'ctm',
    surface: cleanText(input.surface ?? existing?.surface ?? '', 120),
    type: cleanEnum(input.type ?? existing?.type, VALID_TYPES, 'bug'),
    status: cleanEnum(input.status ?? existing?.status, VALID_STATUS, 'draft'),
    severity: cleanEnum(input.severity ?? existing?.severity, VALID_SEVERITIES, 'medium'),
    title: cleanText(input.title ?? existing?.title ?? '', 220),
    description_text: cleanText(input.description_text ?? input.description ?? existing?.description_text ?? '', 20000),
    description_html: cleanHtml(input.description_html ?? existing?.description_html ?? '', 40000),
    consent_level: normalizeConsent(input.consent_level ?? existing?.consent_level, 'maintainer_report'),
    ...facts,
  };
}

function createFeedbackDraft(db, input = {}) {
  ensureFeedbackSchema(db);
  const item = normalizeInput(db, input);
  const feedbackId = id('fb');
  db.prepare(`
    INSERT INTO feedback_items (
      id, source, surface, type, status, severity, title, description_text, description_html,
      ctm_session_id, agent_session_id, project_path, git_branch, model_provider, model_id, consent_level,
      updated_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    feedbackId, item.source, item.surface, item.type, item.status, item.severity, item.title,
    item.description_text, item.description_html, item.ctm_session_id || null, item.agent_session_id || null,
    item.project_path || null, item.git_branch || null, item.model_provider || null, item.model_id || null,
    item.consent_level, nowIso()
  );
  addEvent(db, feedbackId, 'feedback.created', { source: item.source, surface: item.surface });
  return getFeedback(db, feedbackId);
}

function updateFeedback(db, feedbackId, input = {}) {
  ensureFeedbackSchema(db);
  const existing = getFeedbackRow(db, feedbackId);
  if (!existing) return null;
  const item = normalizeInput(db, input, existing);
  db.prepare(`
    UPDATE feedback_items SET
      source = ?, surface = ?, type = ?, severity = ?, title = ?, description_text = ?, description_html = ?,
      ctm_session_id = ?, agent_session_id = ?, project_path = ?, git_branch = ?, model_provider = ?, model_id = ?,
      consent_level = ?, updated_at = ?
    WHERE id = ?
  `).run(
    item.source, item.surface, item.type, item.severity, item.title, item.description_text, item.description_html,
    item.ctm_session_id || null, item.agent_session_id || null, item.project_path || null, item.git_branch || null,
    item.model_provider || null, item.model_id || null, item.consent_level, nowIso(), feedbackId
  );
  addEvent(db, feedbackId, 'feedback.updated', {});
  return getFeedback(db, feedbackId);
}

function getFeedbackRow(db, feedbackId) {
  ensureFeedbackSchema(db);
  return db.prepare('SELECT * FROM feedback_items WHERE id = ?').get(feedbackId) || null;
}

function parseItem(row) {
  if (!row) return null;
  return {
    ...row,
    actionable_score: Number(row.actionable_score || 0),
    triage_result: json(row.triage_result, {}),
    clarification_questions: json(row.clarification_questions, []),
    evidence_manifest: json(row.evidence_manifest, {}),
  };
}

function getFeedback(db, feedbackId) {
  ensureFeedbackSchema(db);
  const item = parseItem(getFeedbackRow(db, feedbackId));
  if (!item) return null;
  const attachments = db.prepare('SELECT * FROM feedback_attachments WHERE feedback_id = ? ORDER BY created_at').all(feedbackId);
  const context_items = db.prepare('SELECT * FROM feedback_context_items WHERE feedback_id = ? ORDER BY created_at').all(feedbackId)
    .map((row) => ({ ...row, metadata: json(row.metadata, {}) }));
  const events = db.prepare('SELECT event, details, created_at FROM feedback_events WHERE feedback_id = ? ORDER BY created_at').all(feedbackId)
    .map((row) => ({ ...row, details: json(row.details, {}) }));
  return { ...item, attachments, context_items, events };
}

function addAttachment(db, feedbackId, input = {}) {
  ensureFeedbackSchema(db);
  const item = getFeedbackRow(db, feedbackId);
  if (!item) return null;
  const imageId = Number(input.image_id || input.imageId || 0) || null;
  let image = null;
  if (imageId && tableExists(db, 'images')) {
    image = db.prepare('SELECT * FROM images WHERE id = ?').get(imageId) || null;
  }
  const attachmentId = id('fba');
  db.prepare(`
    INSERT INTO feedback_attachments (id, feedback_id, image_id, kind, filename, mime_type, file_path, redacted)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    attachmentId,
    feedbackId,
    imageId,
    cleanText(input.kind || 'screenshot', 80),
    cleanText(input.filename || image?.filename || '', 512),
    cleanText(input.mime_type || image?.mime_type || 'image/png', 80),
    cleanText(input.file_path || image?.file_path || '', 2048),
    input.redacted ? 1 : 0
  );
  addEvent(db, feedbackId, 'feedback.attachment_added', { kind: input.kind || 'screenshot', image_id: imageId });
  return getFeedback(db, feedbackId);
}

function removeAttachment(db, feedbackId, attachmentId) {
  ensureFeedbackSchema(db);
  const result = db.prepare('DELETE FROM feedback_attachments WHERE feedback_id = ? AND id = ?').run(feedbackId, attachmentId);
  if (result.changes) addEvent(db, feedbackId, 'feedback.attachment_removed', { attachment_id: attachmentId });
  return result.changes > 0;
}

function summarizeMessages(rows) {
  return rows.map((r) => {
    const role = cleanText(r.role || 'message', 32);
    const content = cleanText(r.content || '', 900).replace(/\s+/g, ' ').trim();
    return `${role}: ${content}`;
  }).filter((line) => line.trim().length > 8).join('\n');
}

function messageContext(db, sessionId) {
  if (!sessionId) return '';
  const sourceIds = sessionConversationSourceIds(db, sessionId);
  try {
    if (tableExists(db, 'session_messages') && sourceIds.length) {
      const rows = db.prepare(`
        SELECT role, content, created_at, message_index FROM session_messages
        WHERE ctm_session_id IN (${placeholders(sourceIds.length)})
        ORDER BY COALESCE(NULLIF(created_at, ''), '0000') DESC, message_index DESC
        LIMIT 24
      `).all(...sourceIds).reverse();
      const text = summarizeMessages(rows);
      if (text) return text;
    }
  } catch {}

  try {
    if (tableExists(db, 'session_conversations') && sourceIds.length) {
      const conv = db.prepare(`
        SELECT messages
        FROM session_conversations
        WHERE ctm_session_id IN (${placeholders(sourceIds.length)})
        ORDER BY imported_at DESC
        LIMIT 1
      `).get(...sourceIds);
      const messages = json(conv?.messages, []);
      if (Array.isArray(messages) && messages.length) {
        const rows = messages.slice(-16).map((m) => ({
          role: m.role || m.type || 'message',
          content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content || m),
        }));
        return summarizeMessages(rows);
      }
    }
  } catch {}
  return '';
}

function scrollbackContext(db, sessionId) {
  if (!sessionId || !tableExists(db, 'scrollback_log')) return '';
  try {
    const rows = db.prepare(`
      SELECT data FROM scrollback_log
      WHERE ctm_session_id = ?
      ORDER BY chunk_seq DESC
      LIMIT 30
    `).all(sessionId).reverse();
    return cleanText(rows.map((r) => r.data || '').join(''), 4000);
  } catch {
    return '';
  }
}

function createContextCandidate(kind, label, sourceTable, sourceId, preview, sensitivity = 'medium', metadata = {}) {
  const content = cleanText(preview || '', 6000);
  if (!content.trim()) return null;
  return {
    id: id('fbc'),
    kind,
    label,
    source_table: sourceTable,
    source_id: sourceId || '',
    content_hash: hashText(`${kind}:${sourceId || ''}:${content}`),
    content_preview: content,
    include_state: sensitivity === 'high' ? 'suggested' : 'included',
    sensitivity,
    token_estimate: Math.ceil(content.length / 4),
    metadata,
  };
}

function buildContextCandidates(db, feedback) {
  const out = [];
  const sessionId = feedback.ctm_session_id || '';
  if (!sessionId) return out;

  const facts = pickSessionFacts(db, sessionId, feedback);
  out.push(createContextCandidate(
    'session_metadata',
    'Session metadata',
    'ctm_sessions',
    sessionId,
    [
      `CTM session: ${sessionId}`,
      facts.agent_session_id ? `Agent session: ${facts.agent_session_id}` : '',
      facts.project_path ? `Project: ${facts.project_path}` : '',
      facts.git_branch ? `Branch: ${facts.git_branch}` : '',
      facts.model_provider || facts.model_id ? `Model: ${[facts.model_provider, facts.model_id].filter(Boolean).join(' / ')}` : '',
    ].filter(Boolean).join('\n'),
    'low',
    facts
  ));

  try {
    if (tableExists(db, 'agent_sessions')) {
      const agents = db.prepare('SELECT * FROM agent_sessions WHERE ctm_session_id = ? ORDER BY modified_at DESC, created_at DESC LIMIT 4').all(sessionId);
      if (agents.length) {
        const preview = agents.map((a) => [
          `Provider: ${a.provider || 'unknown'}`,
          `Agent session: ${a.agent_session_id || ''}`,
          a.model ? `Model: ${a.model}` : '',
          a.git_branch ? `Branch: ${a.git_branch}` : '',
          a.jsonl_path ? `Transcript file: ${path.basename(a.jsonl_path)}` : '',
          Number.isFinite(a.file_size) ? `File size: ${a.file_size}` : '',
        ].filter(Boolean).join('\n')).join('\n\n');
        out.push(createContextCandidate('agent_sessions', 'Linked agent sessions', 'agent_sessions', sessionId, preview, 'medium'));
      }
    }
  } catch {}

  out.push(createContextCandidate('transcript_excerpt', 'Recent transcript excerpt', 'session_messages', sessionId, messageContext(db, sessionId), 'high'));
  out.push(createContextCandidate('terminal_scrollback', 'Terminal scrollback tail', 'scrollback_log', sessionId, scrollbackContext(db, sessionId), 'high'));
  return out.filter(Boolean);
}

function refreshContextItems(db, feedbackId) {
  ensureFeedbackSchema(db);
  const feedback = getFeedbackRow(db, feedbackId);
  if (!feedback) return null;
  const existing = new Map(
    db.prepare('SELECT content_hash, include_state FROM feedback_context_items WHERE feedback_id = ?').all(feedbackId)
      .map((row) => [row.content_hash, row.include_state])
  );
  const insert = db.prepare(`
    INSERT INTO feedback_context_items (
      id, feedback_id, kind, label, source_table, source_id, content_hash, content_preview,
      include_state, sensitivity, token_estimate, metadata, updated_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(feedback_id, content_hash) DO UPDATE SET
      label = excluded.label,
      content_preview = excluded.content_preview,
      sensitivity = excluded.sensitivity,
      token_estimate = excluded.token_estimate,
      metadata = excluded.metadata,
      updated_at = excluded.updated_at
  `);
  const txn = db.transaction((candidates) => {
    for (const c of candidates) {
      const includeState = cleanEnum(existing.get(c.content_hash) || c.include_state, VALID_INCLUDE, c.include_state);
      insert.run(
        c.id, feedbackId, c.kind, c.label, c.source_table, c.source_id, c.content_hash,
        c.content_preview, includeState, c.sensitivity, c.token_estimate, stringify(c.metadata), nowIso()
      );
    }
  });
  const candidates = buildContextCandidates(db, feedback);
  txn(candidates);
  if (candidates.length) addEvent(db, feedbackId, 'feedback.context_refreshed', { count: candidates.length });
  return getFeedback(db, feedbackId);
}

function updateContextItems(db, feedbackId, items = []) {
  ensureFeedbackSchema(db);
  const stmt = db.prepare(`
    UPDATE feedback_context_items
    SET include_state = ?, updated_at = ?
    WHERE feedback_id = ? AND id = ?
  `);
  const txn = db.transaction((rows) => {
    for (const row of rows) {
      const include = cleanEnum(row.include_state || row.includeState, VALID_INCLUDE, 'suggested');
      stmt.run(include, nowIso(), feedbackId, row.id);
    }
  });
  txn(Array.isArray(items) ? items : []);
  addEvent(db, feedbackId, 'feedback.context_selection_updated', { count: Array.isArray(items) ? items.length : 0 });
  return getFeedback(db, feedbackId);
}

function hasExpectedActual(text) {
  const lower = text.toLowerCase();
  return /(expected|should|want|wanted|supposed to|actual|instead|but it|got|observed)/.test(lower);
}

function hasReproWords(text) {
  return /(when i|after i|click|clicked|open|opened|run|ran|type|typed|session|tab|screen|steps?|repro)/i.test(text);
}

function triageFeedback(db, feedbackId) {
  ensureFeedbackSchema(db);
  refreshContextItems(db, feedbackId);
  const feedback = getFeedback(db, feedbackId);
  if (!feedback) return null;

  const text = `${feedback.title || ''}\n${feedback.description_text || ''}`.trim();
  const hasSession = Boolean(feedback.ctm_session_id);
  const hasScreenshot = feedback.attachments.length > 0;
  const hasContext = feedback.context_items.length > 0;

  let score = 0;
  if (text.length >= 20) score += 0.2;
  if (text.length >= 80) score += 0.15;
  if (hasExpectedActual(text)) score += 0.2;
  if (hasReproWords(text)) score += 0.15;
  if (hasSession) score += 0.15;
  if (hasScreenshot) score += 0.1;
  if (hasContext) score += 0.05;
  if (feedback.type && feedback.type !== 'other') score += 0.05;
  score = Math.min(1, Math.round(score * 100) / 100);

  const questions = [];
  if (text.length < 20) questions.push('What happened, and what were you trying to do?');
  if (!hasExpectedActual(text)) questions.push('What did you expect to happen, and what happened instead?');
  if (!hasSession && !hasScreenshot) questions.push('Can you attach a screenshot or link this feedback to the relevant session?');
  if (!hasReproWords(text) && feedback.type === 'bug') questions.push('What steps reliably reproduce it?');

  const status = score >= 0.65 ? (hasContext || hasScreenshot ? 'evidence_ready' : 'actionable') : 'needs_clarification';
  const included = feedback.context_items.filter((item) => item.include_state === 'included' || item.include_state === 'suggested');
  const triage = {
    actionable: score >= 0.65,
    score,
    reason: score >= 0.65
      ? 'Enough description/context exists to start investigation.'
      : 'The report needs one or two missing facts before an agent can fix it efficiently.',
    missing: questions,
    evidence: {
      session_linked: hasSession,
      screenshots: feedback.attachments.length,
      context_items: feedback.context_items.length,
      included_context_items: included.length,
      estimated_tokens: included.reduce((sum, item) => sum + Number(item.token_estimate || 0), 0),
    },
  };
  const summary = triage.actionable
    ? `Actionable (${Math.round(score * 100)}%). ${triage.evidence.context_items || triage.evidence.screenshots ? 'Evidence bundle is ready for review.' : 'Enough detail to start.'}`
    : `Needs clarification (${Math.round(score * 100)}%). ${questions[0] || 'A little more detail would help.'}`;

  db.prepare(`
    UPDATE feedback_items
    SET status = ?, actionable_score = ?, triage_summary = ?, triage_result = ?, clarification_questions = ?, updated_at = ?
    WHERE id = ?
  `).run(status, score, summary, stringify(triage), JSON.stringify(questions), nowIso(), feedbackId);
  db.prepare(`
    INSERT INTO feedback_agent_runs (id, feedback_id, status, task_type, result_json, created_at, completed_at)
    VALUES (?, ?, 'completed', 'triage', ?, ?, ?)
  `).run(id('fbar'), feedbackId, stringify(triage), nowIso(), nowIso());
  addEvent(db, feedbackId, status === 'needs_clarification' ? 'feedback.needs_clarification' : 'feedback.actionable', triage);
  return getFeedback(db, feedbackId);
}

function appendClarification(db, feedbackId, message) {
  ensureFeedbackSchema(db);
  const item = getFeedbackRow(db, feedbackId);
  if (!item) return null;
  const addition = cleanText(message, 4000).trim();
  if (!addition) return getFeedback(db, feedbackId);
  const next = [item.description_text || '', `Clarification: ${addition}`].filter(Boolean).join('\n\n');
  db.prepare('UPDATE feedback_items SET description_text = ?, updated_at = ? WHERE id = ?').run(next, nowIso(), feedbackId);
  addEvent(db, feedbackId, 'feedback.clarification_added', { message: addition });
  return triageFeedback(db, feedbackId);
}

function includedContextItems(feedback) {
  return (feedback.context_items || []).filter((item) => item.include_state === 'included' || item.include_state === 'suggested');
}

function attachmentManifest(feedback) {
  return (feedback.attachments || []).map((a) => ({
    id: a.id,
    image_id: a.image_id,
    kind: a.kind,
    filename: a.filename,
    mime_type: a.mime_type,
    redacted: Boolean(a.redacted),
  }));
}

function evidenceManifest(feedback) {
  const included = includedContextItems(feedback);
  return {
    consent_level: feedback.consent_level,
    attachments: attachmentManifest(feedback),
    context_items: included.map((item) => ({
      id: item.id,
      kind: item.kind,
      label: item.label,
      source_table: item.source_table,
      source_id: item.source_id,
      sensitivity: item.sensitivity,
      token_estimate: item.token_estimate,
      content_hash: item.content_hash,
    })),
    estimated_tokens: included.reduce((sum, item) => sum + Number(item.token_estimate || 0), 0),
  };
}

function buildWallePayload(feedback) {
  const included = includedContextItems(feedback);
  const includeDiagnosticPreviews = feedback.consent_level === 'diagnostic_bundle';
  return {
    id: feedback.id,
    source: 'ctm-feedback',
    type: feedback.type,
    status: feedback.status,
    severity: feedback.severity,
    title: feedback.title,
    description_text: feedback.description_text,
    consent_level: feedback.consent_level,
    ctm_session_id: feedback.ctm_session_id,
    agent_session_id: feedback.agent_session_id,
    project_path: feedback.project_path,
    git_branch: feedback.git_branch,
    model_provider: feedback.model_provider,
    model_id: feedback.model_id,
    actionable_score: feedback.actionable_score,
    triage_summary: feedback.triage_summary,
    triage_result: feedback.triage_result,
    evidence_manifest: evidenceManifest(feedback),
    context_previews: includeDiagnosticPreviews ? included.map((item) => ({
      kind: item.kind,
      label: item.label,
      sensitivity: item.sensitivity,
      content_preview: item.content_preview,
    })) : [],
    attachments: attachmentManifest(feedback),
    created_at: feedback.created_at,
    submitted_at: nowIso(),
  };
}

function buildRemotePayload(feedback) {
  const included = includedContextItems(feedback);
  const includeDiagnosticPreviews = feedback.consent_level === 'diagnostic_bundle';
  return {
    id: feedback.id,
    source: 'ctm-feedback',
    type: feedback.type,
    status: feedback.status,
    severity: feedback.severity,
    title: feedback.title,
    description_text: feedback.description_text,
    consent_level: feedback.consent_level,
    ctm_session_id: feedback.ctm_session_id,
    agent_session_id: feedback.agent_session_id,
    project_path: feedback.project_path,
    git_branch: feedback.git_branch,
    model_provider: feedback.model_provider,
    model_id: feedback.model_id,
    actionable_score: feedback.actionable_score,
    triage_summary: feedback.triage_summary,
    triage_result: feedback.triage_result,
    evidence_manifest: evidenceManifest(feedback),
    context_previews: includeDiagnosticPreviews ? included.map((item) => ({
      kind: item.kind,
      label: item.label,
      sensitivity: item.sensitivity,
      content_preview: item.content_preview,
    })) : [],
    attachments: attachmentManifest(feedback),
    created_at: feedback.created_at,
    submitted_at: nowIso(),
  };
}

function submitFeedback(db, feedbackId) {
  ensureFeedbackSchema(db);
  const triaged = triageFeedback(db, feedbackId);
  if (!triaged) return null;
  const manifest = evidenceManifest(triaged);
  db.prepare(`
    UPDATE feedback_items
    SET status = 'submitted', evidence_manifest = ?, submitted_at = ?, updated_at = ?
    WHERE id = ?
  `).run(stringify(manifest), nowIso(), nowIso(), feedbackId);
  addEvent(db, feedbackId, 'feedback.submitted', manifest);
  return getFeedback(db, feedbackId);
}

function markWalleResult(db, feedbackId, result = {}) {
  ensureFeedbackSchema(db);
  db.prepare('UPDATE feedback_items SET walle_memory_id = ?, walle_status = ?, updated_at = ? WHERE id = ?')
    .run(result.memory_id || result.memoryId || null, result.ok ? 'ingested' : cleanText(result.error || 'failed', 300), nowIso(), feedbackId);
  addEvent(db, feedbackId, 'feedback.walle_ingest_result', result);
  return getFeedback(db, feedbackId);
}

function markRemoteResult(db, feedbackId, result = {}) {
  ensureFeedbackSchema(db);
  const ok = Boolean(result.ok);
  const remoteId = result.feedback_id || result.feedbackId || result.remote_feedback_id || null;
  const status = ok ? 'sent' : 'failed';
  const error = ok ? '' : cleanText(result.error || result.message || 'failed', 300);
  db.prepare('UPDATE feedback_items SET remote_feedback_id = ?, remote_status = ?, remote_error = ?, updated_at = ? WHERE id = ?')
    .run(remoteId, status, error, nowIso(), feedbackId);
  addEvent(db, feedbackId, 'feedback.remote_submit_result', result);
  return getFeedback(db, feedbackId);
}

module.exports = {
  ensureFeedbackSchema,
  createFeedbackDraft,
  updateFeedback,
  getFeedback,
  addAttachment,
  removeAttachment,
  refreshContextItems,
  updateContextItems,
  triageFeedback,
  appendClarification,
  submitFeedback,
  buildWallePayload,
  buildRemotePayload,
  markWalleResult,
  markRemoteResult,
  _private: {
    pickSessionFacts,
    evidenceManifest,
    normalizeConsent,
    cleanText,
  },
};
