/**
 * Knowledge graph: SQLite-backed directed graph for code knowledge representation.
 *
 * Drop-in replacement for the original Map-based implementation.
 * Uses better-sqlite3 for disk-backed storage — memory stays flat regardless of graph size.
 */

import Database, { type Database as DatabaseType, type Statement } from "better-sqlite3";
import type { EdgeType, GraphEdge, GraphNode, NodeId, NodeType } from "../types";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Stmt = Statement<any[], any>;

export interface SerializableEdge {
  source: NodeId;
  target: NodeId;
  type: string;
  attributes: Record<string, unknown>;
}

export class KnowledgeGraph {
  private _db: DatabaseType;

  constructor(dbPath?: string) {
    this._db = new Database(dbPath ?? ":memory:");
    this._db.pragma("journal_mode = WAL");
    this._db.pragma("synchronous = NORMAL");
    this._db.pragma("cache_size = -64000"); // 64 MB max page cache
    this._db.pragma("mmap_size = 268435456"); // 256 MB mmap — let OS manage pages
    this._initSchema();
  }

  private _initSchema(): void {
    this._db.exec(`
      CREATE TABLE IF NOT EXISTS nodes (
        id        TEXT PRIMARY KEY,
        type      TEXT NOT NULL,
        name      TEXT NOT NULL,
        file_path TEXT,
        line_start INTEGER,
        line_end   INTEGER,
        attrs     TEXT NOT NULL DEFAULT '{}'
      );
      CREATE TABLE IF NOT EXISTS edges (
        rowid   INTEGER PRIMARY KEY AUTOINCREMENT,
        source  TEXT NOT NULL,
        target  TEXT NOT NULL,
        type    TEXT NOT NULL,
        attrs   TEXT NOT NULL DEFAULT '{}'
      );
      CREATE TABLE IF NOT EXISTS metadata (
        key   TEXT PRIMARY KEY,
        value TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_nodes_type   ON nodes(type);
      CREATE INDEX IF NOT EXISTS idx_nodes_name   ON nodes(name);
      CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source, type);
      CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target, type);
    `);

    this._prepareStatements();
  }

  // Prepared statements for hot paths
  private _stmts!: {
    insertNode: Stmt;
    insertEdge: Stmt;
    hasNode: Stmt;
    getNodeAttrs: Stmt;
    getNodesByType: Stmt;
    getOutNeighbors: Stmt;
    getOutNeighborsAll: Stmt;
    getInNeighbors: Stmt;
    getInNeighborsAll: Stmt;
    nodeCount: Stmt;
    edgeCount: Stmt;
    searchNodes: Stmt;
    searchNodesTyped: Stmt;
    allNodes: Stmt;
    allEdges: Stmt;
    outEdgesForNode: Stmt;
    outEdgesForNodeTyped: Stmt;
    inEdgesForNode: Stmt;
    inEdgesForNodeTyped: Stmt;
    allEdgesTyped: Stmt;
  };

  private _prepareStatements(): void {
    this._stmts = {
      insertNode: this._db.prepare(
        `INSERT OR REPLACE INTO nodes (id, type, name, file_path, line_start, line_end, attrs)
         VALUES (@id, @type, @name, @file_path, @line_start, @line_end, @attrs)`,
      ),
      insertEdge: this._db.prepare(
        `INSERT INTO edges (source, target, type, attrs) VALUES (@source, @target, @type, @attrs)`,
      ),
      hasNode: this._db.prepare(`SELECT 1 FROM nodes WHERE id = ?`),
      getNodeAttrs: this._db.prepare(`SELECT * FROM nodes WHERE id = ?`),
      getNodesByType: this._db.prepare(`SELECT id FROM nodes WHERE type = ?`),
      getOutNeighbors: this._db.prepare(`SELECT target FROM edges WHERE source = ? AND type = ?`),
      getOutNeighborsAll: this._db.prepare(`SELECT target FROM edges WHERE source = ?`),
      getInNeighbors: this._db.prepare(`SELECT source FROM edges WHERE target = ? AND type = ?`),
      getInNeighborsAll: this._db.prepare(`SELECT source FROM edges WHERE target = ?`),
      nodeCount: this._db.prepare(`SELECT COUNT(*) as cnt FROM nodes`),
      edgeCount: this._db.prepare(`SELECT COUNT(*) as cnt FROM edges`),
      searchNodes: this._db.prepare(
        `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%' COLLATE NOCASE`,
      ),
      searchNodesTyped: this._db.prepare(
        `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%' COLLATE NOCASE AND type IN (SELECT value FROM json_each(?))`,
      ),
      allNodes: this._db.prepare(`SELECT * FROM nodes`),
      allEdges: this._db.prepare(`SELECT source, target, type, attrs FROM edges`),
      outEdgesForNode: this._db.prepare(`SELECT source, target, type, attrs FROM edges WHERE source = ?`),
      outEdgesForNodeTyped: this._db.prepare(`SELECT source, target, type, attrs FROM edges WHERE source = ? AND type = ?`),
      inEdgesForNode: this._db.prepare(`SELECT source, target, type, attrs FROM edges WHERE target = ?`),
      inEdgesForNodeTyped: this._db.prepare(`SELECT source, target, type, attrs FROM edges WHERE target = ? AND type = ?`),
      allEdgesTyped: this._db.prepare(`SELECT source, target, type, attrs FROM edges WHERE type = ?`),
    };
  }

