import { appendFile, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { basename, dirname, join, relative, resolve } from "node:path";
import { promisify } from "node:util";

const exec = promisify(execFile);
const DEFAULT_APPLY_LOCK_MAX_AGE_MS = 30 * 60 * 1000;
const DEFAULT_APPLY_PICKUP_TIMEOUT_MS = 10 * 1000;
const DEFAULT_APPLY_RESULT_TIMEOUT_MS = 10 * 60 * 1000;

const excludedNames = new Set([
  ".git",
  ".superpowers",
  ".turbo",
  ".worktrees",
  ".code-remote-manager-attachments",
  "node_modules",
  "dist",
  "data",
  "logs",
  "config.local.json"
]);

export function windowsCommand(command, args = []) {
  return {
    command: process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe",
    args: ["/d", "/s", "/c", command, ...args]
  };
}

export function safeApplyWaitError({
  elapsedMs,
  requestPending,
  pickupTimeoutMs = DEFAULT_APPLY_PICKUP_TIMEOUT_MS,
  resultTimeoutMs = DEFAULT_APPLY_RESULT_TIMEOUT_MS
}) {
  if (requestPending && elapsedMs > pickupTimeoutMs) return "safe_runtime_supervisor_unavailable";
  if (elapsedMs > resultTimeoutMs) return "safe_runtime_apply_timeout";
  return "";
}

function isInside(parent, child) {
  const parentPath = resolve(parent);
  const childPath = resolve(child);
  const path = relative(parentPath, childPath);
  return !path || (!path.startsWith("..") && !path.startsWith("/") && !/^[A-Za-z]:/.test(path));
}

function assertInsideSafeHome(safeHome, target, label) {
  if (!safeHome) return;
  if (!isInside(safeHome, target)) {
    throw new Error(`${label} is outside safe runtime home`);
  }
}

function comparablePath(path) {
  const resolved = resolve(path);
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}

export function assertCanonicalSourceRoot(canonicalSourceRoot, requestedSourceRoot) {
  if (typeof canonicalSourceRoot !== "string" || typeof requestedSourceRoot !== "string" ||
    comparablePath(canonicalSourceRoot) !== comparablePath(requestedSourceRoot)) {
    throw new Error("safe_runtime_source_mismatch");
  }
}

export async function acquireApplyLock(lockPath, id, {
  now = () => Date.now(),
  maxAgeMs = DEFAULT_APPLY_LOCK_MAX_AGE_MS
} = {}) {
  const payload = JSON.stringify({ id, createdAt: new Date(now()).toISOString() });
  await mkdir(dirname(lockPath), { recursive: true });

  for (let attempt = 0; attempt < 2; attempt += 1) {
    try {
      await writeFile(lockPath, payload, { flag: "wx" });
      return async () => {
        try {
          const current = JSON.parse(await readFile(lockPath, "utf8"));
          if (current.id === id) await rm(lockPath, { force: true });
        } catch (error) {
          if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
        }
      };
    } catch (error) {
      if (error.code !== "EEXIST") throw error;
      const ageMs = await stat(lockPath).then((entry) => now() - entry.mtimeMs, () => 0);
      if (attempt === 0 && ageMs > maxAgeMs) {
        await rm(lockPath, { force: true });
        continue;
      }
      throw new Error("safe_runtime_apply_in_progress");
    }
  }

  throw new Error("safe_runtime_apply_in_progress");
}

export function runtimeHealthUrls(config = {}, options = {}) {
  const host = config.bindHost || "127.0.0.1";
  const port = config.port || 43110;
  return [
    `http://${host}:${port}/health`,
    options.pwaUrl || "http://127.0.0.1:43111/"
  ];
}

export function pwaAllowedHostsEnvironment(config = {}, env = process.env) {
  const hosts = new Set(String(env.CODE_REMOTE_MANAGER_PWA_ALLOWED_HOSTS || "")
    .split(",")
    .map((host) => host.trim())
    .filter(Boolean));
  for (const origin of config.allowedOrigins ?? []) {
    try {
      const url = new URL(origin);
      if (url.protocol === "https:") hosts.add(url.hostname);
    } catch {}
  }
  return hosts.size > 0 ? { CODE_REMOTE_MANAGER_PWA_ALLOWED_HOSTS: [...hosts].join(",") } : {};
}

export function redactOperationalError(value) {
  return String(value?.message || value || "")
    .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [redacted]")
    .replace(/\b(api[_-]?key|password|token)\s*[:=]\s*[^\s\r\n]+/gi, "$1=[redacted]")
    .replace(/CODEX_HOME=(?:"[^"\r\n]*"|[^\s\r\n]+)/gi, "CODEX_HOME=[redacted]")
    .replace(/"[A-Z]:\\[^"\r\n]+"/gi, "\"[local path redacted]\"")
    .replace(/[A-Z]:\\[^\r\n\s]*/gi, "[local path redacted]")
    .replace(/\bauth\.json\b/gi, "[credential-file redacted]");
}

export async function appendBoundedLog(logPath, chunk, maxBytes = 1024 * 1024) {
  await mkdir(dirname(logPath), { recursive: true });
  await appendFile(logPath, redactOperationalError(chunk));
  const content = await readFile(logPath);
  if (content.byteLength <= maxBytes) return;
  await writeFile(logPath, content.subarray(content.byteLength - maxBytes));
}

function isSafeSourcePath(sourceRoot, sourcePath, ignoredPaths = new Set()) {
  const path = relative(sourceRoot, sourcePath);
  if (!path) return true;
  if (ignoredPaths.has(normalizeRelativePath(path))) return false;
  const parts = path.split(/[\\/]/);
  if (parts.some((part) => excludedNames.has(part))) return false;
  const name = basename(sourcePath);
  if (name.endsWith(".local.json")) return false;
  if (name === ".env" || (name.startsWith(".env.") && name !== ".env.example")) return false;
  if (name.endsWith(".db") || name.endsWith(".db-shm") || name.endsWith(".db-wal")) return false;
  return true;
}

function normalizeRelativePath(path) {
  return path.replaceAll("\\", "/");
}

async function repositoryIgnoredPaths(sourceRoot) {
  try {
    const { stdout } = await exec("git", ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"], {
      cwd: sourceRoot,
      encoding: "utf8",
      windowsHide: true,
      maxBuffer: 16 * 1024 * 1024
    });
    return new Set(stdout.split("\0").filter(Boolean).map(normalizeRelativePath));
  } catch (error) {
    if (error.code === "ENOENT") throw new Error("Git is required to apply repository ignore rules");
    return new Set();
  }
}

