/**
 * Provider model catalog: fetches the list of models available to an
 * Organization from its ACTIVE provider connection (OpenRouter), with an
 * in-memory TTL cache so the admin model picker stays responsive without
 * hammering the upstream API on every page load.
 *
 * The model list is **derived** from the provider connection — there is no
 * separate model table to maintain. When an org activates a provider, its
 * models become selectable; when the provider is disabled, the list empties.
 */
import type { PrismaClient } from "@prisma/client";
import type { LocalSecretEnvelope } from "../security/secretEnvelope.js";
import { decryptStoredProviderCredential } from "./providerConnections.js";

/** A model entry surfaced to the admin model picker. */
export interface ProviderModelEntry {
  readonly id: string;
  readonly label: string;
  readonly toolCapable: boolean;
}

/** A model entry as returned by OpenRouter's /v1/models endpoint. */
interface OpenRouterModel {
  readonly id: string;
  readonly name: string;
  readonly supported_parameters: readonly string[];
}

interface OpenRouterModelsResponse {
  readonly data: readonly OpenRouterModel[];
}

/** Cache entry: models + expiry timestamp. */
interface CacheEntry {
  readonly models: readonly ProviderModelEntry[];
  readonly expiresAt: number;
}

const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Per-organization model catalog cache. Keyed by organizationId so that
 * multiple orgs in the same Silo don't cross-pollute. The cache is intentionally
 * process-local: it survives for the lifetime of the Hub process and is
 * invalidated by TTL, not by DB events. A cache miss re-fetches from the
 * provider API.
 */
export class ProviderModelCatalog {
  private readonly cache = new Map<string, CacheEntry>();
  private readonly fetchImpl: typeof fetch;

  constructor(
    private readonly prisma: PrismaClient,
    private readonly secrets: LocalSecretEnvelope,
    private readonly ttlMs: number = DEFAULT_TTL_MS,
    fetchImpl: typeof fetch = fetch,
  ) {
    this.fetchImpl = fetchImpl;
  }

  /**
   * List tool-capable models available to the organization's ACTIVE provider
   * connection. Returns cached results when fresh; otherwise fetches from the
   * provider API. Falls back to an empty list (not an error) when the org has
   * no ACTIVE provider — the admin sees the env-default model from the
   * separate env fallback in the route.
   */
  async listModels(organizationId: string): Promise<readonly ProviderModelEntry[]> {
    const cached = this.cache.get(organizationId);
    if (cached !== undefined && cached.expiresAt > Date.now()) {
      return cached.models;
    }

    const credential = await this.resolveActiveProviderCredential(organizationId);
    if (credential === null) return [];

    const models = await this.fetchModelsFromProvider(credential);
    this.cache.set(organizationId, {
      models,
      expiresAt: Date.now() + this.ttlMs,
    });
    return models;
  }

  /** Force a cache invalidation (e.g. after provider connection changes). */
  invalidate(organizationId: string): void {
    this.cache.delete(organizationId);
  }

  private async resolveActiveProviderCredential(
    organizationId: string,
  ): Promise<{ readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey: string } | null> {
    const connection = await this.prisma.organizationProviderConnection.findFirst({
      where: { organizationId, status: "ACTIVE" },
      include: { activeSecretVersion: true },
    });
    const version = connection?.activeSecretVersion;
    if (connection === null || version === null || version === undefined || version.retiredAt !== null) {
      return null;
    }
    const payload = decryptStoredProviderCredential(this.secrets, {
      organizationId,
      connectionId: connection.id,
      providerId: connection.providerId,
      secretVersionId: version.id,
      envelopeVersion: version.envelopeVersion,
      keyId: version.keyId,
      envelope: version.envelope,
    });
    return {
      baseUrl: payload.baseUrl,
      authToken: payload.authToken,
      anthropicApiKey: payload.anthropicApiKey,
    };
  }

  private async fetchModelsFromProvider(
    credential: { readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey: string },
  ): Promise<readonly ProviderModelEntry[]> {
    const base = credential.baseUrl.endsWith("/") ? credential.baseUrl : `${credential.baseUrl}/`;
    // Filter to models that support tool calling — the agent runner requires it.
    const url = new URL("v1/models?supported_parameters=tools", base);
    const headers: Record<string, string> = {
      authorization: `Bearer ${credential.authToken}`,
      accept: "application/json",
    };
    if (credential.anthropicApiKey !== "") headers["x-api-key"] = credential.anthropicApiKey;

    const response = await this.fetchImpl(url, {
      method: "GET",
      headers,
      signal: AbortSignal.timeout(15_000),
    });
    if (!response.ok) {
      throw new Error(`provider models request failed: status ${response.status}`);
    }
    const body = (await response.json()) as OpenRouterModelsResponse;
    return body.data.map((m) => ({
      id: m.id,
      label: m.name,
      toolCapable: m.supported_parameters.includes("tools"),
    }));
  }
}
