/**
 * AI Key Health Check Worker
 *
 * Periodically verifies every configured AI provider API key (Claude/Anthropic,
 * Zhipu/GLM, Ollama, OpenAI — plus any per-key model/url) by sending a tiny probe
 * request through the real provider client. Based on the result it maintains an
 * automated health status on each key:
 *
 *   - A key the probe proves unusable — invalid, revoked, unauthorized, or without
 *     access (401/403) — is marked `health: 'inactive'`.
 *   - A key whose probe fails for a temporary reason (rate limit, quota, 5xx, timeout,
 *     unreachable endpoint) KEEPS its current status: the probe learned nothing about
 *     the key's validity. The error and failure count are still recorded so the Super
 *     Admin panel shows why the last probe was inconclusive.
 *   - A previously-inactive key that starts responding again is flipped back to
 *     `health: 'active'`.
 *   - The latest result (status, timestamp, latency, error, consecutive failures) is
 *     stored on the key entry in the database.
 *
 * The generation selector (utils/aiProvider) only ever uses keys whose health is not
 * 'inactive', so no manual intervention is required — the platform always reaches for
 * a valid, active key, and reports a clear error when a provider has none.
 *
 * Keys live embedded in the User document: the global config on the super-admin
 * (`panelSettings.aiConfig`) and per-user overrides (`userSettings.aiConfig` when
 * `useGlobalAIConfig === false`).
 */

import { getModels } from '../models';
import { notificationService } from '../services/notificationService';
import {
  APIKeyEntry,
  migrateToKeysArray,
  probeKeyHealth,
  invalidateAICache,
} from '../utils/aiProvider';

type ProviderId = 'claude' | 'zhipu' | 'ollama' | 'openai';

const PROVIDERS: Array<{ provider: ProviderId; field: string }> = [
  { provider: 'claude', field: 'claudeKeys' },
  { provider: 'zhipu', field: 'zhipuKeys' },
  { provider: 'ollama', field: 'ollamaKeys' },
  { provider: 'openai', field: 'openaiKeys' },
];

interface HealthWorkerConfig {
  /** How often to run the health check, in ms. */
  intervalMs: number;
  /** Delay before the first (warm-start) run after boot, in ms. */
  warmupMs: number;
  /** Max number of per-user (non-global) configs to check per tick. */
  maxUsers: number;
  verbose: boolean;
}

const DEFAULT_CONFIG: HealthWorkerConfig = {
  intervalMs: parseInt(process.env.AI_KEY_HEALTHCHECK_INTERVAL_MS || '3600000', 10) || 3600000, // 1 hour
  warmupMs: parseInt(process.env.AI_KEY_HEALTHCHECK_WARMUP_MS || '30000', 10) || 30000, // 30s
  maxUsers: parseInt(process.env.AI_KEY_HEALTHCHECK_MAX_USERS || '50', 10) || 50,
  verbose: process.env.NODE_ENV === 'development',
};

export interface HealthRunSummary {
  at: string;
  durationMs: number;
  keysChecked: number;
  active: number;
  inactive: number;
  configs: number;
  transitions: string[];
  error?: string;
}

function last4(key: string): string {
  return '****' + (key || '').slice(-4);
}

/**
 * Probe every key in one aiConfig object (mutating each entry's health fields in
 * place) and return a per-config summary. `userId` is passed to the provider clients
 * so per-user base URLs/config resolve correctly.
 */
