/**
 * RepoScanner orchestrator: discovery -> parse -> extract -> build graph.
 *
 * Parsing runs in a child process to isolate tree-sitter's native memory.
 * The child is restarted every BATCH_SIZE files so the OS reclaims all
 * native allocations. The main process stays small regardless of repo size.
 */

import { readFileSync, writeFileSync, mkdtempSync, unlinkSync } from "node:fs";
import { resolve, join } from "node:path";
import { tmpdir } from "node:os";
import { execFileSync } from "node:child_process";
import { type ScanConfig, DEFAULT_SCAN_CONFIG } from "../config";
import { RepoIndex } from "../graph/index";
import { KnowledgeGraph } from "../graph/knowledge-graph";
import { makeFileNode } from "../graph/model";
import { discoverJavaFiles } from "./discovery";
import { resolveRelationships } from "./relationship";
import type { WorkerResult } from "./scan-worker";

/** Number of files per worker batch. Worker is killed after each batch. */
const WORKER_BATCH_SIZE = 2000;

export class RepoScanner {
  private _sourcePath: string;
  private _config: ScanConfig;

  constructor(sourcePath: string, config?: Partial<ScanConfig>) {
    this._sourcePath = resolve(sourcePath);
    this._config = { ...DEFAULT_SCAN_CONFIG, ...config };
  }

  scan(): RepoIndex {
    const start = performance.now();

    // 1. Discover files
    const javaFiles = discoverJavaFiles(
      this._sourcePath,
      this._config.maxFileSizeKb,
    );

    if (javaFiles.length === 0) {
      console.warn(`[scan.no_files] ${this._sourcePath}`);
      const graph = new KnowledgeGraph();
      return new RepoIndex(graph, {
        source_path: this._sourcePath,
        source_files: [],
      });
    }

    // 2. Set up disk-backed SQLite
    const dbPath = this._config.dbPath ?? join(mkdtempSync(join(tmpdir(), "repopilot-")), "scan.db");
    console.log(`[scan.db] Using SQLite at ${dbPath}`);
    const graph = new KnowledgeGraph(dbPath);

    // 3. Process files in batches via child processes
    console.log(`[scan.start] ${javaFiles.length} files, worker batch size ${WORKER_BATCH_SIZE}`);

    const allSourceFiles: string[] = [];
    const tmpDir = mkdtempSync(join(tmpdir(), "repopilot-ipc-"));
    let fileIdx = 0;

    // Resolve worker script path — prefer .cjs (CommonJS) since worker uses require.main
    // __dirname varies: in dev it's the scanner/ dir, in bundled builds it's dist/
    const candidates = [
      resolve(__dirname, "scan-worker.cjs"),                         // dev: same dir as scanner.ts
      resolve(__dirname, "repopilot/scanner/scan-worker.cjs"),       // bundled: __dirname=dist/
      resolve(process.cwd(), "packages/sdk/dist/repopilot/scanner/scan-worker.cjs"), // absolute fallback
      resolve(__dirname, "scan-worker.js"),                          // dev: ESM variant
      resolve(__dirname, "repopilot/scanner/scan-worker.js"),        // bundled: ESM variant
    ];
    const workerScript = candidates.find((p) => {
      try { readFileSync(p, { flag: "r" }); return true; } catch { return false; }
    }) ?? candidates[0];
    console.log(`[scan.worker] Using worker script at ${workerScript}`);

    while (fileIdx < javaFiles.length) {
      const batchEnd = Math.min(fileIdx + WORKER_BATCH_SIZE, javaFiles.length);
      const batchFiles = javaFiles.slice(fileIdx, batchEnd);
      const batchNum = Math.floor(fileIdx / WORKER_BATCH_SIZE) + 1;

      console.log(`[scan.batch.${batchNum}] files ${fileIdx + 1}-${batchEnd} of ${javaFiles.length}`);

      // Write input file paths
      const inputPath = join(tmpDir, `batch-${batchNum}-in.json`);
      const outputPath = join(tmpDir, `batch-${batchNum}-out.json`);
      writeFileSync(inputPath, JSON.stringify(batchFiles));

      // Spawn worker — when it exits, ALL native memory is freed
      try {
        execFileSync("node", [
          "--max-old-space-size=4096",
          workerScript,
          inputPath,
          outputPath,
          String(this._config.includeSnippets),
        ], {
          stdio: ["pipe", "inherit", "inherit"],
          timeout: 600000, // 10 min per batch
        });
      } catch (e) {
        console.error(`[scan.batch.${batchNum}.error] Worker failed: ${e instanceof Error ? e.message : e}`);
        // Mark all files in this batch as failed
        for (const fp of batchFiles) {
          addRawFileNode(graph, fp);
          allSourceFiles.push(fp);
        }
        fileIdx = batchEnd;
        continue;
      }

      // Read results and insert into SQLite
      let results: WorkerResult[];
      try {
        results = JSON.parse(readFileSync(outputPath, "utf-8"));
      } catch {
        console.error(`[scan.batch.${batchNum}.error] Failed to read worker output`);
        for (const fp of batchFiles) {
          addRawFileNode(graph, fp);
          allSourceFiles.push(fp);
        }
        fileIdx = batchEnd;
        continue;
      }

      // Insert into graph
      for (const result of results) {
        if (result.nodes.length === 0 && result.status === "parse_failed") {
          addRawFileNode(graph, result.filePath);
        } else {
          graph.transaction(() => {
            for (const node of result.nodes) {
              graph.addNode(node);
            }
            for (const edge of result.edges) {
              if (
                graph.hasNode(edge.source) ||
                edge.target.startsWith("import:") ||
                edge.target.startsWith("class_ref:")
              ) {
                graph.addEdge(edge);
              }
            }
          });
        }
        allSourceFiles.push(result.filePath);
      }

      // Clean up temp files
      try { unlinkSync(inputPath); } catch { /* ignore */ }
      try { unlinkSync(outputPath); } catch { /* ignore */ }

      console.log(
        `[scan.batch.${batchNum}.done] processed=${batchEnd}, total_nodes=${graph.nodeCount}, total_edges=${graph.edgeCount}`,
      );

      fileIdx = batchEnd;
    }

    // 4. Resolve cross-file relationships
    const resolved = resolveRelationships(graph);

    const elapsed = (performance.now() - start) / 1000;

    const metadata = {
      source_path: this._sourcePath,
      source_files: allSourceFiles,
      file_count: javaFiles.length,
      scan_time_seconds: Math.round(elapsed * 100) / 100,
    };

    console.log(
      `[scan.complete] files=${javaFiles.length} nodes=${graph.nodeCount} edges=${graph.edgeCount} resolved=${resolved} elapsed=${elapsed.toFixed(2)}s`,
    );

    return new RepoIndex(graph, metadata);
  }
}

function addRawFileNode(graph: KnowledgeGraph, filePath: string): void {
  let loc = 0;
  try {
    const content = readFileSync(filePath, "utf-8");
    loc = 1;
    for (let i = 0; i < content.length; i++) {
      if (content.charCodeAt(i) === 10) loc++;
    }
  } catch {
    // ignore
  }

  let pkg = "";
  const parts = filePath.split("/");
  const srcIdx = parts.indexOf("src");
  const ejbIdx = parts.indexOf("ejbModule");
  if (srcIdx !== -1) {
    pkg = parts.slice(srcIdx + 1, -1).join(".");
  } else if (ejbIdx !== -1) {
    pkg = parts.slice(ejbIdx + 1, -1).join(".");
  }

  const node = makeFileNode(filePath, pkg, loc);
  node.attributes.parse_failed = true;
  graph.addNode(node);
}
