import path from 'node:path';
import {
  findCallees,
  findCallers,
  findCrossFileCallTargets,
  findDbPath,
  findFileNodes,
  findImplementors,
  findImportSources,
  findImportTargets,
  findInterfaces,
  findIntraFileCallEdges,
  findNodeChildren,
  findNodesByFile,
  getComplexityForNode,
  getLineCountForNode,
  getMaxEndLineForFile,
} from '../../db/index.js';
import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import {
  createFileLinesReader,
  extractSignature,
  extractSummary,
  isFileLikeTarget,
  readSourceRange,
} from '../../shared/file-utils.js';
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
import { paginateResult } from '../../shared/paginate.js';
import type {
  BetterSqlite3Database,
  ChildNodeRow,
  ImportEdgeRow,
  IntraFileCallEdge,
  NodeRow,
  RelatedNodeRow,
  StmtCache,
} from '../../types.js';
import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
import { findMatchingNodes } from './symbol-lookup.js';

interface DisplayOpts {
  maxLines?: number;
  excerptLines?: number;
  jsdocEndScanLines?: number;
  jsdocOpenScanLines?: number;
  summaryMaxChars?: number;
  signatureGatherLines?: number;
  [key: string]: unknown;
}

/** Format a callee row into the output shape with summary and source. */
function formatCalleeRow(
  c: RelatedNodeRow,
  repoRoot: string,
  getFileLines: (file: string) => string[] | null,
  displayOpts: DisplayOpts,
  includeSource: boolean,
) {
  const cLines = getFileLines(c.file);
  return {
    name: c.name,
    kind: c.kind,
    file: c.file,
    line: c.line,
    endLine: c.end_line || null,
    summary: cLines ? extractSummary(cLines, c.line, displayOpts) : null,
    source: includeSource
      ? readSourceRange(repoRoot, c.file, c.line, c.end_line ?? undefined, displayOpts)
      : null,
  };
}

/** BFS to collect deeper callees beyond the first level. */
function collectDeeperCallees(
  db: BetterSqlite3Database,
  startIds: number[],
  rootId: number,
  repoRoot: string,
  getFileLines: (file: string) => string[] | null,
  opts: { noTests: boolean; maxDepth: number; displayOpts: DisplayOpts },
) {
  const { noTests, maxDepth, displayOpts } = opts;
  const visited = new Set(startIds);
  visited.add(rootId);
  let frontier = [...startIds];
  const result: ReturnType<typeof formatCalleeRow>[] = [];

  for (let d = 2; d <= maxDepth; d++) {
    const nextFrontier: number[] = [];
    for (const fid of frontier) {
      const deeper = findCallees(db, fid) as RelatedNodeRow[];
      for (const c of deeper) {
        if (visited.has(c.id) || (noTests && isTestFile(c.file))) continue;
        visited.add(c.id);
        nextFrontier.push(c.id);
        result.push(formatCalleeRow(c, repoRoot, getFileLines, displayOpts, true));
      }
    }
    frontier = nextFrontier;
    if (frontier.length === 0) break;
  }
  return result;
}

function buildCallees(
  db: BetterSqlite3Database,
  node: NodeRow,
  repoRoot: string,
  getFileLines: (file: string) => string[] | null,
  opts: { noTests: boolean; depth: number; displayOpts: DisplayOpts },
) {
  const { noTests, depth, displayOpts } = opts;
  const calleeRows = findCallees(db, node.id) as RelatedNodeRow[];
  const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;

  const callees = filteredCallees.map((c) =>
    formatCalleeRow(c, repoRoot, getFileLines, displayOpts, depth >= 1),
  );

  if (depth > 1) {
    const deeper = collectDeeperCallees(
      db,
      filteredCallees.map((c) => c.id),
      node.id,
      repoRoot,
      getFileLines,
      { noTests, maxDepth: Math.min(depth, 5), displayOpts },
    );
    callees.push(...deeper);
  }

  return callees;
}

