import { randomUUID } from "node:crypto";
import { execFile } from "node:child_process";
import { mkdir, readdir } from "node:fs/promises";
import { basename, dirname, parse, resolve } from "node:path";
import { promisify } from "node:util";
import { createWorkspaceForPath, validateProjectFolderName } from "./workspaces.js";

const folders = new Map();
const TOKEN_TTL_MS = 30 * 60 * 1000;
const execFileAsync = promisify(execFile);

export async function browseFolders(config, folderId, dependencies = {}) {
  pruneExpired();
  if (!folderId) {
    const roots = (await browserRoots(config, dependencies)).map((path) => ({ id: remember(path), name: driveLabel(path) }));
    return { current: "This PC", folders: roots };
  }
  const currentPath = folderPath(folderId);
  const readDirectory = dependencies.readDirectory ?? readdir;
  let entries;
  try {
    entries = await readDirectory(currentPath, { withFileTypes: true });
  } catch {
    throw publicError("folder_browse_failed", 403);
  }
  const root = parse(currentPath).root;
  const parentId = normalized(currentPath) === normalized(root) ? undefined : remember(dirname(currentPath));
  const childFolders = entries
    .filter((entry) => entry.isDirectory())
    .map((entry) => ({ id: remember(resolve(currentPath, entry.name)), name: entry.name }))
    .sort((left, right) => left.name.localeCompare(right.name));
  return { current: basename(currentPath) || driveLabel(currentPath), currentId: folderId, ...(parentId ? { parentId } : {}), folders: childFolders };
}

export function registerBrowsedWorkspace(config, configPath, folderId, dependencies = {}) {
  return createWorkspaceForPath(config, configPath, folderPath(folderId), dependencies);
}

export async function createBrowsedFolder(config, folderId, value, dependencies = {}) {
  const parentPath = (dependencies.resolveFolder ?? folderPath)(folderId);
  const name = validateProjectFolderName(value);
  const childPath = resolve(parentPath, name);
  if (normalized(dirname(childPath)) !== normalized(parentPath)) throw publicError("invalid_project_name", 400);
  try {
    await (dependencies.makeDirectory ?? mkdir)(childPath);
  } catch {
    throw publicError("folder_create_failed", 409);
  }
  if (dependencies.openFolder) return dependencies.openFolder(config, childPath);
  return browseFolders(config, remember(childPath), dependencies);
}

async function browserRoots(config, dependencies) {
  const candidates = [config.projectsRoot, process.cwd(), ...(config.workspaces ?? []).map((item) => item.path)];
  const listDrives = dependencies.listDrives ?? logicalDriveRoots;
  candidates.push(...await listDrives().catch(() => []));
  const roots = new Map();
  for (const candidate of candidates) {
    if (typeof candidate !== "string" || !candidate.trim()) continue;
    const root = parse(resolve(candidate)).root;
    roots.set(normalized(root), root);
  }
  return [...roots.values()].sort();
}

async function logicalDriveRoots() {
  const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-Command", "Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Root }"]);
  return stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
}

function remember(path) {
  const id = randomUUID();
  folders.set(id, { path: resolve(path), expiresAt: Date.now() + TOKEN_TTL_MS });
  return id;
}

function folderPath(id) {
  const entry = typeof id === "string" ? folders.get(id) : null;
  if (!entry || entry.expiresAt < Date.now()) throw publicError("folder_selection_expired", 400);
  entry.expiresAt = Date.now() + TOKEN_TTL_MS;
  return entry.path;
}

function pruneExpired() {
  const now = Date.now();
  for (const [id, entry] of folders) if (entry.expiresAt < now) folders.delete(id);
}

function driveLabel(path) {
  return parse(resolve(path)).root.replace(/[\\/]$/, "") || "Drive";
}

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

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