import fs from "node:fs";
import path from "node:path";
import { matchesAnyGlob, toPosixPath } from "./glob.js";
import type { AllowedRoot, FollowLinksPolicy, ProjectFsConfig } from "./config.js";

export interface AuthorizedPath {
  requestedPath: string;
  absolutePath: string;
  realPath: string;
  relativePath: string;
  root: AllowedRoot;
  hadLinks: boolean;
  hidden: boolean;
  stats: fs.Stats;
}

export interface TraversalDecision {
  allowed: boolean;
  reason?: string;
}

function compareKey(value: string): string {
  return process.platform === "win32" ? value.toLowerCase() : value;
}

function isWithin(parentPath: string, candidatePath: string): boolean {
  const relative = path.relative(parentPath, candidatePath);
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}

function findContainingRoot(roots: AllowedRoot[], candidatePath: string, field: keyof AllowedRoot): AllowedRoot | null {
  const matching = roots
    .filter((root) => isWithin(root[field], candidatePath))
    .sort((left, right) => compareKey(right[field]).length - compareKey(left[field]).length);

  return matching[0] || null;
}

function normalizeSegmentKey(value: string): string {
  return process.platform === "win32" ? value.toLowerCase() : value;
}

function relativeSegments(relativePath: string): string[] {
  return toPosixPath(relativePath)
    .split("/")
    .filter(Boolean);
}

function hasBlockedDir(segments: string[], blockedDirs: string[]): boolean {
  const blocked = new Set(blockedDirs.map((entry) => normalizeSegmentKey(entry)));
  return segments.some((segment) => blocked.has(normalizeSegmentKey(segment)));
}

function pathIsBlocked(relativePath: string, config: ProjectFsConfig): boolean {
  const posixRelative = toPosixPath(relativePath);
  if (!posixRelative || posixRelative === ".") {
    return false;
  }

  const segments = relativeSegments(posixRelative);
  if (hasBlockedDir(segments, config.blockedDirs)) {
    return true;
  }

  return matchesAnyGlob(posixRelative, config.blockedGlobs);
}

function inspectLinks(rootPath: string, candidatePath: string): boolean {
  const relative = path.relative(rootPath, candidatePath);
  if (relative.startsWith("..")) {
    return false;
  }

  const segments = relative.split(path.sep).filter(Boolean);
  let current = rootPath;

  for (const segment of segments) {
    current = path.join(current, segment);
    const stat = fs.lstatSync(current);
    if (stat.isSymbolicLink()) {
      return true;
    }
  }

  return false;
}

function assertExpectedKind(stats: fs.Stats, expectedKind: "file" | "dir" | "any"): void {
  if (expectedKind === "any") return;
  if (expectedKind === "file" && !stats.isFile()) {
    throw new Error("Il path richiesto non e' un file.");
  }
  if (expectedKind === "dir" && !stats.isDirectory()) {
    throw new Error("Il path richiesto non e' una directory.");
  }
}

export function resolveUserPath(config: ProjectFsConfig, inputPath: string): string {
  const trimmed = String(inputPath || "").trim();
  if (!trimmed) {
    throw new Error("Parametro path mancante o vuoto.");
  }

  if (path.isAbsolute(trimmed)) {
    return path.resolve(trimmed);
  }

  return path.resolve(config.allowedRoots[0].originalPath, trimmed);
}

export function authorizePath(
  config: ProjectFsConfig,
  inputPath: string,
  expectedKind: "file" | "dir" | "any" = "any"
): AuthorizedPath {
  const absolutePath = resolveUserPath(config, inputPath);
  const lexicalRoot = findContainingRoot(config.allowedRoots, absolutePath, "originalPath");
  if (!lexicalRoot) {
    throw new Error("Accesso negato: il path richiesto e' fuori da allowedRoots.");
  }

  const lexicalRelative = path.relative(lexicalRoot.originalPath, absolutePath);
  if (pathIsBlocked(lexicalRelative, config)) {
    throw new Error("Accesso negato: il path richiesto ricade in una directory o glob bloccata.");
  }

  const stats = fs.lstatSync(absolutePath);
  const hadLinks = inspectLinks(lexicalRoot.originalPath, absolutePath) || stats.isSymbolicLink();
  const realPath = fs.realpathSync.native(absolutePath);
  const realRoot = findContainingRoot(config.allowedRoots, realPath, "realPath");

  if (!realRoot) {
    throw new Error("Accesso negato: il target reale risolve fuori da allowedRoots.");
  }

  const realRelative = path.relative(realRoot.realPath, realPath);
  if (pathIsBlocked(realRelative, config)) {
    throw new Error("Accesso negato: il target reale ricade in una directory o glob bloccata.");
  }

  if (hadLinks && config.followLinks === "deny") {
    throw new Error("Accesso negato: link simbolici e junction sono disabilitati dalla policy corrente.");
  }

  const realStats = fs.statSync(realPath);
  assertExpectedKind(realStats, expectedKind);

  return {
    requestedPath: inputPath,
    absolutePath,
    realPath,
    relativePath: toPosixPath(realRelative || "."),
    root: realRoot,
    hadLinks,
    hidden: path.basename(realPath).startsWith("."),
    stats: realStats
  };
}

export function evaluateTraversalTarget(
  config: ProjectFsConfig,
  candidatePath: string
): TraversalDecision {
  try {
    authorizePath(config, candidatePath, "any");
    return { allowed: true };
  } catch (error) {
    return {
      allowed: false,
      reason: error instanceof Error ? error.message : String(error)
    };
  }
}

export function linkPolicyStatus(policy: FollowLinksPolicy, hadLinks: boolean): "none" | "denied" | "allowed" | "reported" {
  if (!hadLinks) return "none";
  if (policy === "deny") return "denied";
  if (policy === "allow-within-whitelist") return "allowed";
  return "reported";
}

export function isHiddenPath(targetPath: string): boolean {
  return path.basename(targetPath).startsWith(".");
}
