import { randomUUID } from "node:crypto";
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { serializeConfigMutation } from "./config-mutation.js";

export function listPublicWorkspaces(config) {
  return config.workspaces.map(publicWorkspace);
}

export async function listProjectFolders(config, dependencies = {}) {
  const root = projectRoot(config);
  const readDirectory = dependencies.readDirectory ?? readdir;
  let entries;
  try {
    entries = await readDirectory(root, { withFileTypes: true });
  } catch {
    throw publicError("projects_root_unavailable", 503);
  }

  const configured = new Map(config.workspaces.map((workspace) => [normalized(workspace.path), workspace.id]));
  return entries
    .filter((entry) => entry.isDirectory())
    .map((entry) => ({
      name: entry.name,
      ...(configured.get(normalized(resolve(root, entry.name))) ? { workspaceId: configured.get(normalized(resolve(root, entry.name))) } : {})
    }))
    .sort((left, right) => left.name.localeCompare(right.name));
}

export function createProjectWorkspace(config, configPath, input, dependencies = {}) {
  return serializeConfigMutation(() => createProjectWorkspaceNow(config, configPath, input, dependencies));
}

async function createProjectWorkspaceNow(config, configPath, input, dependencies = {}) {
  const root = projectRoot(config);
  const name = validateProjectFolderName(input?.name);
  const projectPath = resolve(root, name);
  if (normalized(resolve(projectPath, "..")) !== normalized(root)) throw publicError("invalid_project_name", 400);

  const existing = config.workspaces.find((workspace) => normalized(workspace.path) === normalized(projectPath));
  if (existing) return { workspace: publicWorkspace(existing), workspaces: listPublicWorkspaces(config) };

  const makeDirectory = dependencies.makeDirectory ?? mkdir;
  const readStats = dependencies.readStats ?? stat;
  const removeDirectory = dependencies.removeDirectory ?? rm;
  let created = false;
  try {
    if (input?.create === true) {
      await makeDirectory(projectPath);
      created = true;
    } else {
      const info = await readStats(projectPath);
      if (!info.isDirectory()) throw new Error("not_directory");
    }
  } catch {
    throw publicError(input?.create === true ? "project_create_failed" : "project_not_found", input?.create === true ? 409 : 404);
  }

  const id = uniqueWorkspaceId(config.workspaces, name, dependencies.randomUUID ?? randomUUID);
  const workspace = { id, label: name, path: projectPath };
  const nextConfig = { ...config, workspaces: [...config.workspaces, workspace] };
  try {
    await (dependencies.persistConfig ?? atomicWriteConfig)(configPath, nextConfig);
  } catch {
    if (created) await removeDirectory(projectPath, { recursive: false, force: true }).catch(() => {});
    throw publicError("workspace_config_write_failed", 500);
  }
  config.workspaces = nextConfig.workspaces;
  return { workspace: publicWorkspace(workspace), workspaces: listPublicWorkspaces(config) };
}

export function createWorkspaceForPath(config, configPath, selectedPath, dependencies = {}) {
  return serializeConfigMutation(() => createWorkspaceForPathNow(config, configPath, selectedPath, dependencies));
}

async function createWorkspaceForPathNow(config, configPath, selectedPath, dependencies = {}) {
  const projectPath = resolve(selectedPath);
  const readStats = dependencies.readStats ?? stat;
  try {
    const info = await readStats(projectPath);
    if (!info.isDirectory()) throw new Error("not_directory");
  } catch {
    throw publicError("project_not_found", 404);
  }
  const existing = config.workspaces.find((workspace) => normalized(workspace.path) === normalized(projectPath));
  if (existing) return { workspace: publicWorkspace(existing), workspaces: listPublicWorkspaces(config) };
  const name = basename(projectPath);
  const id = uniqueWorkspaceId(config.workspaces, name, dependencies.randomUUID ?? randomUUID);
  const workspace = { id, label: name, path: projectPath };
  const nextConfig = { ...config, workspaces: [...config.workspaces, workspace] };
  try {
    await (dependencies.persistConfig ?? atomicWriteConfig)(configPath, nextConfig);
  } catch {
    throw publicError("workspace_config_write_failed", 500);
  }
  config.workspaces = nextConfig.workspaces;
  return { workspace: publicWorkspace(workspace), workspaces: listPublicWorkspaces(config) };
}

export function workspaceDisplayLabel(workspace) {
  return workspace?.path ? basename(workspace.path) : undefined;
}

function publicWorkspace(workspace) {
  return { id: workspace.id, label: workspaceDisplayLabel(workspace) };
}

function projectRoot(config) {
  if (typeof config.projectsRoot !== "string" || !config.projectsRoot.trim()) throw publicError("projects_root_unavailable", 503);
  return resolve(config.projectsRoot);
}

export function validateProjectFolderName(value) {
  const name = typeof value === "string" ? value.trim() : "";
  const reserved = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
  if (!name || name.length > 80 || name === "." || name === ".." || /[<>:"/\\|?*\u0000-\u001f]/.test(name) || /[ .]$/.test(name) || reserved.test(name)) {
    throw publicError("invalid_project_name", 400);
  }
  return name;
}

function uniqueWorkspaceId(workspaces, name, uuidFactory) {
  const slug = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "project";
  const suffix = uuidFactory().replaceAll("-", "").slice(0, 8);
  let id = `${slug}-${suffix}`;
  let counter = 2;
  while (workspaces.some((workspace) => workspace.id === id)) id = `${slug}-${suffix}-${counter++}`;
  return id;
}

function normalized(path) {
  return resolve(path).toLowerCase();
}

async function atomicWriteConfig(configPath, config) {
  const temporaryPath = `${configPath}.tmp`;
  await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
  await rename(temporaryPath, configPath);
}

function publicError(message, status) {
  const error = new Error(message);
  error.status = status;
  return error;
}