  get db(): DatabaseType {
    return this._db;
  }

  get nodeCount(): number {
    return (this._stmts.nodeCount.get() as { cnt: number }).cnt;
  }

  get edgeCount(): number {
    return (this._stmts.edgeCount.get() as { cnt: number }).cnt;
  }

  addNode(node: GraphNode): void {
    const { location, attributes, ...rest } = node;
    const extraAttrs: Record<string, unknown> = { ...attributes };

    this._stmts.insertNode.run({
      id: rest.id,
      type: rest.type,
      name: rest.name,
      file_path: location?.filePath ?? null,
      line_start: location?.lineStart ?? null,
      line_end: location?.lineEnd ?? null,
      attrs: JSON.stringify(extraAttrs),
    });
  }

  addEdge(edge: GraphEdge): void {
    this._stmts.insertEdge.run({
      source: edge.source,
      target: edge.target,
      type: edge.type,
      attrs: JSON.stringify(edge.attributes),
    });
  }

  hasNode(nodeId: NodeId): boolean {
    return this._stmts.hasNode.get(nodeId) !== undefined;
  }

  getNodeAttrs(nodeId: NodeId): Record<string, unknown> {
    const row = this._stmts.getNodeAttrs.get(nodeId) as {
      id: string; type: string; name: string;
      file_path: string | null; line_start: number | null; line_end: number | null;
      attrs: string;
    } | undefined;
    if (!row) throw new Error(`Node not found: ${nodeId}`);

    const attrs: Record<string, unknown> = {
      type: row.type,
      name: row.name,
      ...JSON.parse(row.attrs),
    };
    if (row.file_path != null) attrs.file_path = row.file_path;
    if (row.line_start != null) attrs.line_start = row.line_start;
    if (row.line_end != null) attrs.line_end = row.line_end;
    return attrs;
  }

  getNodesByType(nodeType: NodeType): NodeId[] {
    const rows = this._stmts.getNodesByType.all(nodeType) as { id: string }[];
    return rows.map((r) => r.id);
  }

  getNeighbors(
    nodeId: NodeId,
    edgeType?: EdgeType,
    direction: "out" | "in" | "both" = "out",
  ): NodeId[] {
    if (direction === "both") {
      const out = this.getNeighbors(nodeId, edgeType, "out");
      const inN = this.getNeighbors(nodeId, edgeType, "in");
      return [...new Set([...out, ...inN])];
    }

    if (direction === "out") {
      const rows = edgeType
        ? (this._stmts.getOutNeighbors.all(nodeId, edgeType) as { target: string }[])
        : (this._stmts.getOutNeighborsAll.all(nodeId) as { target: string }[]);
      return rows.map((r) => r.target);
    }

    // direction === "in"
    const rows = edgeType
      ? (this._stmts.getInNeighbors.all(nodeId, edgeType) as { source: string }[])
      : (this._stmts.getInNeighborsAll.all(nodeId) as { source: string }[]);
    return rows.map((r) => r.source);
  }

  getEdges(
    edgeType?: EdgeType,
    direction: "out" | "in" | "both" = "out",
    nodeId?: NodeId,
  ): SerializableEdge[] {
    const edges: SerializableEdge[] = [];

    const parseRow = (row: { source: string; target: string; type: string; attrs: string }): SerializableEdge => ({
      source: row.source,
      target: row.target,
      type: row.type,
      attributes: JSON.parse(row.attrs),
    });

    type EdgeRow = { source: string; target: string; type: string; attrs: string };

    if (direction === "out" || direction === "both") {
      let rows: EdgeRow[];
      if (nodeId && edgeType) {
        rows = this._stmts.outEdgesForNodeTyped.all(nodeId, edgeType) as EdgeRow[];
      } else if (nodeId) {
        rows = this._stmts.outEdgesForNode.all(nodeId) as EdgeRow[];
      } else if (edgeType) {
        rows = this._stmts.allEdgesTyped.all(edgeType) as EdgeRow[];
      } else {
        rows = this._stmts.allEdges.all() as EdgeRow[];
      }
      for (const row of rows) edges.push(parseRow(row));
    }

    if (direction === "in" || direction === "both") {
      let rows: EdgeRow[];
      if (nodeId && edgeType) {
        rows = this._stmts.inEdgesForNodeTyped.all(nodeId, edgeType) as EdgeRow[];
      } else if (nodeId) {
        rows = this._stmts.inEdgesForNode.all(nodeId) as EdgeRow[];
      } else if (edgeType) {
        rows = this._stmts.allEdgesTyped.all(edgeType) as EdgeRow[];
      } else {
        rows = this._stmts.allEdges.all() as EdgeRow[];
      }
      for (const row of rows) edges.push(parseRow(row));
    }

    return edges;
  }

