'use strict';

const STALENESS_THRESHOLD_MS = 60000;   // 60s
const MAX_BACKOFF_MS = 600000;          // 10min
const UNHEALTHY_AFTER_FAILURES = 2;

function log(msg) {
  const ts = new Date().toISOString();
  console.log(`[${ts}] [provider-availability] ${msg}`);
}

class ProviderAvailability {
  constructor() {
    /** @type {Map<string, object>} */
    this._providers = new Map();
    /** @type {Map<string, Promise>} */
    this._inflight = new Map();
  }

  /**
   * Register a new provider to track.
   */
  registerProvider({ providerId, providerType, baseUrl = null, apiKey = null }) {
    this._providers.set(providerId, {
      providerId,
      providerType,
      baseUrl,
      apiKey,
      status: 'unknown',
      lastCheck: 0,
      lastSuccess: 0,
      consecutiveFailures: 0,
      backoffMs: STALENESS_THRESHOLD_MS,
      lastError: null,
    });
  }

  /**
   * Get the raw state for a provider, or null if not registered.
   */
  getProviderState(id) {
    return this._providers.get(id) || null;
  }

  getProviderForType(providerType) {
    return [...this._providers.values()].find(p => p.providerType === providerType || p.providerId === providerType) || null;
  }

  isProviderUsable(idOrType) {
    const state = this.getProviderState(idOrType) || this.getProviderForType(idOrType);
    if (!state) return true;
    return state.status !== 'unhealthy';
  }

  /**
   * Return all registered providers.
   */
  getConfiguredProviders() {
    return [...this._providers.values()];
  }

  /**
   * Return providers where status is not 'unhealthy'.
   */
  getAvailableProviders() {
    return [...this._providers.values()].filter(p => p.status !== 'unhealthy');
  }

  /**
   * True if at least one provider is not unhealthy. False when none registered.
   */
  isAnyProviderAvailable() {
    if (this._providers.size === 0) return false;
    for (const p of this._providers.values()) {
      if (p.status !== 'unhealthy') return true;
    }
    return false;
  }

  /**
   * Check if a provider's last check is stale and needs refreshing.
   * Healthy/unknown: stale after STALENESS_THRESHOLD_MS.
   * Unhealthy: stale after backoffMs (exponential).
   */
  isStale(id) {
    const state = this._providers.get(id);
    if (!state) return true;
    const threshold = state.status === 'unhealthy' ? state.backoffMs : STALENESS_THRESHOLD_MS;
    return (Date.now() - state.lastCheck) >= threshold;
  }

  /**
   * Record a successful health check or call for a provider.
   */
  recordSuccess(id) {
    const state = this._providers.get(id);
    if (!state) return;
    const prev = state.status;
    const now = Date.now();
    state.status = 'healthy';
    state.consecutiveFailures = 0;
    state.backoffMs = STALENESS_THRESHOLD_MS;
    state.lastCheck = now;
    state.lastSuccess = now;
    if (prev !== 'healthy') {
      log(`${id}: ${prev} -> healthy`);
    }
  }

  /**
   * Record a failed health check or call for a provider.
   */
  recordFailure(id, errorMsg) {
    const state = this._providers.get(id);
    if (!state) return;
    const now = Date.now();
    state.consecutiveFailures++;
    state.lastCheck = now;
    state.lastError = errorMsg || null;

    if (state.consecutiveFailures >= UNHEALTHY_AFTER_FAILURES) {
      if (state.status !== 'unhealthy') {
        const prev = state.status;
        state.status = 'unhealthy';
        log(`${id}: ${prev} -> unhealthy (${state.consecutiveFailures} failures)`);
      }
      // Exponential backoff grows on every failure while unhealthy (60s, 120s, 240s, ... capped at 10min)
      state.backoffMs = Math.min(state.backoffMs * 2, MAX_BACKOFF_MS);
    }
  }

  /**
   * Ensure the provider's status is fresh. If stale, run probeFn.
   * Deduplicates concurrent probes for the same provider.
   */
  async ensureFresh(id, probeFn) {
    const state = this._providers.get(id);
    if (!state) return null;

    if (!this.isStale(id)) return state;

    // Deduplicate: if a probe is already in-flight, await it
    if (this._inflight.has(id)) {
      await this._inflight.get(id);
      return this._providers.get(id);
    }

    const probePromise = this._runProbe(id, probeFn);
    this._inflight.set(id, probePromise);

    try {
      await probePromise;
    } finally {
      this._inflight.delete(id);
    }

    return this._providers.get(id);
  }

  async _runProbe(id, probeFn) {
    try {
      await Promise.race([
        probeFn(id),
        new Promise((_, reject) => {
          const t = setTimeout(() => reject(new Error('probe timeout')), 3000);
          if (t.unref) t.unref();
        }),
      ]);
      this.recordSuccess(id);
    } catch (err) {
      this.recordFailure(id, err.message || String(err));
    }
  }

  /**
   * Refresh all stale providers in parallel.
   */
  async ensureFreshAll(probeFn) {
    const stale = [...this._providers.keys()].filter(id => this.isStale(id));
    await Promise.allSettled(stale.map(id => this.ensureFresh(id, probeFn)));
  }

  /**
   * Get a status summary suitable for API consumption.
   */
  getStatus() {
    const providers = this.getConfiguredProviders();
    return {
      configured: providers.length > 0,
      anyAvailable: this.isAnyProviderAvailable(),
      providers,
    };
  }

  /**
   * Reset backoff for a provider, forcing re-check on next ensureFresh.
   */
  resetBackoff(id) {
    const state = this._providers.get(id);
    if (!state) return;
    state.backoffMs = STALENESS_THRESHOLD_MS;
    state.lastCheck = 0;
  }

  /**
   * Clear all state. Useful for testing.
   */
  clear() {
    this._providers.clear();
    this._inflight.clear();
  }
}

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

module.exports = {
  ProviderAvailability,
  default: defaultInstance,
  STALENESS_THRESHOLD_MS,
  MAX_BACKOFF_MS,
  UNHEALTHY_AFTER_FAILURES,
};
