import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

export type FollowLinksPolicy = "deny" | "allow-within-whitelist" | "report-only";

export interface AllowedRoot {
  originalPath: string;
  realPath: string;
}

export interface ProjectFsConfig {
  allowedRoots: AllowedRoot[];
  blockedGlobs: string[];
  blockedDirs: string[];
  followLinks: FollowLinksPolicy;
  maxFileBytes: number;
  maxSearchResults: number;
  maxDepth: number;
  searchTimeoutMs: number;
  configPath: string;
}

interface RawConfig {
  allowedRoots?: unknown;
  blockedGlobs?: unknown;
  blockedDirs?: unknown;
  followLinks?: unknown;
  maxFileBytes?: unknown;
  maxSearchResults?: unknown;
  maxDepth?: unknown;
  searchTimeoutMs?: unknown;
}

interface LoadConfigOptions {
  envFilePaths?: string[];
  projectPath?: string;
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export const DEFAULT_BLOCKED_DIRS = [
  ".git",
  "node_modules",
  "vendor",
  "dist",
  "build",
  "bin",
  "obj"
];

const DEFAULTS = {
  blockedGlobs: [] as string[],
  blockedDirs: DEFAULT_BLOCKED_DIRS,
  followLinks: "allow-within-whitelist" as FollowLinksPolicy,
  maxFileBytes: 262144,
  maxSearchResults: 100,
  maxDepth: 8,
  searchTimeoutMs: 3000
};

function parseDotEnv(raw: string): Record<string, string> {
  const parsed: Record<string, string> = {};

  for (const line of raw.split(/\r?\n/)) {
    const trimmed = line.trim();
    if (trimmed === "" || trimmed.startsWith("#")) {
      continue;
    }

    const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
    if (!match) {
      continue;
    }

    const [, key, rawValue] = match;
    let value = rawValue.trim();

    if (
      (value.startsWith("\"") && value.endsWith("\"")) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      const quote = value[0];
      value = value.slice(1, -1);
      if (quote === "\"") {
        value = value
          .replace(/\\n/g, "\n")
          .replace(/\\r/g, "\r")
          .replace(/\\t/g, "\t")
          .replace(/\\"/g, "\"");
      }
    } else {
      const commentIndex = value.search(/\s+#/);
      if (commentIndex >= 0) {
        value = value.slice(0, commentIndex).trimEnd();
      }
    }

    parsed[key] = value;
  }

  return parsed;
}

function uniquePathList(values: string[]): string[] {
  return uniqueCaseAware(values.map((value) => path.resolve(value)));
}

function collectEnvFilePaths(env: NodeJS.ProcessEnv, options: LoadConfigOptions = {}): string[] {
  const projectEnvPath =
    typeof options.projectPath === "string" && options.projectPath.trim() !== ""
      ? path.resolve(options.projectPath, ".env")
      : undefined;

  return projectEnvPath ? uniquePathList([projectEnvPath]) : [];
}

function mergeEnvOverrides(
  env: NodeJS.ProcessEnv,
  options: LoadConfigOptions = {}
): NodeJS.ProcessEnv {
  const mergedEnv: NodeJS.ProcessEnv = { ...env };
  const envFilePaths = options.envFilePaths ?? collectEnvFilePaths(env, options);

  for (const envPath of envFilePaths) {
    if (!fs.existsSync(envPath)) {
      continue;
    }

    const parsed = parseDotEnv(fs.readFileSync(envPath, "utf8"));
    for (const [key, value] of Object.entries(parsed)) {
      mergedEnv[key] = value;
    }
  }

  return mergedEnv;
}

function parseJsonConfig(filePath: string): RawConfig {
  if (!fs.existsSync(filePath)) {
    return {};
  }

  const raw = fs.readFileSync(filePath, "utf8");
  if (raw.trim() === "") {
    return {};
  }

  const parsed = JSON.parse(raw);
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error(`Config non valida in ${filePath}: atteso un oggetto JSON.`);
  }

  return parsed as RawConfig;
}

function parseListEnv(value: string | undefined): string[] | undefined {
  if (typeof value !== "string" || value.trim() === "") {
    return undefined;
  }

  const trimmed = value.trim();
  if (trimmed.startsWith("[")) {
    const parsed = JSON.parse(trimmed);
    if (!Array.isArray(parsed)) {
      throw new Error("Le variabili list PROJECTFS_* devono essere array JSON o liste separate da virgola/punto e virgola.");
    }
    return parsed.map((entry) => String(entry).trim()).filter(Boolean);
  }

  return trimmed
    .split(/[,\n;]+/)
    .map((entry) => entry.trim())
    .filter(Boolean);
}

function coerceStringArray(value: unknown, fieldName: string): string[] | undefined {
  if (value === undefined) return undefined;
  if (!Array.isArray(value)) {
    throw new Error(`${fieldName} deve essere un array di stringhe.`);
  }

  return value.map((entry) => String(entry).trim()).filter(Boolean);
}

function coercePositiveInteger(value: unknown, fieldName: string): number | undefined {
  if (value === undefined) return undefined;
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) {
    throw new Error(`${fieldName} deve essere un intero positivo.`);
  }
  return parsed;
}

