import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { getConfigDir } from "../config";
import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
import type { OcxConfig } from "../types";

// ---------------------------------------------------------------------------
// Shell-hook env file: written on inject, sourced by the shell hook in .zshrc.
// This works for ALL new shells immediately, unlike launchctl setenv which only
// reaches processes launched directly by launchd (not Terminal.app children).
// ---------------------------------------------------------------------------

export function getShellEnvFilePath(): string {
  return join(getConfigDir(), "claude-env.sh");
}

function shellValue(value: string): string {
  return `'${value.replaceAll("'", `'\\''`)}'`;
}

function writeShellEnvFile(port: number, config: OcxConfig, modelEnv: Record<string, string> = {}, auto?: AutoContextMode): void {
  const lines = [
    `# Generated by opencodex — do not edit manually`,
    `export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`,
    `export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`,
  ];
  if (config.apiKeys?.length) {
    lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`);
  }
  // New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already
  // exported in their shell wins even though launchctl knows nothing about it.
  const conditional = (name: string, value: string) =>
    `[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`;
  // Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2).
  if (modelEnv.ANTHROPIC_MODEL) {
    lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`);
  } else if (config.claudeCode?.model) {
    lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`);
  }
  for (const [name, value] of Object.entries(modelEnv)) {
    if (name === "ANTHROPIC_MODEL") continue;
    lines.push(conditional(name, value));
  }
  const maxCtx = config.claudeCode?.maxContextTokens;
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
    lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx))));
    lines.push(conditional("DISABLE_COMPACT", "1"));
  }
  // Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl.
  const autoShell = auto ?? resolveAutoContext(config.claudeCode);
  if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow)));
  if (config.claudeCode?.alwaysEnableEffort === true) {
    lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
  }
  mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
  writeFileSync(getShellEnvFilePath(), lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
}

function removeShellEnvFile(): void {
  try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ }
}

// ---------------------------------------------------------------------------
// .zshrc hook auto-install: adds a one-liner that sources claude-env.sh.
// Idempotent — skips if the hook line already exists.
// ---------------------------------------------------------------------------

const SHELL_HOOK_MARKER = "# opencodex claude-env hook";
const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`;

export function installShellHook(): { installed: boolean; reason?: string } {
  if (process.platform !== "darwin") return { installed: false, reason: "not macOS" };
  const home = process.env.HOME;
  if (!home) return { installed: false, reason: "no HOME" };
  const zshrcPath = join(home, ".zshrc");
  try {
    let content = "";
    try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ }
    if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" };
    const addition = `\n${SHELL_HOOK_LINE}\n`;
    writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 });
    return { installed: true };
  } catch (err) {
    return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` };
  }
}

export function uninstallShellHook(): { removed: boolean; reason?: string } {
  if (process.platform !== "darwin") return { removed: false, reason: "not macOS" };
  const home = process.env.HOME;
  if (!home) return { removed: false, reason: "no HOME" };
  const zshrcPath = join(home, ".zshrc");
  try {
    const content = readFileSync(zshrcPath, "utf8");
    if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" };
    // Remove the hook block (marker line + source line + surrounding newlines)
    const cleaned = content.replace(/\n?# opencodex claude-env hook\n\[.*claude-env\.sh.*\n?/g, "\n");
    writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 });
    return { removed: true };
  } catch {
    return { removed: false, reason: "read/write failed" };
  }
}

const SYSTEM_ENV_NAMES = [
  "ANTHROPIC_BASE_URL",
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
  "ANTHROPIC_AUTH_TOKEN",
] as const;

interface SystemEnvTracking {
  pid: number;
  port: number;
  injectedAt: string;
  /** Keys that were actually set by injection (revert only unsets these). */
  injectedKeys?: string[];
}

type SystemEnvResult = { injected: boolean; reason?: string };
type RevertResult = { reverted: boolean; reason?: string };
type CleanupResult = { cleaned: boolean; reason?: string };

export function getSystemEnvTrackingPath(): string {
  return join(getConfigDir(), "system-env-port");
}

export function launchctlGetenv(name: string): string | undefined {
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return undefined;
  try {
    const value = execSync(`launchctl getenv ${name}`, { encoding: "utf8" }).trim();
    return value || undefined;
  } catch {
    return undefined;
  }
}

function readTracking(): SystemEnvTracking | undefined {
  try {
    const tracking = JSON.parse(readFileSync(getSystemEnvTrackingPath(), "utf8")) as Partial<SystemEnvTracking>;
    if (!Number.isInteger(tracking.port) || typeof tracking.pid !== "number" || typeof tracking.injectedAt !== "string") {
      return undefined;
    }
    return tracking as SystemEnvTracking;
  } catch {
    return undefined;
  }
}

function shellArg(value: string): string {
  return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
}

function setLaunchctlEnv(name: string, value: string): void {
  execSync(`launchctl setenv ${name} ${shellArg(value)}`);
}

function unsetLaunchctlEnv(name: string): void {
  execSync(`launchctl unsetenv ${name}`);
}

function ownedBaseUrl(port: number): string {
  return `http://127.0.0.1:${port}`;
}

