/**
 * Heuristic context packaging for lens subagents.
 *
 * Builds shared context header, file manifest (AST/regex), per-lens file routing,
 * and chunking for large files. No LLM calls -- purely deterministic.
 */

import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { resolveAndValidate } from "./path-safety.js";
import type { LensConfig, LensName, ReviewStage } from "./types.js";

// ── Per-lens file routing ──────────────────────────────────────

const SECURITY_GETS_ALL = true;

const LENS_FILE_FILTERS: Record<string, (file: string) => boolean> = {
  "clean-code": () => true,
  security: () => SECURITY_GETS_ALL,
  "error-handling": () => true,
  performance: () => true, // Activation already filtered
  "api-design": (f) =>
    /\/(api|routes?|controllers?|resolvers?)\//i.test(f) ||
    /\.(controller|resolver|route)\./i.test(f),
  concurrency: () => true, // Activation already filtered
  "test-quality": (f) =>
    /\.(test|spec)\./i.test(f) || /__tests__\//i.test(f) || /\/test\//i.test(f),
  accessibility: (f) =>
    /\.(tsx|jsx|html|vue|svelte|css|scss|ejs|hbs|pug)$/i.test(f),
};

// ── Public API ─────────────────────────────────────────────────

export interface PackagedContext {
  readonly sharedHeader: string;
  readonly projectRules: string;
  readonly fileManifest: string;
  readonly perLensArtifacts: ReadonlyMap<string, string>;
  readonly fileContents: ReadonlyMap<string, string>;
}

export function packageContext(opts: {
  stage: ReviewStage;
  diff: string;
  changedFiles: readonly string[];
  activeLenses: readonly LensName[];
  ticketDescription: string;
  projectRoot: string;
  config: LensConfig;
}): PackagedContext {
  const { stage, diff, changedFiles, activeLenses, ticketDescription, projectRoot, config } = opts;

  // Read project rules
  const rulesPath = join(projectRoot, "RULES.md");
  const projectRules = existsSync(rulesPath)
    ? readFileSync(rulesPath, "utf-8").slice(0, 2000)
    : "(no RULES.md found)";

  // Build file contents map (with path traversal + symlink protection)
  const fileContents = new Map<string, string>();
  for (const file of changedFiles) {
    const safePath = resolveAndValidate(projectRoot, file);
    if (!safePath) continue;
    try {
      fileContents.set(file, readFileSync(safePath, "utf-8"));
    } catch {
      // Skip unreadable files
    }
  }

  // Build file manifest (function signatures via regex)
  const manifest = buildFileManifest(changedFiles, fileContents);

  // Diff stats
  const stats = computeDiffStats(diff);

  // Shared header
  const sharedHeader = [
    `## Review Context`,
    ``,
    `**Stage:** ${stage}`,
    `**Ticket:** ${ticketDescription}`,
    `**Diff stats:** ${stats.filesChanged} files, +${stats.insertions}/-${stats.deletions} lines`,
    ``,
    `**Project rules (excerpt):**`,
    projectRules,
  ].join("\n");

  // Per-lens artifacts: route files to each lens
  const perLensArtifacts = new Map<string, string>();
  for (const lens of activeLenses) {
    const filter = LENS_FILE_FILTERS[lens] ?? (() => true);
    const relevantFiles = changedFiles.filter(filter);

    if (stage === "PLAN_REVIEW") {
      // Plan review: lens gets the full plan text
      perLensArtifacts.set(lens, diff);
    } else {
      // Code review: route relevant diff hunks
      const artifact = extractDiffForFiles(diff, relevantFiles, config.tokenBudgetPerLens);
      perLensArtifacts.set(lens, artifact);
    }
  }

  return {
    sharedHeader,
    projectRules,
    fileManifest: manifest,
    perLensArtifacts,
    fileContents,
  };
}

// ── File manifest ──────────────────────────────────────────────

function buildFileManifest(
  files: readonly string[],
  contents: ReadonlyMap<string, string>,
): string {
  const lines: string[] = ["## Changed Files", ""];

  for (const file of files) {
    const content = contents.get(file);
    if (!content) {
      lines.push(`- ${file}`);
      continue;
    }

    const signatures = extractSignatures(content);
    if (signatures.length > 0) {
      lines.push(`- ${file}: ${signatures.join(", ")}`);
    } else {
      lines.push(`- ${file} (${content.split("\n").length} lines)`);
    }
  }

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

function extractSignatures(content: string): string[] {
  const sigs: string[] = [];
  const patterns = [
    /^export\s+(?:async\s+)?function\s+(\w+)/gm,
    /^export\s+(?:default\s+)?class\s+(\w+)/gm,
    /^export\s+(?:const|let)\s+(\w+)\s*=/gm,
    /^(?:async\s+)?function\s+(\w+)/gm,
    /^class\s+(\w+)/gm,
    /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/gm, // Rust
    /^func\s+(\w+)/gm, // Go/Swift
    /^struct\s+(\w+)/gm, // Swift/Rust/Go
  ];

  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(content)) !== null) {
      if (match[1] && !sigs.includes(match[1])) {
        sigs.push(match[1]);
      }
    }
  }

  return sigs.slice(0, 10); // Cap at 10 signatures
}

// ── Diff stats ─────────────────────────────────────────────────

interface DiffStats {
  filesChanged: number;
  insertions: number;
  deletions: number;
}

function computeDiffStats(diff: string): DiffStats {
  let filesChanged = 0;
  let insertions = 0;
  let deletions = 0;

  for (const line of diff.split("\n")) {
    if (line.startsWith("diff --git")) filesChanged++;
    else if (line.startsWith("+") && !line.startsWith("+++")) insertions++;
    else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
  }

  return { filesChanged, insertions, deletions };
}

// ── Diff extraction per file ───────────────────────────────────

function extractDiffForFiles(
  fullDiff: string,
  files: readonly string[],
  tokenBudget: number,
): string {
  if (files.length === 0) return "(no relevant files in diff)";

  const fileSet = new Set(files);
  const chunks: string[] = [];
  let currentChunk: string[] = [];
  let currentFile = "";

  for (const line of fullDiff.split("\n")) {
    if (line.startsWith("diff --git")) {
      // Flush previous chunk
      if (currentChunk.length > 0 && fileSet.has(currentFile)) {
        chunks.push(currentChunk.join("\n"));
      }
      currentChunk = [line];
      // Extract file path: diff --git a/path b/path
      const match = line.match(/diff --git a\/(.+?) b\//);
      currentFile = match?.[1] ?? "";
    } else {
      currentChunk.push(line);
    }
  }
  // Flush last chunk
  if (currentChunk.length > 0 && fileSet.has(currentFile)) {
    chunks.push(currentChunk.join("\n"));
  }

  const result = chunks.join("\n");

  // Rough token estimate: ~4 chars per token
  const estimatedTokens = result.length / 4;
  if (estimatedTokens > tokenBudget) {
    // Truncate with indicator
    const maxChars = tokenBudget * 4;
    return result.slice(0, maxChars) + "\n\n[TRUNCATED -- diff exceeds token budget]";
  }

  return result;
}