async function checkConfig(cfg: any, userId?: string): Promise<{ checked: number; active: number; inactive: number; transitions: string[] }> {
  let checked = 0;
  let active = 0;
  let inactive = 0;
  const transitions: string[] = [];

  // Probe providers in parallel; keys within a provider are probed sequentially
  // to avoid hammering a single provider with concurrent requests.
  await Promise.all(
    PROVIDERS.map(async ({ provider, field }) => {
      const keys: APIKeyEntry[] = Array.isArray(cfg?.[field]) ? cfg[field] : [];
      for (const key of keys) {
        if (!key || !key.key || !key.key.trim()) continue;
        const prev = key.health;
        const res = await probeKeyHealth(provider, key, userId);

        key.lastCheckedAt = new Date().toISOString();
        key.lastLatencyMs = res.latencyMs;
        if (res.ok) {
          key.health = 'active';
          key.lastError = '';
          key.consecutiveFailures = 0;
        } else if (res.classification === 'key') {
          // Proven unusable — invalid, revoked, unauthorized, no access.
          key.health = 'inactive';
          key.lastError = res.error || 'health check failed';
          key.consecutiveFailures = (key.consecutiveFailures || 0) + 1;
        } else {
          // Temporary: rate limit, quota, 5xx, timeout, unreachable endpoint. The probe
          // could not tell us anything about the key's validity, so its status is left
          // as it is — a busy provider (e.g. while a Website Planner run is consuming
          // the same quota) must not turn a valid key Inactive. The failure is still
          // recorded so the panel shows why the last probe did not confirm the key.
          key.lastError = res.error || 'health check inconclusive';
          key.consecutiveFailures = (key.consecutiveFailures || 0) + 1;
          active++;
        }
        if (key.health === 'inactive') inactive++; else active++;
        checked++;

        const nowStatus = key.health;
        if ((prev || 'active') !== nowStatus) {
          transitions.push(`${provider} ${last4(key.key)}: ${prev || 'unchecked'} → ${nowStatus}${res.error ? ` (${res.error})` : ''}`);
        }
      }
    }),
  );

  return { checked, active, inactive, transitions };
}

/** True when the config object actually has at least one non-empty key. */
function hasAnyKey(cfg: any): boolean {
  return PROVIDERS.some(({ field }) => Array.isArray(cfg?.[field]) && cfg[field].some((k: APIKeyEntry) => k?.key && k.key.trim()));
}

export class AiKeyHealthWorker {
  private config: HealthWorkerConfig;
  private isRunning = false;
  private tickRunning = false;
  private intervalId: NodeJS.Timeout | null = null;
  private warmupId: NodeJS.Timeout | null = null;
  private lastSummary: HealthRunSummary | null = null;

  constructor(config: Partial<HealthWorkerConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  start(): void {
    if (this.isRunning) {
      console.log('[AiKeyHealthWorker] Already running');
      return;
    }
    this.isRunning = true;
    // Warm start shortly after boot (let DB/routes settle), then on the interval.
    this.warmupId = setTimeout(() => { this.runTick(); }, this.config.warmupMs);
    this.intervalId = setInterval(() => { this.runTick(); }, this.config.intervalMs);
    console.log(`[AiKeyHealthWorker] Started — interval ${this.config.intervalMs}ms, warmup ${this.config.warmupMs}ms`);
  }

  stop(): void {
    if (!this.isRunning) return;
    this.isRunning = false;
    if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; }
    if (this.warmupId) { clearTimeout(this.warmupId); this.warmupId = null; }
    console.log('[AiKeyHealthWorker] Stopped');
  }

  getStatus() {
    return {
      running: this.isRunning,
      intervalMs: this.config.intervalMs,
      lastRun: this.lastSummary,
    };
  }

