import { appendFileSync, mkdirSync } from "fs";
import { dirname, join } from "path";
import { getConfigDir, readConfig } from "./config.mjs";
import { discoverCodexProviderModels } from "./codex-model-discovery.mjs";
import {
  PROVIDER_MODEL_REFRESH_INTERVAL_MS,
  readProviderModelParams,
  writeProviderModelParams,
} from "./provider-model-params.mjs";

const DEFAULT_STARTUP_DELAY_MS = 5 * 60 * 1000;
const MAX_ERROR_LENGTH = 500;
const PROVIDER_MODEL_REFRESH_OPERATION_LOG_NAME =
  "provider_model_refresh.jsonl";

function truncateError(err) {
  const message = err?.message || String(err);
  return message.length > MAX_ERROR_LENGTH
    ? `${message.slice(0, MAX_ERROR_LENGTH)}...`
    : message;
}

function sanitizeOperationLogError(err) {
  return truncateError(err)
    .replace(/\b(token|password|credential|secret|api[_-]?key)(\s*[=:]\s*)[^\s,;]+/gi, "$1$2<redacted>")
    .replace(/(?:\/[^\s'",;:]+)+/g, "<path_redacted>");
}

function operationLogTimestamp(nowMs) {
  return new Date(nowMs).toISOString();
}

function durationMs(startedMs, finishedMs) {
  return Math.max(0, finishedMs - startedMs);
}

export function getProviderModelRefreshOperationLogPath(
  configDir = getConfigDir()
) {
  return join(
    configDir,
    "operation_logs",
    PROVIDER_MODEL_REFRESH_OPERATION_LOG_NAME
  );
}

function appendProviderModelRefreshOperationLog(row, options = {}) {
  const configDir = options.configDir || getConfigDir();
  const logPath =
    options.operationLogPath ||
    getProviderModelRefreshOperationLogPath(configDir);
  mkdirSync(dirname(logPath), { recursive: true });
  appendFileSync(logPath, `${JSON.stringify(row)}\n`, "utf-8");
  return logPath;
}

function parseTime(value) {
  if (typeof value !== "string" || !value.trim()) return null;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? ms : null;
}

export class ProviderModelRefreshRunner {
  constructor(options = {}) {
    this.configDir = options.configDir || null;
    this.readConfigFn = options.readConfigFn || readConfig;
    this.readParamsFn = options.readParamsFn || readProviderModelParams;
    this.writeParamsFn = options.writeParamsFn || writeProviderModelParams;
    this.appendOperationLogFn =
      options.appendOperationLogFn || appendProviderModelRefreshOperationLog;
    this.discoverCodexProviderModelsFn =
      options.discoverCodexProviderModelsFn || discoverCodexProviderModels;
    this.setTimeoutFn = options.setTimeoutFn || setTimeout;
    this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
    this.nowFn = options.nowFn || Date.now;
    this.startupDelayMs = Number.isInteger(options.startupDelayMs)
      ? options.startupDelayMs
      : DEFAULT_STARTUP_DELAY_MS;
    this.refreshIntervalMs = Number.isInteger(options.refreshIntervalMs)
      ? options.refreshIntervalMs
      : PROVIDER_MODEL_REFRESH_INTERVAL_MS;
    this.logger = options.logger || console;
    this.timer = null;
    this.started = false;
    this.running = false;
  }

  start() {
    if (this.started) return;
    this.started = true;
    this.scheduleNext({ startup: true });
  }

  stop() {
    this.started = false;
    if (this.timer) {
      this.clearTimeoutFn(this.timer);
      this.timer = null;
    }
  }

  computeNextDelay({ startup = false } = {}) {
    if (startup) {
      return Math.max(0, this.startupDelayMs);
    }
    try {
      const result = this.readParamsFn({ configDir: this.configDir });
      const nextRefreshAt = parseTime(result.params.refresh?.next_refresh_at);
      if (nextRefreshAt && nextRefreshAt > this.nowFn()) {
        return nextRefreshAt - this.nowFn();
      }
    } catch {
      // Bad params should not create a timer storm; use the normal interval.
    }
    return Math.max(0, this.refreshIntervalMs);
  }

  scheduleNext(options = {}) {
    if (!this.started || this.timer) return;
    const delayMs = this.computeNextDelay(options);
    this.timer = this.setTimeoutFn(async () => {
      this.timer = null;
      try {
        await this.runOnce();
      } finally {
        this.scheduleNext();
      }
    }, delayMs);
    if (typeof this.timer?.unref === "function") {
      this.timer.unref();
    }
  }

  async runOnce() {
    const startedMs = this.nowFn();
    const startedAt = operationLogTimestamp(startedMs);
    if (this.running) {
      const finishedMs = this.nowFn();
      this.appendOperationLogFn(
        {
          schema_version: 1,
          operation: "provider_model_refresh",
          provider: "codex",
          status: "skipped",
          reason: "refresh_already_running",
          started_at: startedAt,
          finished_at: operationLogTimestamp(finishedMs),
          duration_ms: durationMs(startedMs, finishedMs),
        },
        { configDir: this.configDir || getConfigDir() }
      );
      return { status: "skipped", reason: "refresh_already_running" };
    }
    this.running = true;
    const attemptedAt = startedAt;
    try {
      const config = this.readConfigFn();
      const discovery = await this.discoverCodexProviderModelsFn({
        config,
        configDir: this.configDir,
      });
      const nextRefreshAt = new Date(
        this.nowFn() + this.refreshIntervalMs
      ).toISOString();
      const writeResult = this.writeParamsFn(
        {
          provider_models: {
            codex: {
              source: "codex_app_server_model_list",
              refreshed_at: attemptedAt,
              models: discovery.models,
            },
          },
          provider_runtime: {
            codex: {
              command_available: discovery.command_available === true,
              configured_command_source: "config",
              app_server_discovery_supported:
                discovery.app_server_discovery_supported === true,
              probe_status: "success",
              last_attempt_at: attemptedAt,
              last_success_at: attemptedAt,
              last_error: null,
            },
          },
          refresh: {
            last_attempt_at: attemptedAt,
            last_success_at: attemptedAt,
            next_refresh_at: nextRefreshAt,
          },
        },
        { configDir: this.configDir }
      );
      const finishedMs = this.nowFn();
      this.appendOperationLogFn(
        {
          schema_version: 1,
          operation: "provider_model_refresh",
          provider: "codex",
          status: "success",
          started_at: attemptedAt,
          finished_at: operationLogTimestamp(finishedMs),
          duration_ms: durationMs(startedMs, finishedMs),
          model_count: Array.isArray(discovery.models)
            ? discovery.models.length
            : 0,
          last_success_at: attemptedAt,
          next_refresh_at: nextRefreshAt,
          params_path: writeResult.path,
          error: null,
        },
        { configDir: this.configDir || getConfigDir() }
      );
      return { status: "refreshed", params: writeResult.params };
    } catch (err) {
      const nextRefreshAt = new Date(
        this.nowFn() + this.refreshIntervalMs
      ).toISOString();
      const writeResult = this.writeParamsFn(
        {
          provider_runtime: {
            codex: {
              command_available: false,
              configured_command_source: "config",
              app_server_discovery_supported: false,
              probe_status: "error",
              last_attempt_at: attemptedAt,
              last_error: truncateError(err),
            },
          },
          refresh: {
            last_attempt_at: attemptedAt,
            next_refresh_at: nextRefreshAt,
          },
        },
        { configDir: this.configDir }
      );
      const finishedMs = this.nowFn();
      this.appendOperationLogFn(
        {
          schema_version: 1,
          operation: "provider_model_refresh",
          provider: "codex",
          status: "error",
          started_at: attemptedAt,
          finished_at: operationLogTimestamp(finishedMs),
          duration_ms: durationMs(startedMs, finishedMs),
          model_count: 0,
          next_refresh_at: nextRefreshAt,
          params_path: writeResult.path,
          error: sanitizeOperationLogError(err),
        },
        { configDir: this.configDir || getConfigDir() }
      );
      if (this.logger?.warn) {
        this.logger.warn(
          `[provider-model-refresh] Codex model refresh failed: ${truncateError(
            err
          )}`
        );
      }
      return { status: "error", error: truncateError(err) };
    } finally {
      this.running = false;
    }
  }
}
