/**
 * Settings store for the web dashboard:
 *  - lists Claude / Codex accounts (safe metadata only, no tokens)
 *  - reads/writes model role configuration via .env
 *  - removes account directories on the filesystem
 *
 * Account directory layout (existing convention):
 *   Claude default:   ~/.claude/.credentials.json
 *   Claude index N≥1: ~/.claude-accounts/{N}/.credentials.json
 *   Codex default:    ~/.codex/auth.json
 *   Codex index N≥1:  ~/.codex-accounts/{N}/auth.json
 */

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import {
  findCodexAccountIndexByAuthPath,
  getActiveCodexAuthPath,
  setCurrentCodexAccountIndex as setRotationIndex,
} from './codex-token-rotation.js';
import {
  normalizeCodexLiveStatus,
  toCodexLiveStatusSummary,
  type CodexLiveStatus,
  type CodexLiveStatusSummary,
} from './codex-live-status.js';
import {
  readCodexFeatureFromFile,
  writeCodexFeatureToFile,
} from './codex-config-features.js';
import { readHostClaudeFastMode } from './claude-session-settings.js';
import { agentTypeForRole, isEffortSupported } from './settings-effort.js';

export type {
  CodexAdditionalRateLimitSummary,
  CodexCreditsSummary,
  CodexLiveStatusSummary,
  CodexRateLimitSummary,
  CodexRateLimitWindowSummary,
  CodexSpendControlSummary,
} from './codex-live-status.js';

export interface ClaudeAccountSummary {
  index: number;
  expiresAt: number | null;
  scopes: string[];
  subscriptionType?: string;
  rateLimitTier?: string;
  exists: boolean;
}

export interface CodexAccountSummary {
  index: number;
  accountId: string | null;
  email: string | null;
  planType: string | null;
  subscriptionUntil: string | null;
  subscriptionLastChecked: string | null;
  subscriptionSource: 'jwt-cache' | null;
  liveStatus: CodexLiveStatusSummary | null;
  exists: boolean;
}

export interface ModelRoleConfig {
  model: string;
  effort: string;
}

export interface ModelAgentTypes {
  owner: 'claude-code' | 'codex';
  reviewer: 'claude-code' | 'codex';
  arbiter: 'claude-code' | 'codex' | null;
}

export interface ModelConfigSnapshot {
  owner: ModelRoleConfig;
  reviewer: ModelRoleConfig;
  arbiter: ModelRoleConfig;
  agentTypes: ModelAgentTypes;
}

export interface FastModeSnapshot {
  codex: boolean;
  claude: boolean;
}

export interface CodexFeatureSnapshot {
  goals: boolean;
}

const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;

function envFilePath(): string {
  return path.join(process.cwd(), '.env');
}

function readJson<T>(file: string): T | null {
  try {
    if (!fs.existsSync(file)) return null;
    return JSON.parse(fs.readFileSync(file, 'utf-8')) as T;
  } catch {
    return null;
  }
}

function settingsHomeDir(): string {
  return process.env.EJCLAW_SETTINGS_HOME || os.homedir();
}

function claudeCredsPath(index: number): string {
  if (index === 0) {
    return path.join(settingsHomeDir(), '.claude', '.credentials.json');
  }
  return path.join(
    settingsHomeDir(),
    '.claude-accounts',
    String(index),
    '.credentials.json',
  );
}

function codexAuthPath(index: number): string {
  if (index === 0) {
    return path.join(settingsHomeDir(), '.codex', 'auth.json');
  }
  return path.join(
    settingsHomeDir(),
    '.codex-accounts',
    String(index),
    'auth.json',
  );
}

