'use strict';

const VALID_TYPES = new Set(['session', 'prompt']);

function normalizeResourceType(type) {
  const clean = String(type || '').trim().toLowerCase();
  if (!VALID_TYPES.has(clean)) {
    const err = new Error('Unsupported resource type');
    err.code = 'EINVAL';
    throw err;
  }
  return clean;
}

function normalizeLimit(value, fallback = 30, max = 100) {
  const n = Number(value);
  if (!Number.isFinite(n) || n <= 0) return fallback;
  return Math.min(max, Math.max(1, Math.floor(n)));
}

function safeJsonParse(value, fallback) {
  if (value === null || value === undefined || value === '') return fallback;
  if (Array.isArray(value) || typeof value === 'object') return value;
  try { return JSON.parse(value); } catch { return fallback; }
}

function compactText(value, max = 160) {
  const text = String(value || '').replace(/\s+/g, ' ').trim();
  if (text.length <= max) return text;
  return `${text.slice(0, max - 3)}...`;
}

function shortPath(value) {
  return String(value || '').replace(/^\/Users\/[^/]+\//, '~/');
}

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

function tableColumns(d, name) {
  try {
    return new Set(d.prepare(`PRAGMA table_info(${name})`).all().map(c => c.name));
  } catch {
    return new Set();
  }
}

function placeholders(values) {
  return values.map(() => '?').join(',');
}

function resourceKey(type, id) {
  return `${type}:${String(id)}`;
}

function makeNode(type, id, label, meta = {}) {
  return {
    key: resourceKey(type, id),
    type,
    id: String(id),
    label: compactText(label || `${type}:${id}`, 90),
    meta,
  };
}

function makeEdge(from, to, relation, evidence = {}) {
  return {
    key: `${from.key}->${relation}->${to.key}`,
    from: from.key,
    to: to.key,
    relation,
    label: relation.replace(/_/g, ' '),
    evidence,
  };
}

function pushNode(graph, node) {
  if (!graph._nodeKeys.has(node.key)) {
    graph._nodeKeys.add(node.key);
    graph.nodes.push(node);
  }
  return node;
}

function pushEdge(graph, edge) {
  if (!graph._edgeKeys.has(edge.key)) {
    graph._edgeKeys.add(edge.key);
    graph.edges.push(edge);
  }
  return edge;
}

function pushLink(list, seen, relation, target, evidence = {}, source) {
  const key = `${relation}:${target.key}:${evidence.id || ''}`;
  if (seen.has(key)) return;
  seen.add(key);
  list.push({
    relation,
    label: relation.replace(/_/g, ' '),
    source: source ? { type: source.type, id: source.id, label: source.label } : undefined,
    target: { type: target.type, id: target.id, label: target.label, meta: target.meta || {} },
    evidence,
  });
}

function emptyGraph() {
  return { nodes: [], edges: [], _nodeKeys: new Set(), _edgeKeys: new Set() };
}

function finishGraph(graph) {
  delete graph._nodeKeys;
  delete graph._edgeKeys;
  return graph;
}

function pickSessionLabel(dbModule, row, id) {
  if (!row) return String(id).slice(0, 12);
  if (typeof dbModule.pickDisplayTitle === 'function') {
    try {
      const title = dbModule.pickDisplayTitle(row);
      if (title) return title;
    } catch { /* fall through */ }
  }
  return row.title || row.rename_name || row.first_message || row.last_user_content || String(id).slice(0, 12);
}

function sessionMeta(row = {}) {
  return {
    provider: row.provider || row.model_provider || '',
    model: row.model || row.model_id || '',
    project: shortPath(row.project_path || row.project || row.cwd || ''),
    cwd: shortPath(row.cwd || row.project_path || row.project || ''),
    modifiedAt: row.modified_at || row.updated_at || row.imported_at || '',
    count: row.user_msg_count || row.message_count || 0,
  };
}

function promptMeta(row = {}) {
  const tags = safeJsonParse(row.tags, []);
  const domainTags = safeJsonParse(row.domain_tags, []);
  return {
    contextType: row.context_type || '',
    lifecycle: row.lifecycle_status || '',
    tags: Array.isArray(tags) ? tags : [],
    domains: Array.isArray(domainTags) ? domainTags : [],
    updatedAt: row.updated_at || '',
    usedAt: row.used_at || '',
  };
}

function getSessionRowsByIds(dbModule, ids) {
  const wanted = new Set(ids.filter(Boolean).map(String));
  if (!wanted.size) return [];
  try {
    return dbModule.getAllSessionsData().filter(row => {
      if (wanted.has(String(row.id))) return true;
      if (wanted.has(String(row.agent_session_id || ''))) return true;
      const agentIds = String(row.agent_session_ids || '').split(',').filter(Boolean);
      return agentIds.some(id => wanted.has(id));
    });
  } catch {
    return [];
  }
}

function sessionNodeFromRow(dbModule, row, fallbackId) {
  const id = row?.id || fallbackId;
  return makeNode('session', id, pickSessionLabel(dbModule, row, id), sessionMeta(row));
}

function promptNodeFromRow(row, fallbackId) {
  const id = row?.id || row?.prompt_id || fallbackId;
  return makeNode('prompt', id, row?.title || `Prompt ${id}`, promptMeta(row));
}

function getIdentitySourceIds(dbModule, id) {
  const identity = typeof dbModule.getSessionIdentity === 'function' ? dbModule.getSessionIdentity(id) : null;
  const ctmId = identity?.ctm_session_id || id;
  const agentRows = identity?.agents || (typeof dbModule.getAgentSessions === 'function' ? dbModule.getAgentSessions(ctmId) : []);
  const sourceIds = [ctmId, ...agentRows.map(a => a.agent_session_id).filter(Boolean)]
    .filter((value, index, arr) => value && arr.indexOf(value) === index);
  return { identity, ctmId, agentRows, sourceIds };
}

function getSessionResourceContext(dbModule, id, opts = {}) {
  const cleanId = String(id || '').trim();
  if (!cleanId) {
    const err = new Error('Missing resource id');
    err.code = 'EINVAL';
    throw err;
  }
  const limit = normalizeLimit(opts.limit);
  const d = dbModule.getDb();
  const { identity, ctmId, agentRows, sourceIds } = getIdentitySourceIds(dbModule, cleanId);
  const rows = getSessionRowsByIds(dbModule, [ctmId, cleanId, ...sourceIds]);
  const sessionRow = rows.find(r => r.id === ctmId) || rows[0] || (identity?.ctm ? { ...identity.ctm, agent_sessions: agentRows } : null);
  if (!sessionRow && !identity) {
    const err = new Error('Resource not found');
    err.code = 'ENOENT';
    throw err;
  }

  const graph = emptyGraph();
  const outgoing = [];
  const backlinks = [];
  const outSeen = new Set();
  const backSeen = new Set();
  const root = pushNode(graph, sessionNodeFromRow(dbModule, sessionRow, ctmId));

  for (const agent of agentRows) {
    const node = pushNode(graph, makeNode('agent_session', agent.agent_session_id, agent.agent_session_id, {
      provider: agent.provider || sessionRow?.provider || '',
      model: agent.model || '',
      jsonl: shortPath(agent.jsonl_path || ''),
      modifiedAt: agent.modified_at || agent.updated_at || '',
    }));
    pushEdge(graph, makeEdge(root, node, 'owns_agent_session', { table: 'agent_sessions' }));
    pushLink(outgoing, outSeen, 'owns_agent_session', node, { table: 'agent_sessions', id: agent.agent_session_id }, root);
  }

  const projectValue = sessionRow?.project_path || sessionRow?.project || sessionRow?.cwd || agentRows.find(a => a.project_path)?.project_path || '';
  if (projectValue) {
    const node = pushNode(graph, makeNode('project', projectValue, shortPath(projectValue), { path: shortPath(projectValue) }));
    pushEdge(graph, makeEdge(root, node, 'in_project', { field: 'project_path' }));
    pushLink(outgoing, outSeen, 'in_project', node, { field: 'project_path' }, root);
  }

  const modelValue = sessionRow?.model || sessionRow?.model_id || agentRows.find(a => a.model)?.model || '';
  const providerValue = sessionRow?.model_provider || sessionRow?.provider || agentRows.find(a => a.provider)?.provider || '';
  if (modelValue || providerValue) {
    const modelId = `${providerValue || 'provider'}:${modelValue || 'default'}`;
    const node = pushNode(graph, makeNode('model', modelId, [providerValue, modelValue].filter(Boolean).join(' / '), {
      provider: providerValue,
      model: modelValue,
    }));
    pushEdge(graph, makeEdge(root, node, 'ran_model', { field: 'provider/model' }));
    pushLink(outgoing, outSeen, 'ran_model', node, { field: 'provider/model' }, root);
  }

  if (sourceIds.length && tableExists(d, 'prompt_usage')) {
    const ph = placeholders(sourceIds);
    const rows = d.prepare(`
      SELECT pu.id AS usage_id, pu.prompt_id, pu.session_id, pu.used_at, pu.result,
        p.id, p.title, p.context_type, p.tags, p.domain_tags, p.lifecycle_status, p.updated_at
      FROM prompt_usage pu
      JOIN prompts p ON p.id = pu.prompt_id
      WHERE pu.session_id IN (${ph})
      ORDER BY pu.used_at DESC, pu.id DESC
      LIMIT ?
    `).all(...sourceIds, limit);
    for (const row of rows) {
      const node = pushNode(graph, promptNodeFromRow(row, row.prompt_id));
      const evidence = { table: 'prompt_usage', id: row.usage_id, sessionId: row.session_id, usedAt: row.used_at, result: row.result || '' };
      pushEdge(graph, makeEdge(root, node, 'used_prompt', evidence));
      pushLink(outgoing, outSeen, 'used_prompt', node, evidence, root);
      pushLink(backlinks, backSeen, 'prompt_used_here', node, evidence, root);
    }
  }

  if (sourceIds.length && tableExists(d, 'prompt_executions')) {
    const peCols = tableColumns(d, 'prompt_executions');
    const executedCol = peCols.has('executed_at') ? 'executed_at' : 'created_at';
    const ph = placeholders(sourceIds);
    const rows = d.prepare(`
      SELECT id, session_id, message_text, message_index, role, project_path, cwd, source, outcome, ${executedCol} AS executed_at
      FROM prompt_executions
      WHERE session_id IN (${ph})
      ORDER BY COALESCE(${executedCol}, created_at) DESC, id DESC
      LIMIT ?
    `).all(...sourceIds, Math.min(limit, 12));
    for (const row of rows) {
      const node = pushNode(graph, makeNode('message', row.id, compactText(row.message_text, 72), {
        role: row.role || 'user',
        sessionId: row.session_id,
        index: row.message_index,
        outcome: row.outcome || '',
        executedAt: row.executed_at || '',
      }));
      const evidence = { table: 'prompt_executions', id: row.id, role: row.role || '', index: row.message_index };
      pushEdge(graph, makeEdge(root, node, 'contains_message', evidence));
      pushLink(outgoing, outSeen, 'contains_message', node, evidence, root);
    }
  }

  if (tableExists(d, 'session_analyses')) {
    const analysis = d.prepare('SELECT * FROM session_analyses WHERE ctm_session_id = ?').get(ctmId);
    if (analysis) {
      const topics = safeJsonParse(analysis.topics, []);
      const skills = safeJsonParse(analysis.skills_used, []);
      for (const topic of Array.isArray(topics) ? topics.slice(0, 8) : []) {
        const node = pushNode(graph, makeNode('topic', topic, topic, { source: 'session_analyses' }));
        pushEdge(graph, makeEdge(root, node, 'has_topic', { table: 'session_analyses' }));
        pushLink(outgoing, outSeen, 'has_topic', node, { table: 'session_analyses' }, root);
      }
      for (const skill of Array.isArray(skills) ? skills.slice(0, 8) : []) {
        const node = pushNode(graph, makeNode('skill', skill, skill, { source: 'session_analyses' }));
        pushEdge(graph, makeEdge(root, node, 'used_skill', { table: 'session_analyses' }));
        pushLink(outgoing, outSeen, 'used_skill', node, { table: 'session_analyses' }, root);
      }
    }
  }

  return {
    resource: {
      type: 'session',
      id: ctmId,
      requestedId: cleanId,
      label: root.label,
      meta: { ...root.meta, agentSessionIds: sourceIds.filter(x => x !== ctmId) },
    },
    backlinks,
    outgoing,
    graph: finishGraph(graph),
    stats: { backlinks: backlinks.length, outgoing: outgoing.length, nodes: graph.nodes.length, edges: graph.edges.length },
  };
}

function getPromptResourceContext(dbModule, id, opts = {}) {
  const promptId = Number(id);
  if (!Number.isInteger(promptId) || promptId <= 0) {
    const err = new Error('Prompt id must be a positive integer');
    err.code = 'EINVAL';
    throw err;
  }
  const limit = normalizeLimit(opts.limit);
  const d = dbModule.getDb();
  const prompt = dbModule.getPrompt(promptId);
  if (!prompt) {
    const err = new Error('Resource not found');
    err.code = 'ENOENT';
    throw err;
  }

  const graph = emptyGraph();
  const outgoing = [];
  const backlinks = [];
  const outSeen = new Set();
  const backSeen = new Set();
  const root = pushNode(graph, promptNodeFromRow(prompt, promptId));

  if (prompt.parent_id) {
    const parent = dbModule.getPrompt(prompt.parent_id);
    if (parent) {
      const node = pushNode(graph, promptNodeFromRow(parent, parent.id));
      pushEdge(graph, makeEdge(root, node, 'child_of', { field: 'parent_id' }));
      pushLink(outgoing, outSeen, 'child_of', node, { field: 'parent_id' }, root);
    }
  }

  const children = typeof dbModule.listChildPrompts === 'function' ? dbModule.listChildPrompts(promptId) : [];
  for (const child of children.slice(0, limit)) {
    const node = pushNode(graph, promptNodeFromRow(child, child.id));
    pushEdge(graph, makeEdge(root, node, 'has_child_prompt', { field: 'parent_id' }));
    pushLink(outgoing, outSeen, 'has_child_prompt', node, { field: 'parent_id' }, root);
  }

  const tags = [...promptMeta(prompt).tags, ...promptMeta(prompt).domains].filter(Boolean);
  for (const tag of tags.slice(0, 12)) {
    const node = pushNode(graph, makeNode('tag', tag, tag, { source: 'prompts.tags' }));
    pushEdge(graph, makeEdge(root, node, 'tagged_with', { table: 'prompts' }));
    pushLink(outgoing, outSeen, 'tagged_with', node, { table: 'prompts' }, root);
  }

  if (tableExists(d, 'prompt_usage')) {
    const usageRows = d.prepare(`
      SELECT pu.id AS usage_id, pu.session_id, pu.used_at, pu.result,
        COALESCE(agent.ctm_session_id, pu.session_id) AS ctm_session_id,
        c.provider AS ctm_provider, c.project_path AS ctm_project_path, c.cwd AS ctm_cwd, c.title AS ctm_title,
        c.updated_at AS ctm_updated_at, agent.provider AS agent_provider, agent.model AS agent_model,
        agent.project_path AS agent_project_path, agent.first_message AS agent_first_message,
        agent.modified_at AS agent_modified_at, sc.first_message AS conversation_first_message,
        sc.title AS conversation_title, sc.user_msg_count AS conversation_user_msg_count
      FROM prompt_usage pu
      LEFT JOIN agent_sessions agent ON agent.agent_session_id = pu.session_id
      LEFT JOIN ctm_sessions c ON c.id = COALESCE(agent.ctm_session_id, pu.session_id)
      LEFT JOIN session_conversations sc ON sc.ctm_session_id = pu.session_id
      WHERE pu.prompt_id = ?
      ORDER BY pu.used_at DESC, pu.id DESC
      LIMIT ?
    `).all(promptId, limit);
    for (const row of usageRows) {
      const sessionRow = {
        id: row.ctm_session_id || row.session_id,
        provider: row.ctm_provider || row.agent_provider || '',
        project_path: row.ctm_project_path || row.agent_project_path || '',
        cwd: row.ctm_cwd || '',
        title: row.ctm_title || row.conversation_title || '',
        first_message: row.conversation_first_message || row.agent_first_message || '',
        user_msg_count: row.conversation_user_msg_count || 0,
        modified_at: row.agent_modified_at || row.ctm_updated_at || row.used_at,
        model: row.agent_model || '',
      };
      const node = pushNode(graph, sessionNodeFromRow(dbModule, sessionRow, row.ctm_session_id || row.session_id));
      const evidence = { table: 'prompt_usage', id: row.usage_id, sessionId: row.session_id, usedAt: row.used_at, result: row.result || '' };
      pushEdge(graph, makeEdge(node, root, 'used_prompt', evidence));
      pushLink(backlinks, backSeen, 'used_in_session', node, evidence, root);
    }
  }

  return {
    resource: {
      type: 'prompt',
      id: String(promptId),
      label: root.label,
      meta: root.meta,
    },
    backlinks,
    outgoing,
    graph: finishGraph(graph),
    stats: { backlinks: backlinks.length, outgoing: outgoing.length, nodes: graph.nodes.length, edges: graph.edges.length },
  };
}

function recentSessionViews(dbModule, limit) {
  const rows = dbModule.getAllSessionsData()
    .slice()
    .sort((a, b) => new Date(b.modified_at || b.updated_at || 0) - new Date(a.modified_at || a.updated_at || 0))
    .slice(0, limit);
  return rows.map(row => ({
    type: 'session',
    id: row.id,
    label: pickSessionLabel(dbModule, row, row.id),
    meta: sessionMeta(row),
  }));
}

function sessionFidelityViews(dbModule, limit) {
  return dbModule.getAllSessionsData()
    .filter(row => Number(row.agent_session_count || 0) > 1 || !row.agent_session_id)
    .sort((a, b) => Number(b.agent_session_count || 0) - Number(a.agent_session_count || 0))
    .slice(0, limit)
    .map(row => ({
      type: 'session',
      id: row.id,
      label: pickSessionLabel(dbModule, row, row.id),
      meta: { ...sessionMeta(row), agentSessionCount: row.agent_session_count || 0 },
    }));
}

function promptReuseViews(dbModule, limit) {
  const d = dbModule.getDb();
  if (!tableExists(d, 'prompt_usage')) return [];
  return d.prepare(`
    SELECT p.id, p.title, p.context_type, p.tags, p.domain_tags, p.lifecycle_status,
      COUNT(pu.id) AS use_count, MAX(pu.used_at) AS last_used
    FROM prompts p
    JOIN prompt_usage pu ON pu.prompt_id = p.id
    GROUP BY p.id
    ORDER BY use_count DESC, last_used DESC
    LIMIT ?
  `).all(limit).map(row => ({
    type: 'prompt',
    id: String(row.id),
    label: row.title || `Prompt ${row.id}`,
    meta: { ...promptMeta(row), useCount: row.use_count || 0, lastUsed: row.last_used || '' },
  }));
}

function getResourceViews(dbModule, opts = {}) {
  const limit = normalizeLimit(opts.limit, 20, 50);
  return {
    views: [
      {
        id: 'recent-session-map',
        title: 'Recent Session Map',
        description: 'Newest CTM sessions with provider, project, and agent-session identity.',
        items: recentSessionViews(dbModule, limit),
      },
      {
        id: 'prompt-reuse',
        title: 'Prompt Reuse',
        description: 'Prompts most often linked to CTM sessions through prompt_usage.',
        items: promptReuseViews(dbModule, limit),
      },
      {
        id: 'session-fidelity',
        title: 'Session Fidelity',
        description: 'Sessions with multiple provider-native agent IDs, or missing an agent row.',
        items: sessionFidelityViews(dbModule, limit),
      },
    ],
  };
}

function getResourceContext(dbModule, opts = {}) {
  const type = normalizeResourceType(opts.type);
  if (type === 'session') return getSessionResourceContext(dbModule, opts.id, opts);
  return getPromptResourceContext(dbModule, opts.id, opts);
}

module.exports = {
  getResourceContext,
  getResourceViews,
  normalizeResourceType,
};