  searchNodes(query: string, nodeTypes?: NodeType[]): NodeId[] {
    if (nodeTypes && nodeTypes.length > 0) {
      const rows = this._stmts.searchNodesTyped.all(query, JSON.stringify(nodeTypes)) as { id: string }[];
      return rows.map((r) => r.id);
    }
    const rows = this._stmts.searchNodes.all(query) as { id: string }[];
    return rows.map((r) => r.id);
  }

  toSerializable(): { nodes: Record<string, unknown>[]; edges: Record<string, unknown>[] } {
    const nodeRows = this._stmts.allNodes.all() as {
      id: string; type: string; name: string;
      file_path: string | null; line_start: number | null; line_end: number | null;
      attrs: string;
    }[];

    const nodes: Record<string, unknown>[] = nodeRows.map((row) => {
      const attrs: Record<string, unknown> = {
        id: row.id,
        type: row.type,
        name: row.name,
        ...JSON.parse(row.attrs),
      };
      if (row.file_path != null) attrs.file_path = row.file_path;
      if (row.line_start != null) attrs.line_start = row.line_start;
      if (row.line_end != null) attrs.line_end = row.line_end;
      return attrs;
    });

    const edgeRows = this._stmts.allEdges.all() as {
      source: string; target: string; type: string; attrs: string;
    }[];

    const edges: Record<string, unknown>[] = edgeRows.map((row) => ({
      source: row.source,
      target: row.target,
      type: row.type,
      ...JSON.parse(row.attrs),
    }));

    return { nodes, edges };
  }

  static fromSerializable(data: {
    nodes: Record<string, unknown>[];
    edges: Record<string, unknown>[];
  }, dbPath?: string): KnowledgeGraph {
    const graph = new KnowledgeGraph(dbPath);

    const insertNodes = graph._db.transaction((nodes: Record<string, unknown>[]) => {
      for (const nodeData of nodes) {
        const { id, type, name, file_path, line_start, line_end, ...rest } = nodeData;
        graph._stmts.insertNode.run({
          id: id as string,
          type: type as string,
          name: name as string,
          file_path: (file_path as string) ?? null,
          line_start: (line_start as number) ?? null,
          line_end: (line_end as number) ?? null,
          attrs: JSON.stringify(rest),
        });
      }
    });
    insertNodes(data.nodes);

    const insertEdges = graph._db.transaction((edges: Record<string, unknown>[]) => {
      for (const edgeData of edges) {
        const { source, target, type, ...attrs } = edgeData;
        graph._stmts.insertEdge.run({
          source: source as string,
          target: target as string,
          type: type as string,
          attrs: JSON.stringify(attrs),
        });
      }
    });
    insertEdges(data.edges);

    return graph;
  }

  getSubgraph(nodeIds: Set<NodeId>): {
    nodes: Record<string, unknown>[];
    edges: Record<string, unknown>[];
  } {
    const ids = [...nodeIds];
    if (ids.length === 0) return { nodes: [], edges: [] };

    // Use a temp table for efficient IN-clause on large sets
    this._db.exec(`CREATE TEMP TABLE IF NOT EXISTS _subgraph_ids (id TEXT PRIMARY KEY)`);
    this._db.exec(`DELETE FROM _subgraph_ids`);

    const insertId = this._db.prepare(`INSERT OR IGNORE INTO _subgraph_ids (id) VALUES (?)`);
    const insertMany = this._db.transaction((idList: string[]) => {
      for (const id of idList) insertId.run(id);
    });
    insertMany(ids);

    const nodeRows = this._db.prepare(`
      SELECT n.* FROM nodes n INNER JOIN _subgraph_ids s ON n.id = s.id
    `).all() as {
      id: string; type: string; name: string;
      file_path: string | null; line_start: number | null; line_end: number | null;
      attrs: string;
    }[];

    const nodes: Record<string, unknown>[] = nodeRows.map((row) => {
      const obj: Record<string, unknown> = {
        id: row.id,
        type: row.type,
        name: row.name,
        ...JSON.parse(row.attrs),
      };
      if (row.file_path != null) obj.file_path = row.file_path;
      if (row.line_start != null) obj.line_start = row.line_start;
      if (row.line_end != null) obj.line_end = row.line_end;
      return obj;
    });

    const edgeRows = this._db.prepare(`
      SELECT e.source, e.target, e.type, e.attrs
      FROM edges e
      INNER JOIN _subgraph_ids s1 ON e.source = s1.id
      INNER JOIN _subgraph_ids s2 ON e.target = s2.id
    `).all() as { source: string; target: string; type: string; attrs: string }[];

    const edges: Record<string, unknown>[] = edgeRows.map((row) => ({
      source: row.source,
      target: row.target,
      type: row.type,
      ...JSON.parse(row.attrs),
    }));

    return { nodes, edges };
  }

  /** Wrap a batch of operations in a transaction for performance. */
  transaction<T>(fn: () => T): T {
    return this._db.transaction(fn)();
  }

  close(): void {
    this._db.close();
  }
}