function readClaudeAccount(index: number): ClaudeAccountSummary | null {
  const file = claudeCredsPath(index);
  if (!fs.existsSync(file)) return null;
  const data = readJson<{ claudeAiOauth?: Record<string, unknown> }>(file);
  const oauth = data?.claudeAiOauth ?? {};
  return {
    index,
    expiresAt: typeof oauth.expiresAt === 'number' ? oauth.expiresAt : null,
    scopes: Array.isArray(oauth.scopes) ? (oauth.scopes as string[]) : [],
    subscriptionType:
      typeof oauth.subscriptionType === 'string'
        ? oauth.subscriptionType
        : undefined,
    rateLimitTier:
      typeof oauth.rateLimitTier === 'string' ? oauth.rateLimitTier : undefined,
    exists: true,
  };
}

function readCodexAccount(index: number): CodexAccountSummary | null {
  const file = codexAuthPath(index);
  if (!fs.existsSync(file)) return null;
  const data = readJson<{
    OPENAI_API_KEY?: string;
    tokens?: { id_token?: string; access_token?: string };
  }>(file);
  const live = readCodexLiveStatus(index);
  let accountId: string | null = null;
  let email: string | null = null;
  let planType: string | null = null;
  let subscriptionUntil: string | null = null;
  let subscriptionLastChecked: string | null = null;
  let subscriptionSource: 'jwt-cache' | null = null;
  let liveStatus: CodexLiveStatusSummary | null = null;
  const idToken = data?.tokens?.id_token;
  if (idToken && typeof idToken === 'string') {
    const parts = idToken.split('.');
    if (parts.length >= 2) {
      try {
        const payload = JSON.parse(
          Buffer.from(parts[1], 'base64').toString('utf-8'),
        ) as Record<string, unknown>;
        if (typeof payload.sub === 'string') accountId = payload.sub;
        if (typeof payload.email === 'string') email = payload.email;
        const auth = payload['https://api.openai.com/auth'] as
          | Record<string, unknown>
          | undefined;
        if (auth) {
          if (typeof auth.chatgpt_plan_type === 'string') {
            planType = auth.chatgpt_plan_type;
          }
          if (typeof auth.chatgpt_subscription_active_until === 'string') {
            subscriptionUntil = auth.chatgpt_subscription_active_until;
            subscriptionSource = 'jwt-cache';
          }
          if (typeof auth.chatgpt_subscription_last_checked === 'string') {
            subscriptionLastChecked = auth.chatgpt_subscription_last_checked;
          }
        }
      } catch {
        /* ignore parse errors */
      }
    }
  }
  // Live wham/usage data, written by refreshCodexAccount, takes precedence.
  // When live status exists, do not surface the stale JWT active_until claim as
  // a real billing expiry. OpenAI/Auth0 can cache that claim for days.
  if (live) {
    liveStatus = toCodexLiveStatusSummary(live);
    if (typeof live.plan_type === 'string') planType = live.plan_type;
    if (typeof live.email === 'string') email = live.email;
    subscriptionLastChecked = live.checked_at;
    subscriptionUntil = null;
    subscriptionSource = null;
  }
  return {
    index,
    accountId,
    email,
    planType,
    subscriptionUntil,
    subscriptionLastChecked,
    subscriptionSource,
    liveStatus,
    exists: true,
  };
}

export function listClaudeAccounts(): ClaudeAccountSummary[] {
  const out: ClaudeAccountSummary[] = [];
  const def = readClaudeAccount(0);
  if (def) out.push(def);
  const dir = path.join(settingsHomeDir(), '.claude-accounts');
  if (fs.existsSync(dir)) {
    for (const i of readAccountIndices(dir)) {
      const acc = readClaudeAccount(i);
      if (acc) out.push(acc);
    }
  }
  return out;
}

export function listCodexAccounts(): CodexAccountSummary[] {
  const out: CodexAccountSummary[] = [];
  const def = readCodexAccount(0);
  if (def) out.push(def);
  const dir = path.join(settingsHomeDir(), '.codex-accounts');
  if (fs.existsSync(dir)) {
    for (const i of readAccountIndices(dir)) {
      const acc = readCodexAccount(i);
      if (acc) out.push(acc);
    }
  }
  return out;
}