function fetchCallerRows(db: BetterSqlite3Database, node: NodeRow) {
  const callerRows: Array<RelatedNodeRow & { viaHierarchy?: string }> = findCallers(
    db,
    node.id,
  ) as RelatedNodeRow[];

  if (node.kind === 'method' && node.name.includes('.')) {
    const methodName = node.name.split('.').pop() ?? '';
    const relatedMethods = resolveMethodViaHierarchy(db, methodName);
    for (const rm of relatedMethods) {
      if (rm.id === node.id) continue;
      const extraCallers = findCallers(db, rm.id) as RelatedNodeRow[];
      callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
    }
  }
  return callerRows;
}

function buildCallers(
  callerRows: Array<RelatedNodeRow & { viaHierarchy?: string }>,
  noTests: boolean,
) {
  const filtered = noTests ? callerRows.filter((c) => !isTestFile(c.file)) : callerRows;

  return filtered.map((c) => ({
    name: c.name,
    kind: c.kind,
    file: c.file,
    line: c.line,
    viaHierarchy: c.viaHierarchy || undefined,
  }));
}

const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
const IMPLEMENTOR_KINDS = new Set(['class', 'struct', 'record', 'enum']);

function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTests: boolean) {
  // For interfaces/traits: show who implements them
  if (INTERFACE_LIKE_KINDS.has(node.kind)) {
    let impls = findImplementors(db, node.id) as RelatedNodeRow[];
    if (noTests) impls = impls.filter((n) => !isTestFile(n.file));
    return {
      implementors: impls.map(toSymbolRef),
    };
  }
  // For classes/structs: show what they implement
  if (IMPLEMENTOR_KINDS.has(node.kind)) {
    let ifaces = findInterfaces(db, node.id) as RelatedNodeRow[];
    if (noTests) ifaces = ifaces.filter((n) => !isTestFile(n.file));
    if (ifaces.length > 0) {
      return {
        implements: ifaces.map(toSymbolRef),
      };
    }
  }
  return {};
}

function buildRelatedTests(
  callerRows: RelatedNodeRow[],
  getFileLines: (file: string) => string[] | null,
  includeTests: boolean,
) {
  const testCallers = callerRows.filter((c) => isTestFile(c.file));

  const testsByFile = new Map<string, RelatedNodeRow[]>();
  for (const tc of testCallers) {
    if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
    testsByFile.get(tc.file)!.push(tc);
  }

  const relatedTests: Array<{
    file: string;
    testCount: number;
    testNames: string[];
    source?: string;
  }> = [];
  for (const [file] of testsByFile) {
    const tLines = getFileLines(file);
    const testNames: string[] = [];
    if (tLines) {
      for (const tl of tLines) {
        const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
        if (tm) testNames.push(tm[1]!);
      }
    }
    const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
    relatedTests.push({
      file,
      testCount: testNames.length,
      testNames,
      source: testSource,
    });
  }

  return relatedTests;
}

function getComplexityMetrics(db: BetterSqlite3Database, nodeId: number) {
  try {
    const cRow = getComplexityForNode(db, nodeId);
    if (!cRow) return null;
    return {
      cognitive: cRow.cognitive,
      cyclomatic: cRow.cyclomatic,
      maxNesting: cRow.max_nesting,
      maintainabilityIndex: cRow.maintainability_index || 0,
      halsteadVolume: cRow.halstead_volume || 0,
    };
  } catch (e: unknown) {
    debug(`complexity lookup failed for node ${nodeId}: ${(e as Error).message}`);
    return null;
  }
}

function getNodeChildrenSafe(db: BetterSqlite3Database, nodeId: number) {
  try {
    return (findNodeChildren(db, nodeId) as ChildNodeRow[]).map((c) => ({
      name: c.name,
      kind: c.kind,
      line: c.line,
      endLine: c.end_line || null,
    }));
  } catch (e: unknown) {
    debug(`findNodeChildren failed for node ${nodeId}: ${(e as Error).message}`);
    return [];
  }
}

