import fs from "fs";
import path from "path";
import { listAllSessions } from "./session-reader";

const IGNORED_NAMES = new Set([
  "node_modules", ".git", ".next", "dist", "build", "__pycache__",
  ".turbo", ".cache", "coverage", ".pytest_cache", ".mypy_cache",
  "target", "vendor", ".DS_Store",
]);

const IGNORED_SUFFIXES = [".pyc"];

const WINDOWS_ABSOLUTE_RE = /^[a-zA-Z]:[\\/]/;

declare global {
  var __piAllowedRootsCache: { roots: Set<string>; expiresAt: number } | undefined;
}

const ALLOWED_ROOTS_TTL_MS = 5_000;

function normalizeSlashes(filePath: string): string {
  return filePath.replace(/\\/g, "/");
}

function isWindowsAbsolutePath(filePath: string): boolean {
  return WINDOWS_ABSOLUTE_RE.test(filePath) || filePath.startsWith("\\\\") || filePath.startsWith("//");
}

export async function getAllowedRoots(): Promise<Set<string>> {
  const now = Date.now();
  const cached = globalThis.__piAllowedRootsCache;
  if (cached && cached.expiresAt > now) return cached.roots;

  const sessions = await listAllSessions();
  const roots = new Set<string>();
  for (const s of sessions) {
    if (s.cwd) roots.add(s.cwd);
  }
  const home = (await import("os")).homedir();
  try {
    for (const name of fs.readdirSync(home)) {
      if (/^pi-cwd-\d{8}$/.test(name)) {
        roots.add(path.join(home, name));
      }
    }
  } catch {
    // ignore
  }

  globalThis.__piAllowedRootsCache = { roots, expiresAt: now + ALLOWED_ROOTS_TTL_MS };
  return roots;
}

export function isPathAllowed(target: string, allowedRoots: Set<string>): boolean {
  for (const root of allowedRoots) {
    const useWindowsRules = isWindowsAbsolutePath(target) || isWindowsAbsolutePath(root);
    const resolver = useWindowsRules ? path.win32 : path;
    const sep = useWindowsRules ? "\\" : path.sep;
    const normalized = resolver.resolve(target);
    const normalizedRoot = resolver.resolve(root);
    const comparable = useWindowsRules ? normalized.toLowerCase() : normalized;
    const comparableRoot = useWindowsRules ? normalizedRoot.toLowerCase() : normalizedRoot;
    const rootWithSep = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
    if (comparable === comparableRoot || comparable.startsWith(rootWithSep)) {
      return true;
    }
  }
  return false;
}

export function isIgnoredName(name: string): boolean {
  if (IGNORED_NAMES.has(name)) return true;
  for (const suf of IGNORED_SUFFIXES) {
    if (name.endsWith(suf)) return true;
  }
  return false;
}

export interface FileSearchItem {
  path: string;
  type: "file" | "dir";
  score: number;
}

function scoreMatch(relPath: string, query: string): number {
  const lower = relPath.toLowerCase();
  const q = query.toLowerCase();
  if (!q) return 1;
  const base = path.basename(lower);
  if (base === q) return 1000;
  if (base.startsWith(q)) return 800 - Math.min(base.length, 100);
  if (base.includes(q)) return 500 - base.indexOf(q);
  if (lower.startsWith(q)) return 400;
  if (lower.includes(q)) return 200 - lower.indexOf(q) * 0.1;
  // fuzzy: all query chars in order
  let qi = 0;
  for (let i = 0; i < lower.length && qi < q.length; i++) {
    if (lower[i] === q[qi]) qi++;
  }
  if (qi === q.length) return 50;
  return 0;
}

/**
 * Walk cwd (bounded) and return fuzzy-matched relative paths.
 */
export function searchFilesInCwd(
  cwd: string,
  query: string,
  options?: { limit?: number; maxDepth?: number; maxNodes?: number }
): FileSearchItem[] {
  const limit = options?.limit ?? 30;
  const maxDepth = options?.maxDepth ?? 8;
  const maxNodes = options?.maxNodes ?? 8000;
  const results: FileSearchItem[] = [];
  let visited = 0;

  function walk(absDir: string, relDir: string, depth: number) {
    if (visited >= maxNodes || depth > maxDepth) return;
    let entries: fs.Dirent[];
    try {
      entries = fs.readdirSync(absDir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const ent of entries) {
      if (visited >= maxNodes) return;
      if (isIgnoredName(ent.name)) continue;
      if (ent.name.startsWith(".") && ent.name !== ".env" && !ent.name.startsWith(".env.")) {
        // skip most dotfiles/dirs except common env files at any depth
        if (ent.isDirectory()) continue;
      }
      visited++;
      const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
      const abs = path.join(absDir, ent.name);
      const type: "file" | "dir" = ent.isDirectory() ? "dir" : "file";
      const score = scoreMatch(normalizeSlashes(rel), query);
      if (score > 0) {
        results.push({ path: normalizeSlashes(rel), type, score });
      }
      if (ent.isDirectory()) {
        walk(abs, rel, depth + 1);
      }
    }
  }

  walk(cwd, "", 0);
  results.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
  return results.slice(0, limit);
}
