/**
 * FailoverRouter — orchestrates "try mcpd's centralized Llm, fall back to a
 * local provider when authorized" for clients that consume the inference
 * proxy.
 *
 * Decision flow on a centralized inference call:
 *
 *   1. Call the primary (the supplied `primary` callback, typically an HTTP
 *      POST to mcpd /api/v1/llms/:name/infer).
 *   2. If that succeeds → done.
 *   3. If it fails AND a local provider is registered as failover for this
 *      Llm name → call mcpd /api/v1/llms/:name (RBAC-gated) to verify the
 *      caller still has permission to view this Llm. mcpd unreachable →
 *      fail-closed (re-throw the original error). 403 → fail-closed.
 *   4. 200 → invoke the local provider's `complete()` and tag the result
 *      as `failover: true` for client-side audit.
 *
 * The check call uses HEAD to avoid pulling the Llm body (and any
 * description / extraConfig) over the wire — mcpd treats both methods the
 * same in the RBAC hook because the URL maps to the same permission.
 */
import type { LlmProvider } from './types.js';
import type { ProviderRegistry } from './registry.js';

export interface FailoverDecision<T> {
  result: T;
  failover: boolean;
  /** Name of the local provider used (only set when failover === true). */
  via?: string;
}

export interface FailoverRouterDeps {
  /** Injected fetch for the RBAC pre-check. Tests mock this. */
  fetch?: typeof globalThis.fetch;
  /** mcpd base URL (no trailing slash). */
  mcpdUrl: string;
  /** Bearer token to attach to the RBAC pre-check call. */
  bearerToken?: string;
}

/** Outcome of the RBAC pre-check. Used internally + exposed for tests. */
export type AuthCheckOutcome = 'allowed' | 'forbidden' | 'unreachable';

export class FailoverRouter {
  private readonly fetchImpl: typeof globalThis.fetch;
  private readonly mcpdUrl: string;
  private readonly bearer: string | undefined;

  constructor(
    private readonly registry: ProviderRegistry,
    deps: FailoverRouterDeps,
  ) {
    this.fetchImpl = deps.fetch ?? globalThis.fetch;
    this.mcpdUrl = deps.mcpdUrl.replace(/\/+$/, '');
    if (deps.bearerToken !== undefined) this.bearer = deps.bearerToken;
  }

  /**
   * Run a primary inference attempt; on failure, fall back to the local
   * provider if one is registered for this Llm AND the caller still has
   * `view:llms:<llmName>` on mcpd.
   *
   * `primary` should reject (throw) when mcpd's proxy is unreachable or
   * returns a 5xx — that's the signal to consider failover. 4xx errors that
   * indicate a bad request are surfaced as-is; the router only retries on
   * primary failure shapes that look like an upstream/network issue.
   */
  async run<T>(
    llmName: string,
    primary: () => Promise<T>,
    localCall: (provider: LlmProvider) => Promise<T>,
  ): Promise<FailoverDecision<T>> {
    try {
      const result = await primary();
      return { result, failover: false };
    } catch (primaryErr) {
      const local = this.registry.getFailoverFor(llmName);
      if (local === null) throw primaryErr;

      const auth = await this.checkAuth(llmName);
      if (auth !== 'allowed') {
        // Fail-closed for forbidden AND unreachable.
        throw primaryErr;
      }

      const result = await localCall(local);
      return { result, failover: true, via: local.name };
    }
  }

  /** RBAC pre-check exposed for tests / status-display callers. */
  async checkAuth(llmName: string): Promise<AuthCheckOutcome> {
    const url = `${this.mcpdUrl}/api/v1/llms/${encodeURIComponent(llmName)}`;
    const headers: Record<string, string> = {};
    if (this.bearer !== undefined) headers['Authorization'] = `Bearer ${this.bearer}`;
    let res: Response;
    try {
      res = await this.fetchImpl(url, { method: 'HEAD', headers });
    } catch {
      return 'unreachable';
    }
    if (res.status === 200 || res.status === 204) return 'allowed';
    if (res.status === 403 || res.status === 401) return 'forbidden';
    // Anything else (404, 500…) — treat as unreachable for the failover flow.
    return 'unreachable';
  }
}