export async function copySourceTree(sourceRoot, targetRoot) {
  const ignoredPaths = await repositoryIgnoredPaths(sourceRoot);
  await rm(targetRoot, { recursive: true, force: true });
  await mkdir(targetRoot, { recursive: true });
  await cp(sourceRoot, targetRoot, {
    recursive: true,
    filter: (sourcePath) => isSafeSourcePath(sourceRoot, sourcePath, ignoredPaths)
  });
}

function isWindowsDirectoryLock(error) {
  return ["EPERM", "EACCES", "EBUSY"].includes(error?.code);
}

async function mirrorDirectoryContents(source, target, {
  escapeCode = "release_link_outside_source",
  skipOutsideLinks = false
} = {}) {
  await mkdir(target, { recursive: true });
  for (const entry of await readdir(target)) {
    await rm(join(target, entry), { recursive: true, force: true });
  }
  for (const entry of await readdir(source)) {
    await cp(join(source, entry), join(target, entry), {
      recursive: true,
      force: true,
      dereference: true,
      filter: async (sourcePath) => {
        if (!(await lstat(sourcePath)).isSymbolicLink()) return true;
        let linkTarget;
        try { linkTarget = await realpath(sourcePath); } catch {
          if (skipOutsideLinks) return false;
          throw new Error(escapeCode);
        }
        if (!isInside(source, linkTarget)) {
          if (skipOutsideLinks) return false;
          throw new Error(escapeCode);
        }
        return true;
      }
    });
  }
}

async function promoteReleaseInPlace({ current, staging, previous }) {
  await mirrorDirectoryContents(current, previous, { skipOutsideLinks: true });
  try {
    await mirrorDirectoryContents(staging, current, { escapeCode: "release_link_outside_staging" });
    await rm(staging, { recursive: true, force: true });
  } catch (error) {
    try { await mirrorDirectoryContents(previous, current); } catch {}
    throw error;
  }
}

export async function promoteRelease({ safeHome, current, staging, previous }, { renameDirectory = rename } = {}) {
  assertInsideSafeHome(safeHome, current, "current");
  assertInsideSafeHome(safeHome, staging, "staging");
  assertInsideSafeHome(safeHome, previous, "previous");
  await rm(previous, { recursive: true, force: true });
  try {
    await renameDirectory(current, previous);
  } catch (error) {
    if (error.code === "ENOENT") {
      await renameDirectory(staging, current);
      return;
    }
    if (isWindowsDirectoryLock(error)) {
      await promoteReleaseInPlace({ current, staging, previous });
      return;
    }
    throw error;
  }
  try {
    await renameDirectory(staging, current);
  } catch (error) {
    try { await renameDirectory(previous, current); } catch {}
    throw error;
  }
}

export async function rollbackRelease({ safeHome, current, previous, failed }, { renameDirectory = rename } = {}) {
  assertInsideSafeHome(safeHome, current, "current");
  assertInsideSafeHome(safeHome, previous, "previous");
  assertInsideSafeHome(safeHome, failed, "failed");
  await rm(failed, { recursive: true, force: true });
  try {
    await renameDirectory(current, failed);
  } catch (error) {
    if (!isWindowsDirectoryLock(error)) throw error;
    await mirrorDirectoryContents(current, failed, { skipOutsideLinks: true });
    try {
      await mirrorDirectoryContents(previous, current);
      await rm(previous, { recursive: true, force: true });
    } catch (rollbackError) {
      try { await mirrorDirectoryContents(failed, current); } catch {}
      throw rollbackError;
    }
    return;
  }
  await renameDirectory(previous, current);
}
