import { join } from "node:path";
import { dirname, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { homedir } from "node:os";
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
import { fileExists } from "./config.js";
import { requestCodexAppServer, startCodexDeviceLogin } from "./codex-runtime.js";
import { serializeConfigMutation } from "./config-mutation.js";

export async function listPublicAccounts(config, requester = requestCodexAppServer, getAccountExecutionAccess) {
  const accounts = await Promise.all(config.accounts
    .filter((account) => !account.historyOnly)
    .map((account) => publicAccount(account, config, requester, getAccountExecutionAccess)));
  return accounts.filter(Boolean);
}

async function publicAccount(account, config, requester, getAccountExecutionAccess) {
  const authenticated = await fileExists(join(account.codexHome, "auth.json"));
  const entitlement = publicEntitlementAccess(account.id, getAccountExecutionAccess);
  if (!authenticated) return isManagedAccount(config, account) ? { ...publicPendingAccount(account), ...entitlement } : null;
  const localEmail = authenticated ? await accountEmail(account) : "";
  let identity = {};
  if (authenticated) {
    try {
      const result = await requester({
        codexCliPath: config.codexCliPath,
        codexHome: account.codexHome,
        method: "account/read",
        params: { refreshToken: false }
      });
      const remote = result.account?.type === "chatgpt" ? result.account : {};
      identity = {
        email: cleanEmail(remote.email) || localEmail,
        planType: publicPlanType(remote.planType)
      };
    } catch {
      identity = { email: localEmail };
    }
  }
  const email = identity.email || localEmail;
  return {
    id: account.id,
    label: account.label,
    ...(email ? { email, displayName: email } : {}),
    ...(identity.planType ? { planType: identity.planType } : {}),
    status: authenticated ? "logged_in" : "needs_login",
    ...entitlement
  };
}

function publicEntitlementAccess(accountId, getAccountExecutionAccess) {
  const access = getAccountExecutionAccess?.(accountId);
  return access?.entitlementLocked ? { entitlementLocked: true } : {};
}

async function accountEmail(account) {
  try {
    const auth = JSON.parse(await readFile(join(account.codexHome, "auth.json"), "utf8"));
    const email = auth.OPENAI_ACCOUNT_EMAIL ?? auth.account?.email ?? auth.user?.email ?? auth.email;
    return typeof email === "string" && email.includes("@") ? email : "";
  } catch {
    return "";
  }
}

function cleanEmail(value) {
  return typeof value === "string" && value.includes("@") ? value : "";
}

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

function publicPlanType(value) {
  const allowed = new Set([
    "free", "go", "plus", "pro", "prolite", "team", "self_serve_business_usage_based",
    "business", "enterprise_cbp_usage_based", "enterprise", "edu", "unknown"
  ]);
  return allowed.has(value) ? value : "";
}

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

async function createAccountNow(config, configPath, input, dependencies) {
  dependencies.assertCanAddExecutionAccount?.();
  const label = input?.label === undefined ? "New account" : cleanLabel(input);
  const existingAccounts = config.accounts;

  const uuid = (dependencies.randomUUID ?? randomUUID)().replaceAll("-", "");
  const base = slugify(label);
  let suffixLength = 8;
  let id = `${base}-${uuid.slice(0, suffixLength)}`;
  while (existingAccounts.some((account) => account.id === id)) {
    suffixLength += 4;
    id = `${base}-${uuid.slice(0, suffixLength)}`;
  }

  const account = {
    id,
    label,
    codexHome: resolve(dirname(config.dataDir), "accounts", id, "codex-home")
  };
  await ensureAccountHome(account.codexHome, dependencies.makeDirectory ?? mkdir);
  const nextConfig = { ...config, accounts: [...existingAccounts, account] };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  return { account: publicPendingAccount(account), accounts: await listPublicAccounts(config) };
}

export function persistCreatedAccount(config, configPath, account, dependencies = {}) {
  return serializeConfigMutation(() => persistCreatedAccountNow(config, configPath, account, dependencies));
}

async function persistCreatedAccountNow(config, configPath, account, dependencies) {
  if (config.accounts.some((item) => item.id === account.id)) throw publicError("account_already_exists", 409);
  dependencies.assertCanAddExecutionAccount?.();
  const nextConfig = { ...config, accounts: [...config.accounts, account] };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  return listPublicAccounts(config, dependencies.requester ?? requestCodexAppServer);
}

export async function pruneUnauthenticatedManagedAccounts(config, configPath, dependencies = {}) {
  return { pruned: false };
}

export async function startAccountLogin(config, accountId, starter = startCodexDeviceLogin, makeDirectory = mkdir) {
  const account = config.accounts.find((item) => item.id === accountId && !item.historyOnly);
  if (!account) throw publicError("account_not_found", 404);
  await ensureAccountHome(account.codexHome, makeDirectory);
  let result;
  try {
    result = await starter({ codexCliPath: config.codexCliPath, codexHome: account.codexHome });
  } catch (error) {
    if (/429|too many requests/i.test(error.message)) throw publicError("account_login_rate_limited", 429);
    throw publicError("account_login_start_failed", 502);
  }
  if (!result.loginId || !result.userCode || !isHttpsUrl(result.verificationUrl)) {
    throw publicError("account_login_start_failed", 502);
  }
  return { loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
}

export function completeAccountLogin(config, configPath, accountId, dependencies = {}) {
  return serializeConfigMutation(() => completeAccountLoginNow(config, configPath, accountId, dependencies));
}

async function completeAccountLoginNow(config, configPath, accountId, dependencies) {
  const account = config.accounts.find((item) => item.id === accountId && !item.historyOnly);
  if (!account) throw publicError("account_not_found", 404);

  const listAccounts = dependencies.listAccounts ?? listPublicAccounts;
  const accounts = await listAccounts(config);
  const completed = accounts.find((item) => item.id === accountId);
  if (completed?.status !== "logged_in") {
    return { connected: false, deduplicated: false, accounts };
  }

  const identity = normalizedIdentity(completed);
  if (!identity) throw publicError("account_identity_unavailable", 409);
  return { connected: true, deduplicated: false, accounts };
}

export function removeAllAccounts(config, configPath, dependencies = {}) {
  return serializeConfigMutation(() => removeAllAccountsNow(config, configPath, dependencies));
}

async function removeAllAccountsNow(config, configPath, dependencies) {
  const nextConfig = {
    ...config,
    accounts: config.accounts.map((account) => ({ ...account, historyOnly: true }))
  };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  return { accounts: [] };
}

export function restoreLocalHistorySources(config, configPath, dependencies = {}) {
  return serializeConfigMutation(() => restoreLocalHistorySourcesNow(config, configPath, dependencies));
}

async function restoreLocalHistorySourcesNow(config, configPath, dependencies) {
  if (!config.dataDir) return { restored: 0 };
  const readDirectory = dependencies.readDirectory ?? readdir;
  const managedRoot = resolve(dirname(config.dataDir), "accounts");
  const entries = await safeReadDirectory(managedRoot, readDirectory);
  const knownIds = new Set(config.accounts.map((account) => account.id));
  const knownHomes = new Set(config.accounts.map((account) => normalizedPath(account.codexHome)));
  const restored = [];

  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
    if (!entry.isDirectory() || !isManagedAccountId(entry.name) || knownIds.has(entry.name)) continue;
    const codexHome = resolve(managedRoot, entry.name, "codex-home");
    if (knownHomes.has(normalizedPath(codexHome))) continue;
    if (!await containsLocalHistory(codexHome, readDirectory)) continue;
    restored.push({
      id: entry.name,
      label: "Saved history",
      codexHome,
      historyOnly: true
    });
  }

  const desktopCodexHome = resolve(dependencies.desktopCodexHome ?? resolve(homedir(), ".codex"));
  const restoredHomes = new Set([...knownHomes, ...restored.map((account) => normalizedPath(account.codexHome))]);
  if (!restoredHomes.has(normalizedPath(desktopCodexHome))
    && await containsLocalHistory(desktopCodexHome, readDirectory)) {
    const usedIds = new Set([...knownIds, ...restored.map((account) => account.id)]);
    restored.push({
      id: availableHistorySourceId("desktop-codex", usedIds),
      label: "Desktop Codex history",
      codexHome: desktopCodexHome,
      historyOnly: true
    });
  }

  if (restored.length === 0) return { restored: 0 };
  const nextConfig = { ...config, accounts: [...config.accounts, ...restored] };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  return { restored: restored.length };
}

export function removeAccount(config, configPath, accountId, dependencies = {}) {
  return serializeConfigMutation(() => removeAccountNow(config, configPath, accountId, dependencies));
}

async function removeAccountNow(config, configPath, accountId, dependencies) {
  const account = config.accounts.find((item) => item.id === accountId && !item.historyOnly);
  if (!account) throw publicError("account_not_found", 404);

  let identity = "";
  if (dependencies.tokenUsageStore) {
    try {
      identity = normalizedIdentity({ email: await accountEmail(account) });
      if (!identity) {
        const listAccounts = dependencies.listAccounts ?? listPublicAccounts;
        const [publicAccount] = await listAccounts({ ...config, accounts: [account] });
        identity = normalizedIdentity(publicAccount);
      }
    } catch {
      (dependencies.logger ?? console).warn("account token usage identity unavailable during removal");
    }
  }

  const nextConfig = {
    ...config,
    accounts: config.accounts.map((item) => item === account ? { ...item, historyOnly: true } : item)
  };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  if (identity) {
    try {
      await dependencies.tokenUsageStore.detach(accountId, identity);
    } catch {
      (dependencies.logger ?? console).warn("account token usage snapshot unavailable during removal");
    }
  }
  return { accounts: await listPublicAccounts(config) };
}

export function updateAccountLabel(config, configPath, accountId, input, dependencies = {}) {
  return serializeConfigMutation(() => updateAccountLabelNow(config, configPath, accountId, input, dependencies));
}

async function updateAccountLabelNow(config, configPath, accountId, input, dependencies) {
  const label = cleanLabel(input);
  const account = config.accounts.find((item) => item.id === accountId && !item.historyOnly);
  if (!account) throw publicError("account_not_found", 404);

  const nextConfig = {
    ...config,
    accounts: config.accounts.map((item) => item === account ? { ...item, label } : item)
  };
  await persistSafely(configPath, nextConfig, dependencies.persistConfig);
  config.accounts = nextConfig.accounts;
  const updated = config.accounts.find((item) => item.id === accountId);
  return { account: (await listPublicAccounts({ ...config, accounts: [updated] }))[0], accounts: await listPublicAccounts(config) };
}

async function persistSafely(configPath, nextConfig, persistConfig = atomicWriteConfig) {
  try {
    await persistConfig(configPath, nextConfig);
  } catch {
    throw publicError("account_config_write_failed", 500);
  }
}

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

function slugify(label) {
  const value = label.toLowerCase().normalize("NFKD")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "")
    .slice(0, 40);
  return value || "account";
}