function buildIntraFileDataFlow(
  db: BetterSqlite3Database,
  file: string,
): Array<{ caller: string; callees: string[] }> {
  const intraEdges = findIntraFileCallEdges(db, file) as IntraFileCallEdge[];
  const dataFlowMap = new Map<string, string[]>();
  for (const edge of intraEdges) {
    if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
    dataFlowMap.get(edge.caller_name)!.push(edge.callee_name);
  }
  return [...dataFlowMap.entries()].map(([caller, callees]) => ({
    caller,
    callees,
  }));
}

function explainFileImpl(
  db: BetterSqlite3Database,
  target: string,
  getFileLines: (file: string) => string[] | null,
  displayOpts: DisplayOpts,
) {
  const fileNodes = findFileNodes(db, `%${target}%`) as NodeRow[];
  if (fileNodes.length === 0) return [];

  return fileNodes.map((fn) => {
    const symbols = findNodesByFile(db, fn.file) as NodeRow[];

    // IDs of symbols that have incoming calls from other files (public)
    const publicIds = findCrossFileCallTargets(db, fn.file) as Set<number>;

    const fileLines = getFileLines(fn.file);
    const mapSymbol = (s: NodeRow) => ({
      name: s.name,
      kind: s.kind,
      line: s.line,
      role: s.role || null,
      summary: fileLines ? extractSummary(fileLines, s.line, displayOpts) : null,
      signature: fileLines ? extractSignature(fileLines, s.line, displayOpts) : null,
    });

    const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
    const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);

    const imports = (findImportTargets(db, fn.id) as ImportEdgeRow[]).map((r) => ({
      file: r.file,
    }));
    const importedBy = (findImportSources(db, fn.id) as ImportEdgeRow[]).map((r) => ({
      file: r.file,
    }));

    const dataFlow = buildIntraFileDataFlow(db, fn.file);

    const metric = getLineCountForNode(db, fn.id) as { line_count: number } | undefined;
    let lineCount: number | null = metric?.line_count || null;
    if (!lineCount) {
      const maxLine = getMaxEndLineForFile(db, fn.file) as { max_end: number | null } | undefined;
      lineCount = maxLine?.max_end || null;
    }

    return {
      file: fn.file,
      lineCount,
      symbolCount: symbols.length,
      publicApi,
      internal,
      imports,
      importedBy,
      dataFlow,
    };
  });
}

const _explainNodeStmtCache: StmtCache<NodeRow> = new WeakMap();
const _EXPLAIN_NODE_SQL = `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`;

function explainFunctionImpl(
  db: BetterSqlite3Database,
  target: string,
  noTests: boolean,
  getFileLines: (file: string) => string[] | null,
  displayOpts: DisplayOpts,
) {
  const stmt = cachedStmt(_explainNodeStmtCache, db, _EXPLAIN_NODE_SQL);
  let nodes = stmt.all(`%${target}%`) as NodeRow[];
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
  if (nodes.length === 0) return [];

  const hc = new Map();
  return nodes.slice(0, 10).map((node) => {
    const fileLines = getFileLines(node.file);
    const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
    const summary = fileLines ? extractSummary(fileLines, node.line, displayOpts) : null;
    const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;

    const callees = (findCallees(db, node.id) as RelatedNodeRow[]).map(toSymbolRef);

    const allCallerRows = findCallers(db, node.id) as RelatedNodeRow[];

    let callers = allCallerRows.map(toSymbolRef);
    if (noTests) callers = callers.filter((c) => !isTestFile(c.file));

    const seenFiles = new Set<string>();
    const relatedTests = allCallerRows
      .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
      .map((r) => ({ file: r.file }));

    return {
      ...normalizeSymbol(node, db, hc),
      lineCount,
      summary,
      signature,
      complexity: getComplexityMetrics(db, node.id),
      callees,
      callers,
      relatedTests,
    };
  });
}