function readAccountIndices(dir: string): number[] {
  const indices: number[] = [];
  for (const entry of fs.readdirSync(dir)) {
    if (!/^\d+$/.test(entry)) continue;
    const index = Number.parseInt(entry, 10);
    if (Number.isFinite(index) && index >= 1) indices.push(index);
  }
  return indices.sort((a, b) => a - b);
}

function pickEnvValue(content: string, key: string): string | undefined {
  const re = new RegExp(`^${key}=(.*)$`, 'm');
  const match = content.match(re);
  if (!match) return undefined;
  return match[1].trim().replace(/^['"]|['"]$/g, '');
}

function readEnvFile(): string {
  const file = envFilePath();
  if (!fs.existsSync(file)) return '';
  return fs.readFileSync(file, 'utf-8');
}

function readEnvOrProcess(key: string): string | undefined {
  const fromFile = pickEnvValue(readEnvFile(), key);
  if (fromFile !== undefined) return fromFile;
  const fromProc = process.env[key];
  return fromProc;
}

export function getModelConfig(): ModelConfigSnapshot {
  const env = readEnvFile();
  const envLookup = (key: string): string | undefined => {
    const fromFile = pickEnvValue(env, key);
    if (fromFile !== undefined) return fromFile;
    return process.env[key];
  };
  const out: Partial<ModelConfigSnapshot> = {
    agentTypes: {
      owner: agentTypeForRole('owner', envLookup) ?? 'codex',
      reviewer: agentTypeForRole('reviewer', envLookup) ?? 'claude-code',
      arbiter: agentTypeForRole('arbiter', envLookup),
    },
  };
  for (const role of ROLE_KEYS) {
    const model = readEnvOrProcess(`${role}_MODEL`) ?? '';
    const effort = readEnvOrProcess(`${role}_EFFORT`) ?? '';
    (out as Record<string, ModelRoleConfig>)[role.toLowerCase()] = {
      model,
      effort,
    };
  }
  return out as ModelConfigSnapshot;
}

export interface ModelUpdateInput {
  owner?: Partial<ModelRoleConfig>;
  reviewer?: Partial<ModelRoleConfig>;
  arbiter?: Partial<ModelRoleConfig>;
}

function setOrInsertEnvLine(
  content: string,
  key: string,
  value: string,
): string {
  const re = new RegExp(`^${key}=.*$`, 'm');
  if (re.test(content)) {
    return content.replace(re, `${key}=${value}`);
  }
  const trimmed = content.replace(/\s*$/, '');
  return `${trimmed}\n${key}=${value}\n`;
}

export function updateModelConfig(
  input: ModelUpdateInput,
): ModelConfigSnapshot {
  const file = envFilePath();
  let content = '';
  if (fs.existsSync(file)) {
    content = fs.readFileSync(file, 'utf-8');
  }
  const envLookup = (key: string): string | undefined =>
    pickEnvValue(content, key) ?? process.env[key];

  for (const role of ROLE_KEYS) {
    const update = (
      input as Record<string, Partial<ModelRoleConfig> | undefined>
    )[role.toLowerCase()];
    if (!update) continue;
    if (update.model !== undefined) {
      content = setOrInsertEnvLine(content, `${role}_MODEL`, update.model);
    }
    if (update.effort !== undefined) {
      const agentType = agentTypeForRole(
        role.toLowerCase() as 'owner' | 'reviewer' | 'arbiter',
        envLookup,
      );
      if (
        update.effort &&
        agentType &&
        !isEffortSupported(agentType, update.effort)
      ) {
        throw new Error(
          `${role}_EFFORT=${update.effort} is not supported for ${agentType} agents`,
        );
      }
      content = setOrInsertEnvLine(content, `${role}_EFFORT`, update.effort);
    }
  }

  const tempPath = `${file}.tmp`;
  fs.writeFileSync(tempPath, content, { mode: 0o600 });
  fs.renameSync(tempPath, file);

  return getModelConfig();
}

const CODEX_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token';
const CODEX_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';

interface CodexAuthFile {
  auth_mode?: string;
  OPENAI_API_KEY?: string | null;
  tokens?: {
    id_token?: string;
    access_token?: string;
    refresh_token?: string;
    account_id?: string;
  };
  last_refresh?: string;
}

function planStatusPath(index: number): string {
  if (index === 0) {
    return path.join(settingsHomeDir(), '.codex', 'plan-status.json');
  }
  return path.join(
    settingsHomeDir(),
    '.codex-accounts',
    String(index),
    'plan-status.json',
  );
}

function readCodexLiveStatus(index: number): CodexLiveStatus | null {
  return readJson<CodexLiveStatus>(planStatusPath(index));
}

function writeCodexLiveStatus(index: number, status: CodexLiveStatus): void {
  const file = planStatusPath(index);
  fs.mkdirSync(path.dirname(file), { recursive: true });
  const tempPath = `${file}.tmp`;
  fs.writeFileSync(tempPath, `${JSON.stringify(status, null, 2)}\n`, {
    mode: 0o600,
  });
  fs.renameSync(tempPath, file);
}

async function fetchCodexLiveUsage(
  accessToken: string,
): Promise<CodexLiveStatus | null> {
  try {
    const res = await fetch(CODEX_USAGE_URL, {
      method: 'GET',
      headers: { authorization: `Bearer ${accessToken}` },
    });
    if (!res.ok) return null;
    const json = (await res.json()) as unknown;
    return normalizeCodexLiveStatus(json, new Date().toISOString());
  } catch {
    return null;
  }
}

function readCodexAuthFile(file: string): CodexAuthFile | null {
  return readJson<CodexAuthFile>(file);
}

function writeCodexAuthFile(file: string, data: CodexAuthFile): void {
  const tempPath = `${file}.tmp`;
  fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
    mode: 0o600,
  });
  fs.renameSync(tempPath, file);
}

