import {
  writeFileSync,
  mkdirSync,
  readFileSync,
  unlinkSync,
  existsSync,
  renameSync,
  copyFileSync,
} from "node:fs";
import { join } from "node:path";
import { logger } from "./logger";
import { getDataDir } from "./dataDir";
import {
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
  RuntimeConfigSchema,
  type RuntimeConfig,
} from "../lib/runtimeConfig";

// Re-export so server-side callers (api/config.ts, tests) keep the
// existing import path `from "../../proxy/config"`.
export { RuntimeConfigSchema };
export type { RuntimeConfig };

/**
 * Runtime-mutable server config. Includes proxy behavior flags and UI tuning
 * settings that should take effect without restarting the dev server. The API
 * layer reads and writes the same object via `getConfig` / `setConfig`.
 *
 * Boot-time precedence (first match wins):
 *   1. `data/config.json` on disk (persisted state)
 *   2. `LLM_INSPECTOR_STRIP_CLAUDE_CODE_BILLING_HEADER` env var
 *   3. `false` (off by default, to keep the proxy transparent)
 *
 * The env var is read once at module init; changing it requires a restart.
 * Once the user has saved a value via the API, the persisted file takes
 * precedence over the env var for the lifetime of the process.
 */

// --- Persistence ---

function getConfigFilePath(): string {
  return join(getDataDir(), "config.json");
}

function getConfigTempPath(): string {
  return join(getDataDir(), "config.json.tmp");
}

// --- Module state ---

// Internal: writeable singleton. Read via getConfig(), mutate via setConfig().
let currentConfig: RuntimeConfig = resolveInitialConfig();

function resolveInitialConfig(): RuntimeConfig {
  // 1. Persisted file
  const filePath = getConfigFilePath();
  if (existsSync(filePath)) {
    try {
      const content = readFileSync(filePath, "utf-8");
      const parsed: unknown = JSON.parse(content);
      const result = RuntimeConfigSchema.safeParse(parsed);
      if (result.success) {
        logger.info(`[config] Loaded persisted config from ${filePath}`);
        return result.data;
      }
      logger.warn(
        `[config] Persisted config at ${filePath} is invalid; falling back to env/default`,
      );
    } catch (err) {
      logger.warn(
        `[config] Failed to read persisted config at ${filePath}: ${String(err)}; falling back to env/default`,
      );
    }
  }

  // 2. Env var
  if (process.env["LLM_INSPECTOR_STRIP_CLAUDE_CODE_BILLING_HEADER"] === "1") {
    return {
      stripClaudeCodeBillingHeader: true,
      hasSeenOnboarding: false,
      slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
    };
  }

  // 3. Default off
  return {
    stripClaudeCodeBillingHeader: false,
    hasSeenOnboarding: false,
    slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
  };
}

/**
 * Returns the current config. Synchronous; safe to call on the hot path.
 */
export function getConfig(): RuntimeConfig {
  return currentConfig;
}

/**
 * Applies a partial update to the in-memory config and persists to disk.
 * Returns the updated full config. Persistence failures are logged but
 * do not throw — the in-memory value is always updated so the caller
 * sees a consistent response.
 */
export function setConfig(patch: Partial<RuntimeConfig>): RuntimeConfig {
  const next: RuntimeConfig = {
    ...currentConfig,
    ...patch,
  };
  currentConfig = next;
  persistConfig(next);
  return next;
}

/**
 * For tests: reset the in-memory config to a known value without
 * touching disk. Use `setConfig` in production code.
 */
export function _resetConfigForTests(value: RuntimeConfig): void {
  currentConfig = value;
}

/**
 * For tests: re-run the boot-time resolution against the current env vars
 * and the on-disk config file, and update the in-memory value. Use this to
 * test env-var precedence without having to reload the module.
 */
export function _rebootForTests(): void {
  currentConfig = resolveInitialConfig();
}

// --- Atomic write ---

/**
 * Atomic write: temp file + rename. Mirrors the pattern in chunkStorage.ts.
 * Returns true on success, false on failure (logged but not thrown).
 */
function persistConfig(value: RuntimeConfig): boolean {
  const dir = getDataDir();
  const targetPath = getConfigFilePath();
  const tempPath = getConfigTempPath();

  try {
    mkdirSync(dir, { recursive: true });
  } catch (err) {
    logger.error(`[config] Failed to create data directory ${dir}: ${String(err)}`);
    return false;
  }

  try {
    writeFileSync(tempPath, JSON.stringify(value), "utf-8");
  } catch (err) {
    logger.error(`[config] Failed to write config temp file: ${String(err)}`);
    return false;
  }

  try {
    renameSync(tempPath, targetPath);
    return true;
  } catch (err) {
    logger.warn(`[config] Rename failed, falling back to copy+delete: ${String(err)}`);
    try {
      copyFileSync(tempPath, targetPath);
      unlinkSync(tempPath);
      return true;
    } catch (copyErr) {
      logger.error(`[config] Failed to copy config file: ${String(copyErr)}`);
      return false;
    }
  }
}