function explainCallees(
  parentResults: any[],
  currentDepth: number,
  visited: Set<string>,
  db: BetterSqlite3Database,
  noTests: boolean,
  getFileLines: (file: string) => string[] | null,
  displayOpts: DisplayOpts,
): void {
  if (currentDepth <= 0) return;
  for (const r of parentResults) {
    const newCallees: typeof parentResults = [];
    for (const callee of r.callees) {
      const key = `${callee.name}:${callee.file}:${callee.line}`;
      if (visited.has(key)) continue;
      visited.add(key);
      const calleeResults = explainFunctionImpl(
        db,
        callee.name,
        noTests,
        getFileLines,
        displayOpts,
      );
      const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
      if (exact) {
        (exact as Record<string, unknown>)._depth =
          (((r as Record<string, unknown>)._depth as number) || 0) + 1;
        newCallees.push(exact);
      }
    }
    if (newCallees.length > 0) {
      r.depDetails = newCallees;
      explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines, displayOpts);
    }
  }
}

// --- Exported functions ---

export function contextData(
  name: string,
  customDbPath: string,
  opts: {
    depth?: number;
    noSource?: boolean;
    noTests?: boolean;
    includeTests?: boolean;
    file?: string;
    kind?: string;
    limit?: number;
    offset?: number;
    config?: any;
  } = {},
) {
  return withReadonlyDb(customDbPath, (db) => {
    const depth = opts.depth || 0;
    const noSource = opts.noSource || false;
    const includeTests = opts.includeTests || false;

    const { noTests, displayOpts } = resolveAnalysisOpts(opts);

    const dbPath = findDbPath(customDbPath);
    const repoRoot = path.resolve(path.dirname(dbPath), '..');

    const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
    if (nodes.length === 0) {
      return { name, results: [] };
    }

    const getFileLines = createFileLinesReader(repoRoot);

    const results = nodes.map((node) => {
      const fileLines = getFileLines(node.file);

      const source = noSource
        ? null
        : readSourceRange(repoRoot, node.file, node.line, node.end_line ?? undefined, displayOpts);

      const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;

      const callees = buildCallees(db, node, repoRoot, getFileLines, {
        noTests,
        depth,
        displayOpts,
      });
      const allCallerRows = fetchCallerRows(db, node);
      const callers = buildCallers(allCallerRows, noTests);
      const relatedTests = buildRelatedTests(allCallerRows, getFileLines, includeTests);
      const complexityMetrics = getComplexityMetrics(db, node.id);
      const nodeChildren = getNodeChildrenSafe(db, node.id);
      const implInfo = buildImplementationInfo(db, node, noTests);

      return {
        name: node.name,
        kind: node.kind,
        file: node.file,
        line: node.line,
        role: node.role || null,
        endLine: node.end_line || null,
        source,
        signature,
        complexity: complexityMetrics,
        children: nodeChildren.length > 0 ? nodeChildren : undefined,
        callees,
        callers,
        relatedTests,
        ...implInfo,
      };
    });

    const base = { name, results };
    return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
  });
}

export function explainData(
  target: string,
  customDbPath?: string,
  opts: {
    noTests?: boolean;
    depth?: number;
    limit?: number;
    offset?: number;
    config?: any;
  } = {},
) {
  return withReadonlyDb(customDbPath, (db) => {
    const depth = opts.depth || 0;
    const kind = isFileLikeTarget(target) ? 'file' : 'function';

    const { noTests, displayOpts } = resolveAnalysisOpts(opts);

    const dbPath = findDbPath(customDbPath);
    const repoRoot = path.resolve(path.dirname(dbPath), '..');

    const getFileLines = createFileLinesReader(repoRoot);

    const results =
      kind === 'file'
        ? explainFileImpl(db, target, getFileLines, displayOpts)
        : explainFunctionImpl(db, target, noTests, getFileLines, displayOpts);

    if (kind === 'function' && depth > 0 && results.length > 0) {
      const visited = new Set(results.map((r: any) => `${r.name}:${r.file}:${r.line ?? ''}`));
      explainCallees(results, depth, visited, db, noTests, getFileLines, displayOpts);
    }

    const base = { target, kind, results };
    return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
  });
}