export async function refreshCodexAccount(
  index: number,
): Promise<CodexAccountSummary> {
  const file = codexAuthPath(index);
  if (!fs.existsSync(file)) {
    throw new Error(`codex auth.json not found for index ${index}`);
  }
  const data = readCodexAuthFile(file);
  const refreshToken = data?.tokens?.refresh_token;
  if (!refreshToken) {
    throw new Error(`codex account #${index} has no refresh_token`);
  }

  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: CODEX_OAUTH_CLIENT_ID,
  });

  const res = await fetch(CODEX_OAUTH_TOKEN_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: body.toString(),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(
      `codex refresh failed (#${index}): ${res.status} ${text.slice(0, 200)}`,
    );
  }

  const payload = (await res.json()) as {
    access_token?: string;
    id_token?: string;
    refresh_token?: string;
  };

  const updated: CodexAuthFile = {
    ...(data ?? {}),
    tokens: {
      ...(data?.tokens ?? {}),
      id_token: payload.id_token ?? data?.tokens?.id_token,
      access_token: payload.access_token ?? data?.tokens?.access_token,
      refresh_token: payload.refresh_token ?? refreshToken,
    },
    last_refresh: new Date().toISOString(),
  };
  writeCodexAuthFile(file, updated);

  // Hit chatgpt.com/backend-api/wham/usage to read the live plan, rate limit,
  // and spend-control state. The JWT id_token's chatgpt_plan_type /
  // *_active_until / *_last_checked claims are cached on OpenAI's Auth0 side and
  // don't refresh on every OAuth refresh_token grant, so the dashboard must not
  // present that cached expiry as live billing truth.
  const accessToken =
    payload.access_token ?? data?.tokens?.access_token ?? null;
  if (accessToken) {
    const live = await fetchCodexLiveUsage(accessToken);
    if (live) {
      writeCodexLiveStatus(index, live);
    }
  }

  const summary = readCodexAccount(index);
  if (!summary)
    throw new Error('failed to re-read codex account after refresh');
  return summary;
}

/**
 * settings-store lists codex accounts in an order that includes the default
 * `~/.codex/auth.json` as index 0 plus `~/.codex-accounts/{N}` as index N.
 * The rotation array (codex-token-rotation) only loads `~/.codex-accounts/{N}`
 * when those dirs exist (it ignores `~/.codex/auth.json` in that mode), so its
 * array indices are off-by-one vs. the settings indices. Translate via path.
 */
