'use strict';

const EventEmitter = require('events');

const WINDOW_MS = 5 * 60 * 1000; // 5 minutes
const DEGRADED_THRESHOLD = 3;
const DOWN_THRESHOLD = 5;

/**
 * Classify an error into a category with retriability info.
 */
function classifyError(error) {
  const msg = error?.message || '';
  const status = error?.status || error?.statusCode;
  if (status === 401 || status === 403) return { type: 'auth', retriable: false };
  if (status === 429) return { type: 'rate_limit', retriable: true };
  if (status >= 500) return { type: 'server_error', retriable: true };
  if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('fetch failed')) return { type: 'network', retriable: true };
  if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) return { type: 'timeout', retriable: true };
  return { type: 'unknown', retriable: true };
}

function createProviderState() {
  return {
    status: 'healthy',         // 'healthy' | 'degraded' | 'down'
    consecutiveFailures: 0,
    lastSuccess: null,
    lastError: null,
    totalCalls: 0,
    recentCalls: [],           // [{ time, success }]
  };
}

class ProviderHealth extends EventEmitter {
  constructor() {
    super();
    /** @type {Map<string, object>} */
    this._providers = new Map();
    this._intervalId = null;
    this._allDown = false;
    // Most recent default-routing notice (set when the live default falls back to
    // another provider/CLI, cleared when the default recovers). Surfaced to the user
    // via getSystemHealth() → /api/models/health.
    this._routingNotice = null;
  }

  /** Record (or clear) the latest default-provider routing notice for the UI. */
  recordRoutingNotice(notice) {
    this._routingNotice = notice ? { ...notice, at: new Date().toISOString() } : null;
    return this._routingNotice;
  }

  getRoutingNotice() {
    return this._routingNotice;
  }

  clearRoutingNotice() {
    this._routingNotice = null;
  }

  _getOrCreate(providerId) {
    if (!this._providers.has(providerId)) {
      this._providers.set(providerId, createProviderState());
    }
    return this._providers.get(providerId);
  }

  _pruneOldCalls(state) {
    const cutoff = Date.now() - WINDOW_MS;
    state.recentCalls = state.recentCalls.filter(c => c.time >= cutoff);
  }

  _getRecentCalls(state) {
    const cutoff = Date.now() - WINDOW_MS;
    return state.recentCalls.filter(c => c.time >= cutoff);
  }

  _calcFailureRate(state) {
    const recent = this._getRecentCalls(state);
    if (recent.length === 0) return 0;
    const failures = recent.filter(c => !c.success).length;
    return failures / recent.length;
  }

  /**
   * Track a call result for a provider.
   */
  trackCall(providerId, success, error) {
    const state = this._getOrCreate(providerId);
    const now = Date.now();

    state.totalCalls++;
    state.recentCalls.push({ time: now, success });

    // Prune old entries periodically (keep sliding window manageable)
    if (state.recentCalls.length > 200) {
      this._pruneOldCalls(state);
    }

    const previousStatus = state.status;

    if (success) {
      state.lastSuccess = new Date(now);
      state.consecutiveFailures = 0;

      if (previousStatus === 'degraded' || previousStatus === 'down') {
        state.status = 'healthy';
        this.emit('provider-recovered', { providerId });

        // Check if we were in all-down state and are now recovering
        if (this._allDown) {
          this._allDown = false;
          this.emit('provider-back-online', { providerId });
        }
      }
    } else {
      state.consecutiveFailures++;
      state.lastError = {
        time: new Date(now),
        message: error?.message || 'Unknown error',
        code: error?.status || error?.statusCode || error?.code || null,
      };

      if (state.consecutiveFailures >= DOWN_THRESHOLD && previousStatus !== 'down') {
        state.status = 'down';
        this.emit('provider-down', {
          providerId,
          consecutiveFailures: state.consecutiveFailures,
          lastError: state.lastError,
        });
        this._checkAllDown();
      } else if (state.consecutiveFailures >= DEGRADED_THRESHOLD && previousStatus === 'healthy') {
        state.status = 'degraded';
        this.emit('provider-degraded', {
          providerId,
          consecutiveFailures: state.consecutiveFailures,
          lastError: state.lastError,
        });
      }
    }
  }

  _checkAllDown() {
    const allDown = [...this._providers.values()].every(s => s.status === 'down');
    if (allDown) {
      this._allDown = true;
      const providers = {};
      for (const [id, s] of this._providers) {
        providers[id] = { status: s.status, consecutiveFailures: s.consecutiveFailures };
      }
      this.emit('all-providers-down', { providers });
    }
  }

  /**
   * Get health status for a specific provider.
   */
  getProviderHealth(providerId) {
    const state = this._getOrCreate(providerId);
    return {
      status: state.status,
      consecutiveFailures: state.consecutiveFailures,
      lastSuccess: state.lastSuccess,
      lastError: state.lastError,
      totalCalls: state.totalCalls,
      failureRate: this._calcFailureRate(state),
    };
  }

  /**
   * Get health status for all tracked providers.
   */
  getSystemHealth() {
    const providers = {};
    const healthyProviders = [];

    for (const [id] of this._providers) {
      const h = this.getProviderHealth(id);
      providers[id] = h;
      if (h.status === 'healthy') {
        healthyProviders.push(id);
      }
    }

    return {
      providers,
      hasHealthyProvider: healthyProviders.length > 0,
      healthyProviders,
      routingNotice: this._routingNotice,
    };
  }

  /**
   * Check if a provider is healthy enough to use.
   * Unknown (never-seen) providers are assumed healthy.
   */
  isHealthy(providerId) {
    if (!this._providers.has(providerId)) return true;
    const state = this._providers.get(providerId);
    return state.status !== 'down';
  }

  /**
   * Get ordered list of healthy provider IDs (best first).
   * Sorted by: healthy before degraded, then by lowest failure rate.
   */
  getHealthyProviders() {
    const result = [];
    for (const [id] of this._providers) {
      const state = this._providers.get(id);
      if (state.status !== 'down') {
        result.push({
          id,
          status: state.status,
          failureRate: this._calcFailureRate(state),
        });
      }
    }

    result.sort((a, b) => {
      // healthy before degraded
      if (a.status !== b.status) {
        return a.status === 'healthy' ? -1 : 1;
      }
      // lower failure rate first
      return a.failureRate - b.failureRate;
    });

    return result.map(r => r.id);
  }

  /**
   * Run a health check against a provider.
   */
  async checkProvider(providerId, clientFactory) {
    try {
      const client = clientFactory(providerId);
      await client.listModels();
      this.trackCall(providerId, true);
      return true;
    } catch (err) {
      this.trackCall(providerId, false, err);
      return false;
    }
  }

  /**
   * Start periodic health checks.
   */
  startPeriodicChecks(clientFactory, intervalMs = 60000) {
    this.stopPeriodicChecks();
    this._intervalId = setInterval(() => {
      for (const [id] of this._providers) {
        this.checkProvider(id, clientFactory).catch(() => {});
      }
    }, intervalMs);
    // Don't keep the process alive just for health checks
    if (this._intervalId.unref) this._intervalId.unref();
  }

  /**
   * Stop periodic health checks.
   */
  stopPeriodicChecks() {
    if (this._intervalId) {
      clearInterval(this._intervalId);
      this._intervalId = null;
    }
  }
}

// Singleton for app-wide use
const defaultInstance = new ProviderHealth();

module.exports = { ProviderHealth, classifyError, default: defaultInstance };