function writeTracking(port: number, injectedKeys: string[]): void {
  mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
  writeFileSync(getSystemEnvTrackingPath(), JSON.stringify({
    pid: process.pid,
    port,
    injectedAt: new Date().toISOString(),
    injectedKeys,
  }), { encoding: "utf8", mode: 0o600 });
}

function rollbackInjectedKeys(port: number, injectedKeys: string[]): void {
  const rollbackFailed: string[] = [];
  for (const name of [...injectedKeys].reverse()) {
    try {
      unsetLaunchctlEnv(name);
    } catch {
      rollbackFailed.unshift(name);
    }
  }

  if (rollbackFailed.length > 0) {
    writeTracking(port, rollbackFailed);
    return;
  }

  try { unlinkSync(getSystemEnvTrackingPath()); } catch { /* already gone */ }
}

/**
 * In-process effective model-env (default + tier slots, [1m] applied) under the shared
 * 3s bound (audit R4#3). Returns {} on timeout/failure so injection degrades safely.
 */
async function computeEffectiveModelEnv(config: OcxConfig, auto?: AutoContextMode): Promise<{ modelEnv: Record<string, string>; windows: Record<string, number> }> {
  const { boundedContextWindows, buildClaudeContextWindows, effectiveModelEnv } = await import("../claude/context-windows");
  const windows = await boundedContextWindows(async () => {
    const { gatherRoutedModels, visibleNativeSlugs } = await import("../codex/catalog");
    return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config));
  });
  return { modelEnv: effectiveModelEnv(config.claudeCode, windows ?? {}, auto), windows: windows ?? {} };
}

