import path from 'node:path';

// ─── Escape Helpers ──────────────────────────────────────────────────

export function escapeXml(s: string | number): string {
  return String(s)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

export function escapeCsv(s: string | number): string {
  const str = String(s);
  if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
    return `"${str.replace(/"/g, '""')}"`;
  }
  return str;
}

export function escapeLabel(label: string): string {
  return label.replace(/"/g, '#quot;');
}

export function mermaidShape(kind: string, label: string): string {
  const escaped = escapeLabel(label);
  switch (kind) {
    case 'function':
    case 'method':
      return `(["${escaped}"])`;
    case 'class':
    case 'interface':
    case 'type':
    case 'struct':
    case 'enum':
    case 'trait':
    case 'record':
      return `{{"${escaped}"}}`;
    case 'module':
      return `[["${escaped}"]]`;
    default:
      return `["${escaped}"]`;
  }
}

export const ROLE_STYLES: Record<string, string> = {
  entry: 'fill:#e8f5e9,stroke:#4caf50',
  core: 'fill:#e3f2fd,stroke:#2196f3',
  utility: 'fill:#f5f5f5,stroke:#9e9e9e',
  dead: 'fill:#ffebee,stroke:#f44336',
  leaf: 'fill:#fffde7,stroke:#fdd835',
};

// ─── DOT Serializer ──────────────────────────────────────────────────

interface FileLevelDOTData {
  dirs: Array<{
    name: string;
    files: Array<{ path: string; basename: string }>;
    cohesion: number | null;
  }>;
  edges: Array<{ source: string; target: string }>;
  totalEdges: number;
  limit?: number;
}

export function renderFileLevelDOT(data: FileLevelDOTData): string {
  const lines = [
    'digraph codegraph {',
    '  rankdir=LR;',
    '  node [shape=box, fontname="monospace", fontsize=10];',
    '  edge [color="#666666"];',
    '',
  ];

  let clusterIdx = 0;
  for (const dir of data.dirs) {
    lines.push(`  subgraph cluster_${clusterIdx++} {`);
    const cohLabel = dir.cohesion !== null ? ` (cohesion: ${dir.cohesion.toFixed(2)})` : '';
    lines.push(`    label="${dir.name}${cohLabel}";`);
    lines.push(`    style=dashed;`);
    lines.push(`    color="#999999";`);
    for (const f of dir.files) {
      lines.push(`    "${f.path}" [label="${f.basename}"];`);
    }
    lines.push(`  }`);
    lines.push('');
  }

  for (const { source, target } of data.edges) {
    lines.push(`  "${source}" -> "${target}";`);
  }
  if (data.limit && data.totalEdges > data.limit) {
    lines.push(`  // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
  }

  lines.push('}');
  return lines.join('\n');
}

interface FunctionLevelDOTData {
  edges: Array<{
    source_name: string;
    source_file: string;
    target_name: string;
    target_file: string;
  }>;
  totalEdges: number;
  limit?: number;
}

export function renderFunctionLevelDOT(data: FunctionLevelDOTData): string {
  const lines = [
    'digraph codegraph {',
    '  rankdir=LR;',
    '  node [shape=box, fontname="monospace", fontsize=10];',
    '  edge [color="#666666"];',
    '',
  ];

  const emittedNodes = new Set<string>();
  for (const e of data.edges) {
    const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
    const tId = `${e.target_file}:${e.target_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
    if (!emittedNodes.has(sId)) {
      lines.push(`  ${sId} [label="${e.source_name}\\n${path.basename(e.source_file)}"];`);
      emittedNodes.add(sId);
    }
    if (!emittedNodes.has(tId)) {
      lines.push(`  ${tId} [label="${e.target_name}\\n${path.basename(e.target_file)}"];`);
      emittedNodes.add(tId);
    }
    lines.push(`  ${sId} -> ${tId};`);
  }
  if (data.limit && data.totalEdges > data.limit) {
    lines.push(`  // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
  }

  lines.push('}');
  return lines.join('\n');
}

// ─── Mermaid Serializer ──────────────────────────────────────────────

interface FileLevelMermaidData {
  direction: string;
  dirs: Array<{ name: string; files: string[] }>;
  edges: Array<{ source: string; target: string; edge_kind: string }>;
  totalEdges: number;
  limit?: number;
}

export function renderFileLevelMermaid(data: FileLevelMermaidData): string {
  const lines = [`flowchart ${data.direction || 'LR'}`];

  let nodeCounter = 0;
  const nodeIdMap = new Map<string, string>();
  function nodeId(key: string): string {
    if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
    return nodeIdMap.get(key)!;
  }

  // Emit subgraphs
  for (const dir of data.dirs) {
    const sgId = dir.name.replace(/[^a-zA-Z0-9]/g, '_');
    lines.push(`  subgraph ${sgId}["${escapeLabel(dir.name)}"]`);
    for (const f of dir.files) {
      const nId = nodeId(f);
      lines.push(`    ${nId}["${escapeLabel(path.basename(f))}"]`);
    }
    lines.push('  end');
  }

  // Deduplicate edges per source-target pair, collecting all distinct kinds
  const edgeMap = new Map<string, { source: string; target: string; labels: Set<string> }>();
  for (const { source, target, edge_kind } of data.edges) {
    const key = `${source}|${target}`;
    const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
    if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
    edgeMap.get(key)!.labels.add(label);
  }

  for (const { source, target, labels } of edgeMap.values()) {
    lines.push(`  ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
  }
  if (data.limit && data.totalEdges > data.limit) {
    lines.push(`  %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
  }

  return lines.join('\n');
}

interface FunctionLevelMermaidEdge {
  source_file: string;
  source_name: string;
  source_kind: string;
  target_file: string;
  target_name: string;
  target_kind: string;
  edge_kind: string;
}

interface FunctionLevelMermaidData {
  direction: string;
  edges: FunctionLevelMermaidEdge[];
  roles?: Map<string, string>;
  totalEdges: number;
  limit?: number;
}

export function renderFunctionLevelMermaid(data: FunctionLevelMermaidData): string {
  const lines = [`flowchart ${data.direction || 'LR'}`];

  let nodeCounter = 0;
  const nodeIdMap = new Map<string, string>();
  function nodeId(key: string): string {
    if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
    return nodeIdMap.get(key)!;
  }

  // Group nodes by file for subgraphs
  const fileNodes = new Map<string, Map<string, string>>();
  const nodeKinds = new Map<string, string>();
  for (const e of data.edges) {
    const sKey = `${e.source_file}::${e.source_name}`;
    const tKey = `${e.target_file}::${e.target_name}`;
    nodeId(sKey);
    nodeId(tKey);
    nodeKinds.set(sKey, e.source_kind);
    nodeKinds.set(tKey, e.target_kind);

    if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
    fileNodes.get(e.source_file)!.set(sKey, e.source_name);

    if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
    fileNodes.get(e.target_file)!.set(tKey, e.target_name);
  }

  // Emit subgraphs grouped by file
  for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
    const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
    lines.push(`  subgraph ${sgId}["${escapeLabel(file)}"]`);
    for (const [key, name] of nodes) {
      const kind = nodeKinds.get(key);
      lines.push(`    ${nodeId(key)}${mermaidShape(kind!, name)}`);
    }
    lines.push('  end');
  }

  // Emit edges with labels
  for (const e of data.edges) {
    const sId = nodeId(`${e.source_file}::${e.source_name}`);
    const tId = nodeId(`${e.target_file}::${e.target_name}`);
    lines.push(`  ${sId} -->|${e.edge_kind}| ${tId}`);
  }
  if (data.limit && data.totalEdges > data.limit) {
    lines.push(`  %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
  }

  // Role styling
  const roleStyles: string[] = [];
  for (const [key, nid] of nodeIdMap) {
    const role = data.roles?.get(key);
    if (role && ROLE_STYLES[role]) {
      roleStyles.push(`  style ${nid} ${ROLE_STYLES[role]}`);
    }
  }
  lines.push(...roleStyles);

  return lines.join('\n');
}

// ─── GraphML Serializer ──────────────────────────────────────────────

interface FileLevelGraphMLData {
  edges: Array<{ source: string; target: string }>;
}

export function renderFileLevelGraphML(data: FileLevelGraphMLData): string {
  const lines = [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
    '  <key id="d0" for="node" attr.name="name" attr.type="string"/>',
    '  <key id="d1" for="node" attr.name="file" attr.type="string"/>',
    '  <key id="d2" for="edge" attr.name="kind" attr.type="string"/>',
    '  <graph id="codegraph" edgedefault="directed">',
  ];

  const files = new Set<string>();
  for (const { source, target } of data.edges) {
    files.add(source);
    files.add(target);
  }

  const fileIds = new Map<string, string>();
  let nIdx = 0;
  for (const f of files) {
    const id = `n${nIdx++}`;
    fileIds.set(f, id);
    lines.push(`    <node id="${id}">`);
    lines.push(`      <data key="d0">${escapeXml(path.basename(f))}</data>`);
    lines.push(`      <data key="d1">${escapeXml(f)}</data>`);
    lines.push('    </node>');
  }

  let eIdx = 0;
  for (const { source, target } of data.edges) {
    lines.push(
      `    <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
    );
    lines.push('      <data key="d2">imports</data>');
    lines.push('    </edge>');
  }

  lines.push('  </graph>');
  lines.push('</graphml>');
  return lines.join('\n');
}

interface FunctionLevelGraphMLEdge {
  source_id: number;
  source_name: string;
  source_kind: string;
  source_file: string;
  source_line: number;
  source_role: string | null;
  target_id: number;
  target_name: string;
  target_kind: string;
  target_file: string;
  target_line: number;
  target_role: string | null;
  edge_kind: string;
  confidence: number;
}

interface FunctionLevelGraphMLData {
  edges: FunctionLevelGraphMLEdge[];
}

export function renderFunctionLevelGraphML(data: FunctionLevelGraphMLData): string {
  const lines = [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
    '  <key id="d0" for="node" attr.name="name" attr.type="string"/>',
    '  <key id="d1" for="node" attr.name="kind" attr.type="string"/>',
    '  <key id="d2" for="node" attr.name="file" attr.type="string"/>',
    '  <key id="d3" for="node" attr.name="line" attr.type="int"/>',
    '  <key id="d4" for="node" attr.name="role" attr.type="string"/>',
    '  <key id="d5" for="edge" attr.name="kind" attr.type="string"/>',
    '  <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>',
    '  <graph id="codegraph" edgedefault="directed">',
  ];

  const emittedNodes = new Set<number>();
  function emitNode(
    id: number,
    name: string,
    kind: string,
    file: string,
    line: number,
    role: string | null,
  ): void {
    if (emittedNodes.has(id)) return;
    emittedNodes.add(id);
    lines.push(`    <node id="n${id}">`);
    lines.push(`      <data key="d0">${escapeXml(name)}</data>`);
    lines.push(`      <data key="d1">${escapeXml(kind)}</data>`);
    lines.push(`      <data key="d2">${escapeXml(file)}</data>`);
    lines.push(`      <data key="d3">${line}</data>`);
    if (role) lines.push(`      <data key="d4">${escapeXml(role)}</data>`);
    lines.push('    </node>');
  }

  let eIdx = 0;
  for (const e of data.edges) {
    emitNode(
      e.source_id,
      e.source_name,
      e.source_kind,
      e.source_file,
      e.source_line,
      e.source_role,
    );
    emitNode(
      e.target_id,
      e.target_name,
      e.target_kind,
      e.target_file,
      e.target_line,
      e.target_role,
    );
    lines.push(`    <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
    lines.push(`      <data key="d5">${escapeXml(e.edge_kind)}</data>`);
    lines.push(`      <data key="d6">${e.confidence}</data>`);
    lines.push('    </edge>');
  }

  lines.push('  </graph>');
  lines.push('</graphml>');
  return lines.join('\n');
}

// ─── Neo4j CSV Serializer ────────────────────────────────────────────

interface FileLevelNeo4jEdge {
  source: string;
  target: string;
  edge_kind: string;
  confidence: number;
}

interface FileLevelNeo4jData {
  edges: FileLevelNeo4jEdge[];
}

export function renderFileLevelNeo4jCSV(data: FileLevelNeo4jData): {
  nodes: string;
  relationships: string;
} {
  const files = new Map<string, number>();
  let idx = 0;
  for (const { source, target } of data.edges) {
    if (!files.has(source)) files.set(source, idx++);
    if (!files.has(target)) files.set(target, idx++);
  }

  const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
  for (const [file, id] of files) {
    nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
  }

  const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
  for (const e of data.edges) {
    const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
    relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
  }

  return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
}

interface FunctionLevelNeo4jEdge {
  source_id: number;
  source_name: string;
  source_kind: string;
  source_file: string;
  source_line: number;
  source_role: string | null;
  target_id: number;
  target_name: string;
  target_kind: string;
  target_file: string;
  target_line: number;
  target_role: string | null;
  edge_kind: string;
  confidence: number;
}

interface FunctionLevelNeo4jData {
  edges: FunctionLevelNeo4jEdge[];
}

export function renderFunctionLevelNeo4jCSV(data: FunctionLevelNeo4jData): {
  nodes: string;
  relationships: string;
} {
  const emitted = new Set<number>();
  const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
  function emitNode(
    id: number,
    name: string,
    kind: string,
    file: string,
    line: number,
    role: string | null,
  ): void {
    if (emitted.has(id)) return;
    emitted.add(id);
    const label = kind.charAt(0).toUpperCase() + kind.slice(1);
    nodeLines.push(
      `${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
    );
  }

  const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
  for (const e of data.edges) {
    emitNode(
      e.source_id,
      e.source_name,
      e.source_kind,
      e.source_file,
      e.source_line,
      e.source_role,
    );
    emitNode(
      e.target_id,
      e.target_name,
      e.target_kind,
      e.target_file,
      e.target_line,
      e.target_role,
    );
    const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
    relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
  }

  return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
}
