import { randomUUID } from "node:crypto";
import { cp, mkdir, rename, rm } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileExists } from "./config.js";
import { listPublicAccounts, persistCreatedAccount } from "./accounts.js";
import { requestCodexAppServer, startCodexDeviceLogin } from "./codex-runtime.js";

const ATTEMPT_TTL_MS = 3 * 60 * 1000;

export async function createAccountLoginAttempts(config, configPath, dependencies = {}) {
  const attemptRoot = resolve(dirname(config.dataDir), "account-login-attempts");
  const makeDirectory = dependencies.makeDirectory ?? mkdir;
  const removeDirectory = dependencies.removeDirectory ?? rm;
  const moveDirectory = dependencies.moveDirectory ?? rename;
  const copyDirectory = dependencies.copyDirectory ?? cp;
  const uuid = dependencies.randomUUID ?? randomUUID;
  const startLogin = dependencies.startLogin ?? startCodexDeviceLogin;
  const requester = dependencies.requester ?? requestCodexAppServer;
  const scheduleExpiry = dependencies.scheduleExpiry ?? ((callback, delay) => setTimeout(() => void callback(), delay));
  const cancelExpiry = dependencies.cancelExpiry ?? clearTimeout;
  const logger = dependencies.logger ?? console;
  const attempts = new Map();
  let startingAttempt = null;

  try {
    await removeDirectory(attemptRoot, { recursive: true, force: true });
  } catch (error) {
    if (!isWindowsLockError(error)) throw error;
    logger.warn("stale account login draft cleanup delayed");
  }
  await makeDirectory(attemptRoot, { recursive: true });

  async function cancelAttempt(attempt) {
    if (!attempts.delete(attempt.attemptId)) return false;
    cancelExpiry(attempt.expiry);
    try { attempt.login.stop?.(); } catch {}
    await removeDirectory(attempt.directory, { recursive: true, force: true }).catch(() => {
      logger.warn("account login draft cleanup delayed");
    });
    return true;
  }

  function armExpiry(attempt) {
    attempt.expiry = scheduleExpiry(() => cancelAttempt(attempt), ATTEMPT_TTL_MS);
  }

  function publicAttempt(attempt) {
    return {
      attemptId: attempt.attemptId,
      loginId: attempt.login.loginId,
      verificationUrl: attempt.login.verificationUrl,
      userCode: attempt.login.userCode
    };
  }

  async function startNewAttempt() {
    dependencies.assertCanAddExecutionAccount?.();
    const token = uuid().replaceAll("-", "");
    const attemptId = `attempt-${token}`;
    const accountId = uniqueAccountId(config, attempts, token);
    const directory = resolve(attemptRoot, attemptId);
    const codexHome = resolve(directory, "codex-home");
    await makeDirectory(codexHome, { recursive: true });

    let login;
    try {
      login = await startLogin({ codexCliPath: config.codexCliPath, codexHome });
      if (!login.loginId || !login.userCode || !isHttpsUrl(login.verificationUrl)) throw new Error("invalid_login_response");
    } catch (error) {
      await removeDirectory(directory, { recursive: true, force: true });
      throw publicError(/429|too many requests/i.test(error.message) ? "account_login_rate_limited" : "account_login_start_failed", /429|too many requests/i.test(error.message) ? 429 : 502);
    }

    const attempt = { attemptId, accountId, directory, codexHome, login };
    attempts.set(attemptId, attempt);
    armExpiry(attempt);
    return publicAttempt(attempt);
  }

  async function start() {
    const activeAttempt = attempts.values().next().value;
    if (activeAttempt) return publicAttempt(activeAttempt);
    if (startingAttempt) return startingAttempt;

    const pending = startNewAttempt();
    startingAttempt = pending;
    try {
      return await pending;
    } finally {
      if (startingAttempt === pending) startingAttempt = null;
    }
  }

  async function complete(attemptId) {
    const attempt = attempts.get(attemptId);
    if (!attempt) throw publicError("account_login_attempt_not_found", 404);
    if (attempt.completing) return attempt.completing;

    const pending = completeAttempt(attempt);
    attempt.completing = pending;
    try {
      return await pending;
    } finally {
      if (attempt.completing === pending) attempt.completing = null;
    }
  }

  async function completeAttempt(attempt) {
    const attemptId = attempt.attemptId;
    if (!await fileExists(resolve(attempt.codexHome, "auth.json"))) {
      return { connected: false, accounts: await listPublicAccounts(config, requester) };
    }

    const draftAccount = { id: attempt.accountId, label: "New account", codexHome: attempt.codexHome };
    const [candidate] = await listPublicAccounts({ ...config, accounts: [draftAccount] }, requester);
    const identity = normalizedIdentity(candidate);
    if (!candidate || candidate.status !== "logged_in" || !identity) throw publicError("account_identity_unavailable", 409);

    const currentAccounts = await listPublicAccounts(config, requester);
    const duplicate = currentAccounts.find((account) => normalizedIdentity(account) === identity);
    if (duplicate) {
      await cancelAttempt(attempt);
      return { connected: true, deduplicated: true, existingAccountId: duplicate.id, accounts: currentAccounts };
    }

    cancelExpiry(attempt.expiry);
    try { await attempt.login.stop?.(); } catch {}
    const finalHome = resolve(dirname(config.dataDir), "accounts", attempt.accountId, "codex-home");
    let promotion = "moved";
    try {
      await makeDirectory(dirname(finalHome), { recursive: true });
      try {
        await moveDirectory(attempt.codexHome, finalHome);
      } catch (error) {
        if (!isWindowsLockError(error)) throw error;
        await copyDirectory(attempt.codexHome, finalHome, { recursive: true, errorOnExist: true, force: false });
        promotion = "copied";
      }
    } catch (error) {
      armExpiry(attempt);
      throw error;
    }
    try {
      const account = { id: attempt.accountId, label: "New account", codexHome: finalHome };
      const accounts = await persistCreatedAccount(config, configPath, account, {
        requester,
        persistConfig: dependencies.persistConfig,
        assertCanAddExecutionAccount: dependencies.assertCanAddExecutionAccount
      });
      try {
        await dependencies.tokenUsageStore?.attach(account.id, identity);
      } catch {
        logger.warn("account token usage snapshot unavailable after login");
      }
      attempts.delete(attemptId);
      await removeDirectory(attempt.directory, { recursive: true, force: true }).catch(() => {
        logger.warn("account login draft cleanup delayed");
      });
      return { connected: true, deduplicated: false, account: accounts.find((item) => item.id === account.id), accounts };
    } catch (error) {
      if (promotion === "copied") await removeDirectory(finalHome, { recursive: true, force: true }).catch(() => {});
      else await moveDirectory(finalHome, attempt.codexHome).catch(() => {});
      armExpiry(attempt);
      throw error;
    }
  }

  async function cancel(attemptId) {
    const attempt = attempts.get(attemptId);
    if (!attempt) return { canceled: false };
    await cancelAttempt(attempt);
    return { canceled: true };
  }

  return { start, complete, cancel };
}

function uniqueAccountId(config, attempts, token) {
  let length = 8;
  let id = `new-account-${token.slice(0, length)}`;
  const used = () => config.accounts.some((account) => account.id === id)
    || [...attempts.values()].some((attempt) => attempt.accountId === id);
  while (used()) {
    length += 4;
    id = `new-account-${token.slice(0, length)}`;
  }
  return id;
}

function normalizedIdentity(account) {
  const value = account?.email ?? account?.displayName;
  return typeof value === "string" ? value.trim().toLowerCase() : "";
}

function isHttpsUrl(value) {
  try { return new URL(value).protocol === "https:"; } catch { return false; }
}

function isWindowsLockError(error) {
  return error?.code === "EPERM" || error?.code === "EBUSY";
}

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