function normalizePolicy(value: unknown): FollowLinksPolicy | undefined {
  if (value === undefined) return undefined;
  if (value === "deny" || value === "allow-within-whitelist" || value === "report-only") {
    return value;
  }
  throw new Error("followLinks deve essere uno tra: deny, allow-within-whitelist, report-only.");
}

function uniqueCaseAware(values: string[]): string[] {
  const seen = new Set<string>();
  const normalized: string[] = [];

  for (const value of values) {
    const key = process.platform === "win32" ? value.toLowerCase() : value;
    if (seen.has(key)) continue;
    seen.add(key);
    normalized.push(value);
  }

  return normalized;
}

function resolveAllowedRoots(values: string[]): AllowedRoot[] {
  if (values.length === 0) {
    throw new Error("allowedRoots obbligatorio: configura projectfs.config.json o PROJECTFS_ALLOWED_ROOTS.");
  }

  return uniqueCaseAware(values.map((entry) => path.resolve(entry))).map((resolvedPath) => {
    if (!fs.existsSync(resolvedPath)) {
      throw new Error(`Allowed root inesistente: ${resolvedPath}`);
    }

    const stat = fs.statSync(resolvedPath);
    if (!stat.isDirectory()) {
      throw new Error(`Allowed root non directory: ${resolvedPath}`);
    }

    return {
      originalPath: resolvedPath,
      realPath: fs.realpathSync.native(resolvedPath)
    };
  });
}

export function getDefaultConfigPath(): string {
  return path.resolve(__dirname, "..", "projectfs.config.json");
}

export function loadConfig(
  env: NodeJS.ProcessEnv = process.env,
  options: LoadConfigOptions = {}
): ProjectFsConfig {
  const mergedEnv = mergeEnvOverrides(env, options);
  const configPath = path.resolve(mergedEnv.PROJECTFS_CONFIG || getDefaultConfigPath());
  const fileConfig = parseJsonConfig(configPath);

  const mergedAllowedRoots =
    parseListEnv(mergedEnv.PROJECTFS_ALLOWED_ROOTS) ??
    coerceStringArray(fileConfig.allowedRoots, "allowedRoots") ??
    [];

  const mergedBlockedGlobs =
    parseListEnv(mergedEnv.PROJECTFS_BLOCKED_GLOBS) ??
    coerceStringArray(fileConfig.blockedGlobs, "blockedGlobs") ??
    DEFAULTS.blockedGlobs;

  const mergedBlockedDirs =
    parseListEnv(mergedEnv.PROJECTFS_BLOCKED_DIRS) ??
    coerceStringArray(fileConfig.blockedDirs, "blockedDirs") ??
    DEFAULTS.blockedDirs;

  const followLinks =
    normalizePolicy(mergedEnv.PROJECTFS_FOLLOW_LINKS) ??
    normalizePolicy(fileConfig.followLinks) ??
    DEFAULTS.followLinks;

  const maxFileBytes =
    coercePositiveInteger(mergedEnv.PROJECTFS_MAX_FILE_BYTES, "PROJECTFS_MAX_FILE_BYTES") ??
    coercePositiveInteger(fileConfig.maxFileBytes, "maxFileBytes") ??
    DEFAULTS.maxFileBytes;

  const maxSearchResults =
    coercePositiveInteger(mergedEnv.PROJECTFS_MAX_SEARCH_RESULTS, "PROJECTFS_MAX_SEARCH_RESULTS") ??
    coercePositiveInteger(fileConfig.maxSearchResults, "maxSearchResults") ??
    DEFAULTS.maxSearchResults;

  const maxDepth =
    coercePositiveInteger(mergedEnv.PROJECTFS_MAX_DEPTH, "PROJECTFS_MAX_DEPTH") ??
    coercePositiveInteger(fileConfig.maxDepth, "maxDepth") ??
    DEFAULTS.maxDepth;

  const searchTimeoutMs =
    coercePositiveInteger(mergedEnv.PROJECTFS_SEARCH_TIMEOUT_MS, "PROJECTFS_SEARCH_TIMEOUT_MS") ??
    coercePositiveInteger(fileConfig.searchTimeoutMs, "searchTimeoutMs") ??
    DEFAULTS.searchTimeoutMs;

  return {
    allowedRoots: resolveAllowedRoots(mergedAllowedRoots),
    blockedGlobs: uniqueCaseAware(mergedBlockedGlobs),
    blockedDirs: uniqueCaseAware(mergedBlockedDirs),
    followLinks,
    maxFileBytes,
    maxSearchResults,
    maxDepth,
    searchTimeoutMs,
    configPath
  };
}