function isManagedAccount(config, account) {
  if (!config.dataDir || !account?.id || !account.codexHome) return false;
  const managedRoot = resolve(dirname(config.dataDir), "accounts", account.id, "codex-home");
  return resolve(account.codexHome).toLowerCase() === managedRoot.toLowerCase();
}

async function containsLocalHistory(codexHome, readDirectory) {
  return await containsJsonl(resolve(codexHome, "sessions"), readDirectory)
    || await containsJsonl(resolve(codexHome, "archived_sessions"), readDirectory);
}

async function containsJsonl(directory, readDirectory) {
  const entries = await safeReadDirectory(directory, readDirectory);
  for (const entry of entries) {
    if (entry.isFile() && entry.name.toLowerCase().endsWith(".jsonl")) return true;
    if (entry.isDirectory() && await containsJsonl(resolve(directory, entry.name), readDirectory)) return true;
  }
  return false;
}

async function safeReadDirectory(directory, readDirectory) {
  try {
    return await readDirectory(directory, { withFileTypes: true });
  } catch {
    return [];
  }
}

function normalizedPath(path) {
  return typeof path === "string" ? resolve(path).toLowerCase() : "";
}

function isManagedAccountId(value) {
  return /^[a-z0-9][a-z0-9-]{0,127}$/i.test(value);
}

function availableHistorySourceId(preferred, usedIds) {
  if (!usedIds.has(preferred)) return preferred;
  const base = `${preferred}-history`;
  if (!usedIds.has(base)) return base;
  let suffix = 2;
  while (usedIds.has(`${base}-${suffix}`)) suffix += 1;
  return `${base}-${suffix}`;
}

function publicPendingAccount(account) {
  return {
    id: account.id,
    label: account.label,
    status: "needs_login"
  };
}

function cleanLabel(input) {
  const label = typeof input?.label === "string" ? input.label.trim() : "";
  if (!label || label.length > 80) throw publicError("invalid_account_label", 400);
  return label;
}

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

async function ensureAccountHome(path, makeDirectory) {
  try {
    await makeDirectory(path, { recursive: true });
  } catch {
    throw publicError("account_home_create_failed", 500);
  }
}

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