export function getActiveCodexSettingsIndex(): number | null {
  const activePath = getActiveCodexAuthPath();
  if (!activePath) return null;
  // Walk the same listing order as listCodexAccounts() to find the matching
  // settings-store index.
  if (codexAuthPath(0) === activePath && fs.existsSync(activePath)) {
    return 0;
  }
  const dir = path.join(settingsHomeDir(), '.codex-accounts');
  if (fs.existsSync(dir)) {
    for (const i of readAccountIndices(dir)) {
      if (codexAuthPath(i) === activePath) return i;
    }
  }
  return null;
}

export function setActiveCodexSettingsIndex(settingsIndex: number): void {
  const file = codexAuthPath(settingsIndex);
  if (!fs.existsSync(file)) {
    throw new Error(
      `codex auth.json not found for settings index ${settingsIndex}`,
    );
  }
  const rotationIndex = findCodexAccountIndexByAuthPath(file);
  if (rotationIndex === null) {
    throw new Error(
      `codex switch: settings #${settingsIndex} (${file}) is not part of the rotation pool. ` +
        `Rotation only manages accounts under ~/.codex-accounts/. ` +
        `If you want to use ~/.codex/auth.json directly, remove the ~/.codex-accounts dir first.`,
    );
  }
  setRotationIndex(rotationIndex);
}