  /** Run a single health-check pass across the global config and per-user configs. */
  async runTick(): Promise<HealthRunSummary> {
    if (this.tickRunning) {
      if (this.config.verbose) console.log('[AiKeyHealthWorker] Previous tick still running — skipping');
      return this.lastSummary || this.emptySummary();
    }
    this.tickRunning = true;
    const start = Date.now();
    let keysChecked = 0;
    let active = 0;
    let inactive = 0;
    let configs = 0;
    const transitions: string[] = [];

    try {
      const { User } = getModels();

      // 1) Global config (super-admin panelSettings.aiConfig)
      try {
        const admin = await User.findOne({ role: 'super-admin' });
        if (admin) {
          const cfg = migrateToKeysArray((admin as any).panelSettings?.aiConfig);
          if (hasAnyKey(cfg)) {
            const r = await checkConfig(cfg, undefined);
            (admin as any).panelSettings = (admin as any).panelSettings || {};
            (admin as any).panelSettings.aiConfig = cfg;
            admin.markModified('panelSettings');
            await admin.save();
            invalidateAICache();
            keysChecked += r.checked; active += r.active; inactive += r.inactive; configs++;
            transitions.push(...r.transitions.map(t => `[global] ${t}`));
          }
        }
      } catch (err: any) {
        console.error('[AiKeyHealthWorker] Global config check failed:', err?.message);
      }

      // 2) Per-user configs (users who opted out of the global config)
      try {
        const users = await User.find({ 'userSettings.useGlobalAIConfig': false }).limit(this.config.maxUsers);
        for (const user of users) {
          try {
            const raw = (user as any).userSettings?.aiConfig;
            if (!raw) continue;
            const cfg = migrateToKeysArray(raw);
            if (!hasAnyKey(cfg)) continue;
            const uid = user._id.toString();
            const r = await checkConfig(cfg, uid);
            (user as any).userSettings = (user as any).userSettings || {};
            (user as any).userSettings.aiConfig = cfg;
            user.markModified('userSettings');
            await user.save();
            invalidateAICache(uid);
            keysChecked += r.checked; active += r.active; inactive += r.inactive; configs++;
            transitions.push(...r.transitions.map(t => `[user ${uid}] ${t}`));
          } catch (err: any) {
            console.error('[AiKeyHealthWorker] User config check failed:', err?.message);
          }
        }
      } catch (err: any) {
        console.error('[AiKeyHealthWorker] Per-user scan failed:', err?.message);
      }

      const summary: HealthRunSummary = {
        at: new Date().toISOString(),
        durationMs: Date.now() - start,
        keysChecked,
        active,
        inactive,
        configs,
        transitions,
      };
      this.lastSummary = summary;

      // Every key dead at once means generation is down platform-wide — the one
      // health signal worth waking a super admin for. Individual keys flipping
      // inactive are routine (rate limits, transient 5xx) and stay in the log.
      // The groupKey holds one entry open until it is read, so a worker ticking
      // every few minutes does not produce an entry per tick.
      if (keysChecked > 0 && active === 0) {
        void notificationService.notifyRole('super-admin', {
          type: 'system.ai_key.exhausted',
          message: `All ${keysChecked} AI provider key(s) across ${configs} config(s) failed their health check. AI generation is unavailable until a working key is configured.`,
          actionUrl: '/super-admin/ai-configuration',
          groupKey: 'system.ai_key.exhausted',
        });
      }

      if (keysChecked > 0 || this.config.verbose) {
        console.log(`[AiKeyHealthWorker] Checked ${keysChecked} key(s) across ${configs} config(s): ${active} active, ${inactive} inactive (${summary.durationMs}ms)`);
        if (transitions.length) console.log('[AiKeyHealthWorker] Status changes:\n  - ' + transitions.join('\n  - '));
      }
      await this.persistLog(summary);
      return summary;
    } catch (err: any) {
      const summary: HealthRunSummary = { ...this.emptySummary(), error: err?.message || 'health check failed', durationMs: Date.now() - start };
      this.lastSummary = summary;
      console.error('[AiKeyHealthWorker] Tick failed:', err?.message);
      await this.persistLog(summary);
      return summary;
    } finally {
      this.tickRunning = false;
    }
  }

  /** Store a run summary as a log document (best-effort — never throws). */
  private async persistLog(summary: HealthRunSummary): Promise<void> {
    try {
      const { AiKeyHealthLog } = getModels();
      if (!AiKeyHealthLog) return;
      await AiKeyHealthLog.create({
        at: summary.at,
        durationMs: summary.durationMs,
        keysChecked: summary.keysChecked,
        active: summary.active,
        inactive: summary.inactive,
        configs: summary.configs,
        transitions: summary.transitions,
        ...(summary.error ? { error: summary.error } : {}),
      });
    } catch (err: any) {
      console.error('[AiKeyHealthWorker] Failed to persist health log:', err?.message);
    }
  }

  private emptySummary(): HealthRunSummary {
    return { at: new Date().toISOString(), durationMs: 0, keysChecked: 0, active: 0, inactive: 0, configs: 0, transitions: [] };
  }
}

// ============================================
// Singleton accessors
// ============================================

let workerInstance: AiKeyHealthWorker | null = null;

export function getAiKeyHealthWorker(config?: Partial<HealthWorkerConfig>): AiKeyHealthWorker {
  if (!workerInstance) workerInstance = new AiKeyHealthWorker(config);
  return workerInstance;
}

export function startAiKeyHealthWorker(config?: Partial<HealthWorkerConfig>): void {
  getAiKeyHealthWorker(config).start();
}

export function stopAiKeyHealthWorker(): void {
  if (workerInstance) workerInstance.stop();
}

/** Run a single health-check pass immediately (used by the manual trigger route). */
export async function runAiKeyHealthCheckOnce(): Promise<HealthRunSummary> {
  return getAiKeyHealthWorker().runTick();
}
