import { existsSync, realpathSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve } from "node:path";
import { projectPublicDiff } from "./public-diff.js";

const defaultFileSystem = {
  existsSync,
  realpathSync: (path) => realpathSync.native(path)
};

export function safeChangePaths(changes, cwd, workspacePath, fileSystem = defaultFileSystem) {
  return safeChangeFiles(changes, cwd, workspacePath, fileSystem).map((file) => file.path);
}

export function safeChangeFiles(changes, cwd, workspacePath, fileSystem = defaultFileSystem) {
  if (!cwd || !workspacePath) return [];
  const seen = new Map();
  for (const change of changes ?? []) {
    const workspaceRelative = safeWorkspaceRelativePath(changePath(change), cwd, workspacePath, fileSystem);
    if (!workspaceRelative) continue;
    const path = workspaceRelative.replaceAll("\\", "/");
    if (!seen.has(path)) seen.set(path, compactChangeFile(path, change));
  }
  return [...seen.values()];
}

export function safeWorkspaceRelativePath(filePath, cwd, workspacePath, fileSystem = defaultFileSystem) {
  if (typeof filePath !== "string" || !filePath || !cwd || !workspacePath) return null;

  const absoluteWorkspace = resolve(workspacePath);
  const absolutePath = resolve(cwd, filePath);
  const workspaceRelative = relative(absoluteWorkspace, absolutePath);
  if (!isContainedRelativePath(workspaceRelative)) return null;

  const realWorkspace = realpathIfExists(absoluteWorkspace, fileSystem);
  const realPath = realpathNearest(absolutePath, fileSystem);
  if (!realWorkspace || !realPath) return null;

  const realRelative = relative(realWorkspace, realPath);
  if (!isContainedRelativePath(realRelative)) return null;

  return workspaceRelative;
}

function isContainedRelativePath(relativePath) {
  return Boolean(
    relativePath &&
    !isAbsolute(relativePath) &&
    relativePath !== ".." &&
    !relativePath.startsWith(`..\\`) &&
    !relativePath.startsWith("../")
  );
}

function realpathNearest(path, fileSystem) {
  let current = path;
  const missingParts = [];

  while (!fileSystem.existsSync(current)) {
    const parent = dirname(current);
    if (parent === current) return null;
    missingParts.unshift(current.slice(parent.length + 1));
    current = parent;
  }

  const realCurrent = realpathIfExists(current, fileSystem);
  return realCurrent ? resolve(realCurrent, ...missingParts) : null;
}

function realpathIfExists(path, fileSystem) {
  if (!fileSystem.existsSync(path)) return null;
  try {
    return fileSystem.realpathSync(path);
  } catch {
    return null;
  }
}

function changePath(change) {
  if (typeof change === "string") return change;
  return change?.path ?? change?.filePath ?? change?.file ?? change?.name;
}

function compactChangeFile(path, change) {
  const diff = typeof change === "object" && change
    ? change.diff ?? change.patch ?? change.unifiedDiff
    : undefined;
  return typeof diff === "string" ? { path, ...projectPublicDiff(diff) } : { path };
}