export async function injectSystemEnv(port: number, config: OcxConfig): Promise<SystemEnvResult> {
  if (process.platform !== "darwin") return { injected: false, reason: "not macOS" };
  if (config.claudeCode?.enabled === false) return { injected: false, reason: "claude disabled" };

  // Default OFF — only inject when explicitly enabled by the user.
  if (config.claudeCode?.systemEnv !== true) return { injected: false, reason: "systemEnv disabled" };

  await cleanStaleSystemEnv();

  const currentBaseUrl = launchctlGetenv("ANTHROPIC_BASE_URL");
  if (currentBaseUrl && !/^http:\/\/127\.0\.0\.1:\d+$/.test(currentBaseUrl)) {
    return { injected: false, reason: "user has custom ANTHROPIC_BASE_URL" };
  }
  // After stale cleanup, if a tracking file still exists with a DIFFERENT port,
  // another live instance owns the env — don't overwrite it.
  const existingTracking = readTracking();
  if (existingTracking && existingTracking.port !== port) {
    return { injected: false, reason: `another instance owns env (port ${existingTracking.port})` };
  }

  const injectedKeys: string[] = existingTracking
    ? [...(existingTracking.injectedKeys ?? SYSTEM_ENV_NAMES)]
    : [];
  const inject = (name: string, value: string) => {
    setLaunchctlEnv(name, value);
    if (!injectedKeys.includes(name)) injectedKeys.push(name);
    writeTracking(port, injectedKeys);
  };

  try {
    inject("ANTHROPIC_BASE_URL", ownedBaseUrl(port));
    inject("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1");
    if (config.apiKeys?.length) {
      inject("ANTHROPIC_AUTH_TOKEN", config.apiKeys[0].key);
    }
    // Lever keys (devlog 136 B6): user-wins — skip any key the user already set in the
    // launchd domain, and track ONLY the keys we actually injected so revert cannot
    // delete a pre-existing user value (audit 139 #3).
    const injectLever = (name: string, value: string) => {
      if (launchctlGetenv(name) !== undefined) return;
      inject(name, value);
    };
    // Model slots (default + tier defaults + legacy small-fast) with [1m] auto-marking
    // (devlog 260712 B2, audit R2#3/R4#3): in-process context-window computation under
    // the same 3s bound; on timeout the tier keys are simply not injected this run.
    // Auto-context: a user-owned launchd value drives the marking predicate so the
    // marker and threshold never separate (audit 021 #2); injectLever's user-wins
    // check below keeps that value untouched.
    const userAutoCompact = launchctlGetenv("CLAUDE_CODE_AUTO_COMPACT_WINDOW");
    const auto = resolveAutoContext(config.claudeCode, userAutoCompact);
    const { modelEnv, windows } = await computeEffectiveModelEnv(config, auto);
    for (const [name, value] of Object.entries(modelEnv)) {
      if (name === "ANTHROPIC_MODEL") continue; // legacy slot handled by shell file only (back-compat)
      injectLever(name, value);
    }
    const maxCtx = config.claudeCode?.maxContextTokens;
    if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
      injectLever("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
      injectLever("DISABLE_COMPACT", "1");
    }
    // Auto-context (devlog 260712 020): user-wins lever, inert when maxContextTokens set.
    if (auto.enabled) injectLever("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(auto.compactWindow));
    if (config.claudeCode?.alwaysEnableEffort === true) {
      injectLever("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
    }

    // Shell-hook env file: works for new shells in already-running Terminal.app.
    writeShellEnvFile(port, config, modelEnv, auto);

    // Gateway-model cache pre-write (devlog 030): plain `claude` sessions read the
    // picker list from ~/.claude/cache/gateway-models.json and cannot refresh it
    // without a token — keep it in sync with this proxy's /v1/models. Best-effort.
    try {
      const { refreshGatewayModelCacheFromProxy } = await import("../claude/gateway-cache");
      await refreshGatewayModelCacheFromProxy(port);
    } catch { /* best-effort */ }

    // Roster agent definitions (devlog 070): same launch-time sync for plain `claude`.
    // Reuses the window map computed above (audit 071 #5 — no second acquisition).
    try {
      const { injectClaudeAgentDefs } = await import("../claude/agents-inject");
      injectClaudeAgentDefs(config, windows);
    } catch { /* best-effort */ }

    writeTracking(port, injectedKeys);
  } catch (error) {
    rollbackInjectedKeys(port, injectedKeys);
    removeShellEnvFile();
    console.error("Failed to inject system environment; rolled back launchctl changes:", error);
    throw error;
  }

  return { injected: true };
}

export async function applySystemEnvToggle(config: OcxConfig, port: number): Promise<SystemEnvResult | RevertResult> {
  if (config.claudeCode?.systemEnv === true) return injectSystemEnv(port, config);
  return revertSystemEnv();
}

export function revertSystemEnv(): RevertResult {
  if (process.platform !== "darwin") return { reverted: false, reason: "not macOS" };

  const tracking = readTracking();
  if (!tracking) return { reverted: false, reason: "no tracking file" };

  try {
    const tracksBaseUrl = tracking.injectedKeys?.includes("ANTHROPIC_BASE_URL") ?? true;
    if (tracksBaseUrl && launchctlGetenv("ANTHROPIC_BASE_URL") !== ownedBaseUrl(tracking.port)) {
      return { reverted: false, reason: "ownership mismatch" };
    }

    // Only unset keys that were actually injected (preserves pre-existing user tokens).
    const keysToUnset = tracking.injectedKeys ?? SYSTEM_ENV_NAMES as unknown as string[];
    for (const name of keysToUnset) {
      try {
        unsetLaunchctlEnv(name);
      } catch {
        // Continue removing the remaining variables during shutdown.
      }
    }
    removeShellEnvFile();
    try {
      unlinkSync(getSystemEnvTrackingPath());
    } catch {
      // The environment was reverted even if the tracking file disappeared concurrently.
    }
    return { reverted: true };
  } catch {
    return { reverted: false, reason: "revert failed" };
  }
}

export async function cleanStaleSystemEnv(): Promise<CleanupResult> {
  const tracking = readTracking();
  if (!tracking) return { cleaned: false, reason: "no tracking file" };

  try {
    const response = await fetch(`${ownedBaseUrl(tracking.port)}/healthz`, {
      signal: AbortSignal.timeout(1_000),
    });
    if (response.ok) return { cleaned: false, reason: "proxy still alive" };
  } catch {
    // A failed or timed-out health check means the tracked proxy is stale.
  }

  const reverted = revertSystemEnv();
  if (!reverted.reverted) return { cleaned: false, reason: reverted.reason };
  return { cleaned: true };
}