export async function refreshAllCodexAccounts(): Promise<{
  refreshed: number[];
  failed: Array<{ index: number; error: string }>;
}> {
  const refreshed: number[] = [];
  const failed: Array<{ index: number; error: string }> = [];
  const accounts = listCodexAccounts();
  for (const acc of accounts) {
    try {
      await refreshCodexAccount(acc.index);
      refreshed.push(acc.index);
    } catch (err) {
      failed.push({
        index: acc.index,
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }
  return { refreshed, failed };
}

const CODEX_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
let codexRefreshTimer: ReturnType<typeof setInterval> | null = null;

export function startCodexAccountRefreshLoop(): void {
  if (codexRefreshTimer) return;
  // Stagger first refresh so server boot isn't slowed.
  setTimeout(() => {
    void refreshAllCodexAccounts().catch(() => {
      /* swallow; per-account errors logged inside */
    });
  }, 60_000).unref?.();
  codexRefreshTimer = setInterval(() => {
    void refreshAllCodexAccounts().catch(() => {
      /* swallow */
    });
  }, CODEX_REFRESH_INTERVAL_MS);
  codexRefreshTimer.unref?.();
}

export function stopCodexAccountRefreshLoop(): void {
  if (codexRefreshTimer) {
    clearInterval(codexRefreshTimer);
    codexRefreshTimer = null;
  }
}

function codexConfigFile(): string {
  return path.join(settingsHomeDir(), '.codex', 'config.toml');
}

function claudeSettingsPath(): string {
  return path.join(settingsHomeDir(), '.claude', 'settings.json');
}

function readCodexFastMode(): boolean {
  return readCodexFeatureFromFile(codexConfigFile(), 'fast_mode');
}

function writeCodexFastMode(value: boolean): void {
  writeCodexFeatureToFile(codexConfigFile(), 'fast_mode', value);
}

function readClaudeFastMode(): boolean {
  return readHostClaudeFastMode();
}

function writeClaudeFastMode(value: boolean): void {
  const file = claudeSettingsPath();
  fs.mkdirSync(path.dirname(file), { recursive: true });
  let data: Record<string, unknown> = {};
  if (fs.existsSync(file)) {
    try {
      data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
        string,
        unknown
      >;
    } catch {
      data = {};
    }
  }
  data.fastMode = value;
  const tempPath = `${file}.tmp`;
  fs.writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
    mode: 0o600,
  });
  fs.renameSync(tempPath, file);
}

export function getFastMode(): FastModeSnapshot {
  return {
    codex: readCodexFastMode(),
    claude: readClaudeFastMode(),
  };
}

export function updateFastMode(
  input: Partial<FastModeSnapshot>,
): FastModeSnapshot {
  if (typeof input.codex === 'boolean') writeCodexFastMode(input.codex);
  if (typeof input.claude === 'boolean') writeClaudeFastMode(input.claude);
  return getFastMode();
}

function readCodexGoals(): boolean {
  const fromConfig = readCodexFeatureFromFile(codexConfigFile(), 'goals');
  if (fromConfig) return true;
  // Legacy .env opt-in kept for backward compatibility until migrated.
  return readEnvOrProcess('CODEX_GOALS') === 'true';
}

function writeCodexGoals(value: boolean): void {
  writeCodexFeatureToFile(codexConfigFile(), 'goals', value);

  const file = envFilePath();
  if (!fs.existsSync(file)) return;
  const content = fs.readFileSync(file, 'utf-8');
  if (!/^CODEX_GOALS=.*$/m.test(content)) return;
  const updated = content
    .split('\n')
    .filter((line) => !/^CODEX_GOALS=.*$/.test(line))
    .join('\n')
    .replace(/\n+$/, '');
  const tempPath = `${file}.tmp`;
  fs.writeFileSync(tempPath, updated ? `${updated}\n` : '', { mode: 0o600 });
  fs.renameSync(tempPath, file);
}

export function getCodexFeatures(): CodexFeatureSnapshot {
  return {
    goals: readCodexGoals(),
  };
}

export function updateCodexFeatures(
  input: Partial<CodexFeatureSnapshot>,
): CodexFeatureSnapshot {
  if (typeof input.goals === 'boolean') writeCodexGoals(input.goals);
  return getCodexFeatures();
}

export function removeAccountDirectory(
  provider: 'claude' | 'codex',
  index: number,
): void {
  if (!Number.isFinite(index) || index < 1) {
    throw new Error('cannot remove default account (index 0)');
  }
  const baseDir =
    provider === 'claude'
      ? path.join(settingsHomeDir(), '.claude-accounts', String(index))
      : path.join(settingsHomeDir(), '.codex-accounts', String(index));
  if (!fs.existsSync(baseDir)) {
    throw new Error(`account directory not found: ${baseDir}`);
  }
  fs.rmSync(baseDir, { recursive: true, force: true });
}

export function addClaudeAccountFromToken(token: string): {
  index: number;
  accountId: string | null;
} {
  const trimmed = token.trim();
  if (!trimmed) throw new Error('empty token');

  const parts = trimmed.split('.');
  if (parts.length < 2) {
    throw new Error('invalid OAuth token: expected JWT format');
  }
  let payload: Record<string, unknown>;
  try {
    payload = JSON.parse(
      Buffer.from(parts[1], 'base64').toString('utf-8'),
    ) as Record<string, unknown>;
  } catch {
    throw new Error('invalid OAuth token: payload not parseable');
  }
  const exp = typeof payload.exp === 'number' ? payload.exp * 1000 : 0;
  const accountId = typeof payload.sub === 'string' ? payload.sub : null;

  const baseDir = path.join(settingsHomeDir(), '.claude-accounts');
  if (!fs.existsSync(baseDir)) {
    fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 });
  }

  let nextIndex = 1;
  while (
    fs.existsSync(path.join(baseDir, String(nextIndex), '.credentials.json'))
  ) {
    nextIndex += 1;
  }
  const dir = path.join(baseDir, String(nextIndex));
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });

  const creds = {
    claudeAiOauth: {
      accessToken: trimmed,
      refreshToken: '',
      expiresAt: exp,
      scopes: ['user:profile', 'user:inference'],
    },
  };
  const credsPath = path.join(dir, '.credentials.json');
  const tempPath = `${credsPath}.tmp`;
  fs.writeFileSync(tempPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
  fs.renameSync(tempPath, credsPath);

  return { index: nextIndex, accountId };
}
