import * as fs from "node:fs";
import * as path from "node:path";
import { globToLike, isGlobPattern, isWildcard as isWildcardTerm } from "./fuzzy-matcher.js";
import { BunStore } from "./store-bun.js";
import { NodeStore } from "./store-node.js";
const CURRENT_SCHEMA_VERSION = 8;
function parseSearchPattern(pattern) {
  const commonExtensions = /* @__PURE__ */ new Set([
    "ts",
    "js",
    "cs",
    "java",
    "py",
    "php",
    "go",
    "rs",
    "swift",
    "kt",
    "svelte",
    "vue",
    "lua",
    "ex",
    "zig",
    "sh",
    "pas",
    "scala",
    "rb",
    "c",
    "cpp",
    "h",
    "hpp",
    "json",
    "yaml",
    "yml",
    "md"
  ]);
  if (pattern.includes("::")) {
    const parts = pattern.split("::");
    const namePart = parts.pop();
    const scopePart = parts.join("::");
    const scopeLike = isWildcardTerm(scopePart) ? "%" : isGlobPattern(scopePart) ? globToLike(scopePart) : `%${scopePart}%`;
    return { scopeLike, namePattern: namePart };
  }
  if (pattern.includes(".")) {
    const parts = pattern.split(".");
    const last = parts[parts.length - 1].toLowerCase();
    if (!commonExtensions.has(last)) {
      const namePart = parts.pop();
      const scopePart = parts.join(".");
      const scopeLike = isWildcardTerm(scopePart) ? "%" : isGlobPattern(scopePart) ? globToLike(scopePart) : `%${scopePart}%`;
      return { scopeLike, namePattern: namePart };
    }
  }
  return { scopeLike: null, namePattern: pattern };
}
const INITIAL_SCHEMA = `
CREATE TABLE IF NOT EXISTS files (
  path TEXT PRIMARY KEY,
  repo TEXT NOT NULL,
  language TEXT NOT NULL,
  git_blob_hash TEXT,
  last_scanned TEXT,
  size_bytes INTEGER DEFAULT 0,
  lines INTEGER DEFAULT 0
);

CREATE TABLE IF NOT EXISTS symbols (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  file_path TEXT NOT NULL,
  repo TEXT NOT NULL,
  name TEXT NOT NULL,
  kind TEXT NOT NULL,
  scope TEXT,
  signature TEXT DEFAULT '',
  start_line INTEGER,
  end_line INTEGER,
  metadata TEXT DEFAULT '{}',
  FOREIGN KEY (file_path) REFERENCES files(path)
);

CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
CREATE INDEX IF NOT EXISTS idx_symbols_scope ON symbols(scope);

CREATE TABLE IF NOT EXISTS edges (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  source_file TEXT NOT NULL,
  target_file TEXT NOT NULL,
  source_symbol TEXT,
  target_symbol TEXT,
  edge_type TEXT NOT NULL,
  repo TEXT NOT NULL,
  weight REAL DEFAULT 1.0
);

CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_file);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_file);
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(edge_type);

CREATE TABLE IF NOT EXISTS snapshots (
  commit_sha TEXT PRIMARY KEY,
  parent_sha TEXT,
  timestamp TEXT,
  files_added TEXT DEFAULT '[]',
  files_modified TEXT DEFAULT '[]',
  files_removed TEXT DEFAULT '[]',
  symbols_delta TEXT DEFAULT '{}'
);

CREATE TABLE IF NOT EXISTS meta (
  key TEXT PRIMARY KEY,
  value TEXT
);
`;
const MIGRATIONS = [
  {
    version: 2,
    description: "Add content_hash column to files table",
    up: [
      `ALTER TABLE files ADD COLUMN content_hash TEXT`
    ]
  },
  {
    version: 3,
    description: "Add verifiability column to edges table",
    up: [
      `ALTER TABLE edges ADD COLUMN verifiability TEXT NOT NULL DEFAULT 'verified'`,
      `CREATE INDEX IF NOT EXISTS idx_edges_verifiability ON edges (verifiability)`
    ]
  },
  {
    version: 4,
    description: "Add metadata column to edges and files tables",
    up: [
      `ALTER TABLE edges ADD COLUMN metadata TEXT DEFAULT '{}'`,
      `ALTER TABLE files ADD COLUMN metadata TEXT DEFAULT '{}'`
    ]
  },
  {
    version: 5,
    description: "Add clusters and cluster_membership tables, namespace column in files",
    up: [
      `ALTER TABLE files ADD COLUMN namespace TEXT`,
      `CREATE TABLE IF NOT EXISTS clusters (
        id   INTEGER PRIMARY KEY AUTOINCREMENT,
        repo TEXT NOT NULL,
        name TEXT NOT NULL,
        label TEXT NOT NULL,
        source TEXT NOT NULL,
        parent_name TEXT,
        depth INTEGER DEFAULT 0,
        file_count INTEGER DEFAULT 0,
        UNIQUE(repo, name)
      )`,
      `CREATE INDEX IF NOT EXISTS idx_clusters_repo ON clusters(repo)`,
      `CREATE INDEX IF NOT EXISTS idx_clusters_parent ON clusters(parent_name)`,
      `CREATE TABLE IF NOT EXISTS cluster_membership (
        file_path    TEXT NOT NULL,
        cluster_name TEXT NOT NULL,
        repo         TEXT NOT NULL,
        is_primary   INTEGER DEFAULT 1,
        PRIMARY KEY (file_path, cluster_name, repo)
      )`,
      `CREATE INDEX IF NOT EXISTS idx_membership_file ON cluster_membership(file_path)`,
      `CREATE INDEX IF NOT EXISTS idx_membership_cluster ON cluster_membership(cluster_name)`
    ]
  },
  {
    version: 6,
    description: "Add target_repo column to edges table",
    up: [
      `ALTER TABLE edges ADD COLUMN target_repo TEXT`,
      `CREATE INDEX IF NOT EXISTS idx_edges_target_repo ON edges(target_repo)`
    ]
  },
  {
    version: 7,
    description: "Add layer column to clusters table for architectural layer assignments",
    up: [
      `ALTER TABLE clusters ADD COLUMN layer TEXT DEFAULT 'other'`
    ]
  },
  {
    version: 8,
    description: "Add codebase classification role columns, classification_signals, cluster_metrics, and arch_smells tables",
    up: [
      `ALTER TABLE files ADD COLUMN role TEXT DEFAULT 'other'`,
      `ALTER TABLE files ADD COLUMN role_confidence REAL DEFAULT 0.0`,
      `CREATE TABLE IF NOT EXISTS classification_signals (
        file_path TEXT NOT NULL,
        repo TEXT NOT NULL,
        source TEXT NOT NULL,
        role TEXT NOT NULL,
        confidence REAL NOT NULL,
        reason TEXT NOT NULL,
        PRIMARY KEY (file_path, repo, source)
      )`,
      `CREATE TABLE IF NOT EXISTS cluster_metrics (
        cluster_name TEXT NOT NULL,
        repo TEXT NOT NULL,
        file_count INTEGER NOT NULL,
        afferent_coupling REAL NOT NULL,
        efferent_coupling REAL NOT NULL,
        instability REAL NOT NULL,
        internal_edges INTEGER NOT NULL,
        external_edges INTEGER NOT NULL,
        cohesion_ratio REAL NOT NULL,
        abstractness REAL NOT NULL,
        distance_from_main_seq REAL NOT NULL,
        PRIMARY KEY (cluster_name, repo)
      )`,
      `CREATE TABLE IF NOT EXISTS arch_smells (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        repo TEXT NOT NULL,
        type TEXT NOT NULL,
        severity TEXT NOT NULL,
        description TEXT NOT NULL,
        involved_files TEXT NOT NULL,
        involved_clusters TEXT NOT NULL,
        suggestion TEXT NOT NULL
      )`,
      `CREATE INDEX IF NOT EXISTS idx_arch_smells_repo ON arch_smells(repo)`
    ]
  }
];
function createStoreBackend(dbPath) {
  const isBun = typeof globalThis.Bun !== "undefined";
  if (isBun) {
    return new BunStore(dbPath);
  }
  return new NodeStore(dbPath);
}
class Store {
  backend;
  constructor(dbPath) {
    if (dbPath !== ":memory:") {
      const parentDir = path.dirname(dbPath);
      if (!fs.existsSync(parentDir)) {
        fs.mkdirSync(parentDir, { recursive: true });
      }
    }
    this.backend = createStoreBackend(dbPath);
    this.backend.exec(INITIAL_SCHEMA);
    this.runMigrations();
  }
  runMigrations() {
    const currentVersion = this.getSchemaVersion();
    if (currentVersion >= CURRENT_SCHEMA_VERSION) return;
    const pending = MIGRATIONS.filter((m) => m.version > currentVersion);
    if (pending.length === 0) {
      this.setMeta("schema_version", String(CURRENT_SCHEMA_VERSION));
      return;
    }
    this.backend.inTransaction(() => {
      for (const migration of pending) {
        for (const sql of migration.up) {
          this.backend.exec(sql);
        }
        this.setMeta("schema_version", String(migration.version));
      }
    });
  }
  getSchemaVersion() {
    try {
      const version = this.getMeta("schema_version");
      if (version !== null) return parseInt(version, 10) || 0;
    } catch {
    }
    const hasContentHash = this.columnExists("files", "content_hash");
    if (hasContentHash) {
      this.setMeta("schema_version", "2");
      return 2;
    }
    return 1;
  }
  columnExists(table, column) {
    try {
      const rows = this.backend.prepare(`PRAGMA table_info(${table})`).all();
      return rows.some((row) => row.name === column);
    } catch {
      return false;
    }
  }
  get raw() {
    return this.backend;
  }
  setMeta(key, value) {
    this.backend.prepare(
      "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"
    ).run(key, value);
  }
  getMeta(key) {
    const row = this.backend.prepare("SELECT value FROM meta WHERE key = ?").get(key);
    return row ? row.value : null;
  }
  upsertFile(file) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO files (path, repo, language, git_blob_hash, content_hash, last_scanned, size_bytes, lines, metadata, namespace)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      file.path,
      file.repo,
      file.language,
      file.gitBlobHash,
      file.contentHash,
      file.lastScanned,
      file.sizeBytes,
      file.lines,
      file.metadata ? JSON.stringify(file.metadata) : "{}",
      file.namespace ?? null
    );
  }
  updateFileMetadata(filePath, metadata) {
    const file = this.getFile(filePath);
    let merged = { ...metadata };
    if (file && file.metadata) {
      try {
        merged = { ...JSON.parse(file.metadata), ...metadata };
      } catch {
      }
    }
    const namespace = metadata.namespace || null;
    if (namespace) {
      this.backend.prepare("UPDATE files SET metadata = ?, namespace = ? WHERE path = ?").run(JSON.stringify(merged), namespace, filePath);
    } else {
      this.backend.prepare("UPDATE files SET metadata = ? WHERE path = ?").run(JSON.stringify(merged), filePath);
    }
  }
  deleteFile(filePath) {
    this.backend.prepare("DELETE FROM symbols WHERE file_path = ?").run(filePath);
    this.backend.prepare("DELETE FROM edges WHERE source_file = ? OR target_file = ?").run(filePath, filePath);
    this.backend.prepare("DELETE FROM files WHERE path = ?").run(filePath);
  }
  getFile(filePath) {
    return this.backend.prepare("SELECT * FROM files WHERE path = ?").get(filePath);
  }
  getAllFiles(repo) {
    if (repo) {
      return this.backend.prepare("SELECT * FROM files WHERE repo = ?").all(repo);
    }
    return this.backend.prepare("SELECT * FROM files").all();
  }
  insertSymbol(sym) {
    this.backend.prepare(`
      INSERT INTO symbols (file_path, repo, name, kind, scope, signature, start_line, end_line, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(sym.filePath, sym.repo, sym.name, sym.kind, sym.scope, sym.signature, sym.startLine, sym.endLine, sym.metadata);
  }
  deleteSymbolsForFile(filePath) {
    this.backend.prepare("DELETE FROM symbols WHERE file_path = ?").run(filePath);
  }
  searchSymbols(namePattern, repo) {
    const { scopeLike, namePattern: term } = parseSearchPattern(namePattern);
    let likePattern;
    if (isWildcardTerm(term)) {
      likePattern = "%";
    } else if (isGlobPattern(term)) {
      likePattern = globToLike(term);
    } else {
      likePattern = `%${term}%`;
    }
    if (scopeLike) {
      if (repo) {
        return this.backend.prepare(
          "SELECT * FROM symbols WHERE name LIKE ? COLLATE NOCASE AND scope LIKE ? COLLATE NOCASE AND repo = ? ORDER BY kind, name"
        ).all(likePattern, scopeLike, repo);
      }
      return this.backend.prepare(
        "SELECT * FROM symbols WHERE name LIKE ? COLLATE NOCASE AND scope LIKE ? COLLATE NOCASE ORDER BY kind, name"
      ).all(likePattern, scopeLike);
    } else {
      if (repo) {
        return this.backend.prepare(
          "SELECT * FROM symbols WHERE name LIKE ? COLLATE NOCASE AND repo = ? ORDER BY kind, name"
        ).all(likePattern, repo);
      }
      return this.backend.prepare(
        "SELECT * FROM symbols WHERE name LIKE ? COLLATE NOCASE ORDER BY kind, name"
      ).all(likePattern);
    }
  }
  getSymbolsForFile(filePath) {
    return this.backend.prepare(
      "SELECT * FROM symbols WHERE file_path = ? ORDER BY start_line"
    ).all(filePath);
  }
  getAllSymbols(repo) {
    if (repo) {
      return this.backend.prepare("SELECT * FROM symbols WHERE repo = ? ORDER BY file_path, start_line").all(repo);
    }
    return this.backend.prepare("SELECT * FROM symbols ORDER BY file_path, start_line").all();
  }
  insertEdge(edge) {
    this.backend.prepare(`
      INSERT INTO edges (source_file, target_file, source_symbol, target_symbol, edge_type, repo, weight, verifiability, metadata, target_repo)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      edge.sourceFile,
      edge.targetFile,
      edge.sourceSymbol,
      edge.targetSymbol,
      edge.edgeType,
      edge.repo,
      edge.weight,
      edge.verifiability ?? "verified",
      edge.metadata ? JSON.stringify(edge.metadata) : "{}",
      edge.targetRepo ?? null
    );
  }
  deleteEdgesForFile(filePath) {
    this.backend.prepare("DELETE FROM edges WHERE source_file = ?").run(filePath);
  }
  deleteFrameworkEdgesForRepo(repoName) {
    this.backend.prepare(
      "DELETE FROM edges WHERE repo = ? AND edge_type IN ('route', 'middleware', 'hook', 'graphql_resolver', 'message_handler', 'websocket_handler')"
    ).run(repoName);
  }
  getEdgesForFile(filePath) {
    return this.backend.prepare(
      "SELECT * FROM edges WHERE source_file = ? ORDER BY edge_type"
    ).all(filePath);
  }
  getReverseEdges(filePath) {
    return this.backend.prepare(
      "SELECT * FROM edges WHERE target_file = ? ORDER BY edge_type"
    ).all(filePath);
  }
  getAllEdges(repo) {
    if (repo) {
      return this.backend.prepare("SELECT * FROM edges WHERE repo = ?").all(repo);
    }
    return this.backend.prepare("SELECT * FROM edges").all();
  }
  queryEdges(options) {
    let sql = "SELECT * FROM edges WHERE 1=1";
    const params = [];
    if (options.repo) {
      sql += " AND repo = ?";
      params.push(options.repo);
    }
    if (options.type) {
      sql += " AND edge_type = ?";
      params.push(options.type);
    }
    if (options.from) {
      sql += " AND source_file LIKE ?";
      params.push(`%${options.from}%`);
    }
    if (options.to) {
      sql += " AND target_file LIKE ?";
      params.push(`%${options.to}%`);
    }
    return this.backend.prepare(sql).all(...params);
  }
  getFileCount(repo) {
    const row = repo ? this.backend.prepare("SELECT COUNT(*) as cnt FROM files WHERE repo = ?").get(repo) : this.backend.prepare("SELECT COUNT(*) as cnt FROM files").get();
    return row ? row.cnt : 0;
  }
  getSymbolCount(repo) {
    const row = repo ? this.backend.prepare("SELECT COUNT(*) as cnt FROM symbols WHERE repo = ?").get(repo) : this.backend.prepare("SELECT COUNT(*) as cnt FROM symbols").get();
    return row ? row.cnt : 0;
  }
  getEdgeCount(repo) {
    const row = repo ? this.backend.prepare("SELECT COUNT(*) as cnt FROM edges WHERE repo = ?").get(repo) : this.backend.prepare("SELECT COUNT(*) as cnt FROM edges").get();
    return row ? row.cnt : 0;
  }
  getLanguageBreakdown(repo) {
    const rows = repo ? this.backend.prepare("SELECT language, COUNT(*) as cnt FROM files WHERE repo = ? GROUP BY language").all(repo) : this.backend.prepare("SELECT language, COUNT(*) as cnt FROM files GROUP BY language").all();
    const result = {};
    for (const row of rows) {
      result[row.language] = row.cnt;
    }
    return result;
  }
  upsertSnapshot(snap) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO snapshots (commit_sha, parent_sha, timestamp, files_added, files_modified, files_removed, symbols_delta)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `).run(snap.commitSha, snap.parentSha, snap.timestamp, snap.filesAdded, snap.filesModified, snap.filesRemoved, snap.symbolsDelta);
  }
  getLatestSnapshot() {
    return this.backend.prepare("SELECT * FROM snapshots ORDER BY timestamp DESC LIMIT 1").get();
  }
  clearClusters(repo) {
    this.backend.prepare("DELETE FROM cluster_membership WHERE repo = ?").run(repo);
    this.backend.prepare("DELETE FROM clusters WHERE repo = ?").run(repo);
  }
  insertCluster(cluster) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO clusters (repo, name, label, source, parent_name, depth, file_count, layer)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      cluster.repo,
      cluster.name,
      cluster.label,
      cluster.source,
      cluster.parentName,
      cluster.depth,
      cluster.fileCount,
      cluster.layer ?? "other"
    );
  }
  insertClusterMembership(membership) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO cluster_membership (file_path, cluster_name, repo, is_primary)
      VALUES (?, ?, ?, ?)
    `).run(
      membership.filePath,
      membership.clusterName,
      membership.repo,
      membership.isPrimary
    );
  }
  getClusters(repo) {
    if (repo) {
      return this.backend.prepare("SELECT * FROM clusters WHERE repo = ?").all(repo);
    }
    return this.backend.prepare("SELECT * FROM clusters").all();
  }
  getClusterMemberships(repo) {
    if (repo) {
      return this.backend.prepare("SELECT * FROM cluster_membership WHERE repo = ?").all(repo);
    }
    return this.backend.prepare("SELECT * FROM cluster_membership").all();
  }
  getClusterFiles(clusterName, repo) {
    const rows = this.backend.prepare(`
      SELECT file_path FROM cluster_membership
      WHERE cluster_name = ? AND repo = ? AND is_primary = 1
    `).all(clusterName, repo);
    return rows.map((row) => row.file_path);
  }
  getClusterEdges(clusterName, repo) {
    const rows = this.backend.prepare(`
      SELECT 
        src_cm.cluster_name AS sourceCluster,
        tgt_cm.cluster_name AS targetCluster,
        COUNT(*) AS edgeCount,
        edge_type AS dominantType
      FROM edges e
      JOIN cluster_membership src_cm ON e.source_file = src_cm.file_path AND src_cm.is_primary = 1 AND src_cm.repo = e.repo
      JOIN cluster_membership tgt_cm ON e.target_file = tgt_cm.file_path AND tgt_cm.is_primary = 1 AND tgt_cm.repo = e.repo
      WHERE e.repo = ? AND (src_cm.cluster_name = ? OR tgt_cm.cluster_name = ?) AND src_cm.cluster_name != tgt_cm.cluster_name
      GROUP BY src_cm.cluster_name, tgt_cm.cluster_name, e.edge_type
    `).all(repo, clusterName, clusterName);
    const grouped = /* @__PURE__ */ new Map();
    for (const r of rows) {
      const key = `${r.sourceCluster}->${r.targetCluster}`;
      if (!grouped.has(key)) {
        grouped.set(key, {
          sourceCluster: r.sourceCluster,
          targetCluster: r.targetCluster,
          edgeCount: 0,
          types: {}
        });
      }
      const entry = grouped.get(key);
      entry.edgeCount += r.edgeCount;
      entry.types[r.dominantType] = (entry.types[r.dominantType] || 0) + r.edgeCount;
    }
    return Array.from(grouped.values()).map((g) => {
      let dominantType = "";
      let maxCount = -1;
      for (const [t, c] of Object.entries(g.types)) {
        if (c > maxCount) {
          maxCount = c;
          dominantType = t;
        }
      }
      return {
        sourceCluster: g.sourceCluster,
        targetCluster: g.targetCluster,
        edgeCount: g.edgeCount,
        dominantType
      };
    });
  }
  /**
   * Wipe all data owned by a repo in preparation for a fresh forced re-scan.
   * Unlike deleteRepo(), this intentionally preserves:
   *   - Cross-repo edges where this repo is only the TARGET (those belong to
   *     other repos' scans and will be cleaned up when those repos are re-scanned).
   *   - last_scan_commit / last_scan_time meta keys (updated once scan completes).
   */
  resetRepoForScan(repoName) {
    this.inTransaction(() => {
      this.backend.prepare("DELETE FROM symbols WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM edges WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM files WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM cluster_membership WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM clusters WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM classification_signals WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM cluster_metrics WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM arch_smells WHERE repo = ?").run(repoName);
    });
  }
  deleteRepo(repoName) {
    this.inTransaction(() => {
      this.backend.prepare("DELETE FROM symbols WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM edges WHERE repo = ? OR target_repo = ?").run(repoName, repoName);
      this.backend.prepare("DELETE FROM files WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM classification_signals WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM cluster_metrics WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM arch_smells WHERE repo = ?").run(repoName);
      this.backend.prepare("DELETE FROM meta WHERE key LIKE ? OR key LIKE ?").run(`last_scan_commit:${repoName}`, `last_scan_time:${repoName}`);
    });
  }
  searchSymbolsFiltered(options) {
    const limit = options.limit ?? 20;
    const offset = options.offset ?? 0;
    const { scopeLike, namePattern: term } = parseSearchPattern(options.term);
    let sql = "SELECT * FROM symbols WHERE ";
    const params = [];
    const wildcard = isWildcardTerm(term);
    if (wildcard) {
      sql += "1=1";
    } else if (options.exact) {
      sql += "(name = ? COLLATE NOCASE OR file_path = ? COLLATE NOCASE)";
      params.push(term, term);
    } else if (isGlobPattern(term)) {
      const likePattern = globToLike(term);
      sql += "(name LIKE ? COLLATE NOCASE)";
      params.push(likePattern);
    } else {
      sql += "(name LIKE ? COLLATE NOCASE OR file_path LIKE ? COLLATE NOCASE)";
      params.push(`%${term}%`, `%${term}%`);
    }
    if (scopeLike) {
      sql += " AND scope LIKE ? COLLATE NOCASE";
      params.push(scopeLike);
    }
    if (options.kind) {
      sql += " AND LOWER(kind) = ?";
      params.push(options.kind.toLowerCase());
    }
    if (options.filePrefix) {
      sql += " AND file_path LIKE ?";
      params.push(`${options.filePrefix}%`);
    }
    if (options.repo) {
      sql += " AND repo = ?";
      params.push(options.repo);
    }
    sql += " ORDER BY kind, name LIMIT ? OFFSET ?";
    params.push(limit, offset);
    return this.backend.prepare(sql).all(...params);
  }
  countSymbolsFiltered(options) {
    let sql = "SELECT COUNT(*) as cnt FROM symbols WHERE ";
    const params = [];
    const { scopeLike, namePattern: term } = parseSearchPattern(options.term);
    const wildcard = isWildcardTerm(term);
    if (wildcard) {
      sql += "1=1";
    } else if (options.exact) {
      sql += "(name = ? COLLATE NOCASE OR file_path = ? COLLATE NOCASE)";
      params.push(term, term);
    } else if (isGlobPattern(term)) {
      const likePattern = globToLike(term);
      sql += "(name LIKE ? COLLATE NOCASE)";
      params.push(likePattern);
    } else {
      sql += "(name LIKE ? COLLATE NOCASE OR file_path LIKE ? COLLATE NOCASE)";
      params.push(`%${term}%`, `%${term}%`);
    }
    if (scopeLike) {
      sql += " AND scope LIKE ? COLLATE NOCASE";
      params.push(scopeLike);
    }
    if (options.kind) {
      sql += " AND LOWER(kind) = ?";
      params.push(options.kind.toLowerCase());
    }
    if (options.filePrefix) {
      sql += " AND file_path LIKE ?";
      params.push(`${options.filePrefix}%`);
    }
    if (options.repo) {
      sql += " AND repo = ?";
      params.push(options.repo);
    }
    const row = this.backend.prepare(sql).get(...params);
    return row?.cnt ?? 0;
  }
  /**
   * Return all distinct symbol names in the index.
   * Used by the fuzzy matcher to generate suggestions when a search fails.
   */
  getAllSymbolNames(repo) {
    const sql = repo ? "SELECT DISTINCT name FROM symbols WHERE repo = ? ORDER BY name" : "SELECT DISTINCT name FROM symbols ORDER BY name";
    const rows = repo ? this.backend.prepare(sql).all(repo) : this.backend.prepare(sql).all();
    return rows.map((r) => r.name);
  }
  /**
   * Return all symbol kinds and their counts.
   * Used to provide helpful error messages when a kind filter yields no results.
   */
  listSymbolKinds(repo) {
    const sql = repo ? "SELECT kind, COUNT(*) as count FROM symbols WHERE repo = ? GROUP BY kind ORDER BY count DESC" : "SELECT kind, COUNT(*) as count FROM symbols GROUP BY kind ORDER BY count DESC";
    const rows = repo ? this.backend.prepare(sql).all(repo) : this.backend.prepare(sql).all();
    return rows;
  }
  /**
   * Return a lightweight candidates list for fuzzy matching.
   * Only fetches name, kind, file_path — no full row scan.
   */
  getSymbolCandidatesForFuzzy(repo) {
    const sql = repo ? "SELECT DISTINCT name, kind, file_path FROM symbols WHERE repo = ?" : "SELECT DISTINCT name, kind, file_path FROM symbols";
    const rows = repo ? this.backend.prepare(sql).all(repo) : this.backend.prepare(sql).all();
    return rows;
  }
  getSymbolByName(fullName, repo) {
    let scope = null;
    let name = fullName;
    if (fullName.includes("::")) {
      const parts = fullName.split("::");
      name = parts.pop();
      scope = parts.join("::");
    } else if (fullName.includes(".")) {
      const parts = fullName.split(".");
      const last = parts[parts.length - 1].toLowerCase();
      const commonExtensions = /* @__PURE__ */ new Set([
        "ts",
        "js",
        "cs",
        "java",
        "py",
        "php",
        "go",
        "rs",
        "swift",
        "kt",
        "svelte",
        "vue",
        "lua",
        "ex",
        "zig",
        "sh",
        "pas",
        "scala",
        "rb",
        "c",
        "cpp",
        "h",
        "hpp",
        "json",
        "yaml",
        "yml",
        "md"
      ]);
      if (!commonExtensions.has(last)) {
        name = parts.pop();
        scope = parts.join(".");
      }
    }
    if (scope) {
      let res;
      if (repo) {
        res = this.backend.prepare("SELECT * FROM symbols WHERE scope = ? AND name = ? AND repo = ? LIMIT 1").get(scope, name, repo);
      } else {
        res = this.backend.prepare("SELECT * FROM symbols WHERE scope = ? AND name = ? LIMIT 1").get(scope, name);
      }
      if (res) return res;
      let candidates;
      if (repo) {
        candidates = this.backend.prepare("SELECT * FROM symbols WHERE name = ? AND repo = ?").all(name, repo);
      } else {
        candidates = this.backend.prepare("SELECT * FROM symbols WHERE name = ?").all(name);
      }
      const normScope = scope.replace(/\./g, "::");
      for (const cand of candidates) {
        if (cand.scope) {
          const candNormScope = cand.scope.replace(/\./g, "::");
          if (candNormScope === normScope) {
            return cand;
          }
        }
      }
      return void 0;
    } else {
      if (repo) {
        return this.backend.prepare("SELECT * FROM symbols WHERE name = ? AND repo = ? LIMIT 1").get(fullName, repo);
      }
      return this.backend.prepare("SELECT * FROM symbols WHERE name = ? LIMIT 1").get(fullName);
    }
  }
  getFilesFiltered(options) {
    const limit = options.limit ?? 50;
    let sql = "SELECT * FROM files WHERE 1=1";
    const params = [];
    if (options.pathPrefix) {
      const raw = options.pathPrefix;
      const isGlob = raw.includes("*") || raw.includes("?");
      if (isGlob) {
        const likePattern = raw.replace(/\*/g, "%").replace(/\?/g, "_");
        sql += " AND path LIKE ?";
        params.push(likePattern);
      } else {
        sql += " AND path LIKE ?";
        params.push(`${raw}%`);
      }
    }
    if (options.lang) {
      sql += " AND LOWER(language) = ?";
      params.push(options.lang.toLowerCase());
    }
    if (options.repo) {
      sql += " AND repo = ?";
      params.push(options.repo);
    }
    if (options.sort === "lines") {
      sql += " ORDER BY lines DESC";
    } else if (options.sort === "path") {
      sql += " ORDER BY path ASC";
    }
    sql += " LIMIT ?";
    params.push(limit);
    return this.backend.prepare(sql).all(...params);
  }
  getCallersOfSymbol(fullName, repo) {
    let symbols = [];
    let scope = null;
    let name = fullName;
    if (fullName.includes("::")) {
      const parts = fullName.split("::");
      name = parts.pop();
      scope = parts.join("::");
    } else if (fullName.includes(".")) {
      const parts = fullName.split(".");
      const last = parts[parts.length - 1].toLowerCase();
      const commonExtensions = /* @__PURE__ */ new Set([
        "ts",
        "js",
        "cs",
        "java",
        "py",
        "php",
        "go",
        "rs",
        "swift",
        "kt",
        "svelte",
        "vue",
        "lua",
        "ex",
        "zig",
        "sh",
        "pas",
        "scala",
        "rb",
        "c",
        "cpp",
        "h",
        "hpp",
        "json",
        "yaml",
        "yml",
        "md"
      ]);
      if (!commonExtensions.has(last)) {
        name = parts.pop();
        scope = parts.join(".");
      }
    }
    if (scope) {
      let sql = "SELECT * FROM symbols WHERE scope = ? AND name = ?";
      const params = [scope, name];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      symbols = this.backend.prepare(sql).all(...params);
      if (symbols.length === 0) {
        let fallbackSql = "SELECT * FROM symbols WHERE name = ?";
        const fallbackParams = [name];
        if (repo) {
          fallbackSql += " AND repo = ?";
          fallbackParams.push(repo);
        }
        const candidates = this.backend.prepare(fallbackSql).all(...fallbackParams);
        const normScope = scope.replace(/\./g, "::");
        symbols = candidates.filter((cand) => cand.scope && cand.scope.replace(/\./g, "::") === normScope);
      }
    } else {
      let sql = "SELECT * FROM symbols WHERE name = ?";
      const params = [fullName];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      symbols = this.backend.prepare(sql).all(...params);
    }
    if (symbols.length === 0) {
      return [];
    }
    const results = [];
    for (const sym of symbols) {
      let sql = "SELECT * FROM edges WHERE target_file = ? AND (target_symbol = ? OR target_symbol = ?) AND edge_type NOT IN ('import', 'require')";
      const params = [sym.file_path, sym.name, `${sym.scope}::${sym.name}`];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      const edges = this.backend.prepare(sql).all(...params);
      results.push(...edges);
    }
    return results;
  }
  getCalleesOfSymbol(fullName, repo) {
    let symbols = [];
    let scope = null;
    let name = fullName;
    if (fullName.includes("::")) {
      const parts = fullName.split("::");
      name = parts.pop();
      scope = parts.join("::");
    } else if (fullName.includes(".")) {
      const parts = fullName.split(".");
      const last = parts[parts.length - 1].toLowerCase();
      const commonExtensions = /* @__PURE__ */ new Set([
        "ts",
        "js",
        "cs",
        "java",
        "py",
        "php",
        "go",
        "rs",
        "swift",
        "kt",
        "svelte",
        "vue",
        "lua",
        "ex",
        "zig",
        "sh",
        "pas",
        "scala",
        "rb",
        "c",
        "cpp",
        "h",
        "hpp",
        "json",
        "yaml",
        "yml",
        "md"
      ]);
      if (!commonExtensions.has(last)) {
        name = parts.pop();
        scope = parts.join(".");
      }
    }
    if (scope) {
      let sql = "SELECT * FROM symbols WHERE scope = ? AND name = ?";
      const params = [scope, name];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      symbols = this.backend.prepare(sql).all(...params);
      if (symbols.length === 0) {
        let fallbackSql = "SELECT * FROM symbols WHERE name = ?";
        const fallbackParams = [name];
        if (repo) {
          fallbackSql += " AND repo = ?";
          fallbackParams.push(repo);
        }
        const candidates = this.backend.prepare(fallbackSql).all(...fallbackParams);
        const normScope = scope.replace(/\./g, "::");
        symbols = candidates.filter((cand) => cand.scope && cand.scope.replace(/\./g, "::") === normScope);
      }
    } else {
      let sql = "SELECT * FROM symbols WHERE name = ?";
      const params = [fullName];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      symbols = this.backend.prepare(sql).all(...params);
    }
    if (symbols.length === 0) {
      return [];
    }
    const results = [];
    for (const sym of symbols) {
      let sql = "SELECT * FROM edges WHERE source_file = ? AND (source_symbol = ? OR source_symbol = ?)";
      const params = [sym.file_path, sym.name, `${sym.scope}::${sym.name}`];
      if (repo) {
        sql += " AND repo = ?";
        params.push(repo);
      }
      const edges = this.backend.prepare(sql).all(...params);
      results.push(...edges);
    }
    return results;
  }
  getTopFilesByPageRank(graph, limit = 5) {
    return graph.getRankedFiles().slice(0, limit);
  }
  getTopSymbolsByPageRank(graph, limit = 5) {
    return graph.getRankedSymbols().slice(0, limit);
  }
  updateFileRole(filePath, role, confidence) {
    this.backend.prepare("UPDATE files SET role = ?, role_confidence = ? WHERE path = ?").run(role, confidence, filePath);
  }
  insertClassificationSignal(signal) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO classification_signals (file_path, repo, source, role, confidence, reason)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(signal.filePath, signal.repo, signal.source, signal.role, signal.confidence, signal.reason);
  }
  getClassificationSignals(filePath) {
    return this.backend.prepare("SELECT * FROM classification_signals WHERE file_path = ?").all(filePath);
  }
  deleteClassificationSignalsForFile(filePath) {
    this.backend.prepare("DELETE FROM classification_signals WHERE file_path = ?").run(filePath);
  }
  insertClusterMetrics(m) {
    this.backend.prepare(`
      INSERT OR REPLACE INTO cluster_metrics (
        cluster_name, repo, file_count, afferent_coupling, efferent_coupling,
        instability, internal_edges, external_edges, cohesion_ratio,
        abstractness, distance_from_main_seq
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      m.clusterName,
      m.repo,
      m.fileCount,
      m.afferentCoupling,
      m.efferentCoupling,
      m.instability,
      m.internalEdges,
      m.externalEdges,
      m.cohesionRatio,
      m.abstractness,
      m.distanceFromMainSeq
    );
  }
  getClusterMetrics(repo) {
    return this.backend.prepare("SELECT * FROM cluster_metrics WHERE repo = ?").all(repo);
  }
  insertArchSmell(smell) {
    this.backend.prepare(`
      INSERT INTO arch_smells (repo, type, severity, description, involved_files, involved_clusters, suggestion)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `).run(
      smell.repo,
      smell.type,
      smell.severity,
      smell.description,
      JSON.stringify(smell.involvedFiles),
      JSON.stringify(smell.involvedClusters ?? []),
      smell.suggestion
    );
  }
  getArchSmells(repo) {
    const rows = this.backend.prepare("SELECT * FROM arch_smells WHERE repo = ?").all(repo);
    return rows.map((r) => ({
      ...r,
      involvedFiles: JSON.parse(r.involved_files),
      involvedClusters: JSON.parse(r.involved_clusters)
    }));
  }
  inTransaction(fn) {
    return this.backend.inTransaction(fn);
  }
  close() {
    this.backend.close();
  }
}
export {
  Store
};
