// ---- Platform & Model Types ----

/** How the global outbound proxy URL is interpreted. Per-key proxies always
 * use the traditional forward-proxy transport. */
export type ProxyMode = 'forward' | 'fetch-relay';

/** A model declared beside a custom endpoint in an import file (#382). A
 *  capability flag is present only when the paste declared it via a trailing
 *  -TOOLS / -VISION suffix. */
export interface ImportModelEntry {
  id: string;
  supportsTools?: boolean;
  supportsVision?: boolean;
}

export interface PreviewKey {
  keyName: string;
  keyValue: string;
  detectedPlatform: string | null;
  prefix: string;
  /** Custom endpoints only: the upstream URL the export file carried (#687). */
  baseUrl?: string;
  /** Custom endpoints only: models to register alongside the key (#382). */
  models?: ImportModelEntry[];
  isDuplicate?: boolean;
}

export interface ImportKey {
  keyName: string;
  keyValue: string;
  platform: string;
  baseUrl?: string;
  models?: ImportModelEntry[];
}

export interface PreviewResponse {
  keys: PreviewKey[];
  total: number;
  skipped: string[];
  duplicates: number;
}

export interface ImportSelectedRequest {
  keys: ImportKey[];
}

export interface ImportSelectedResponse {
  imported: number;
  skipped: string[];
  errors: Array<{ key: string; error: string }>;
  total: number;
  /** Models registered for imported custom endpoints (#382). */
  modelsRegistered: number;
}

// Active platforms — must match server/src/providers/index.ts and
// server/src/routes/keys.ts PLATFORMS allowlist.
// Moonshot and MiniMax direct integrations were dropped in migrateModelsV4
// (see server/src/db/index.ts). HuggingFace was dropped in V4 and re-added
// in V13 via the router.huggingface.co Inference Providers meta-router.
// SambaNova was dropped in V23 (free tier permanently retired — 402
// "payment method required" once the one-time $5 trial credit lapses).
export type Platform =
  | 'google'
  | 'groq'
  | 'cerebras'
  // Sail Research — native Responses API provider. $5 in free credits refreshes
  // monthly when a payment method is attached; usage beyond the grant is
  // pay-as-you-go. Background polling is required for its flex-only models.
  | 'sail'
  // Responses-only gateway; a shared monthly free allowance, not per model.
  | 'aclide'
  // OpenAI-compatible chat and embeddings; one shared $1 monthly allowance.
  | 'speka'
  // Named free chat/image routes share one renewable daily account allowance.
  | 'kenari'
  | 'kiosapi'
  // Decision-only JEV adapter; promotional availability is catalog-managed.
  | 'beatapi'
  // Free-category chat and decisions share one weighted daily allowance.
  | 'apinex'
  // OpenAI-compatible gateway; free models share a 30-calls/5-hour account
  // allowance on top of a site-wide pool (issue #1460).
  | 'onomeo'
  // OpenAI-compatible gateway; the free tier is one account-wide monthly
  // token allowance (30M, resets monthly; issue #1649).
  | 'iteracompute'
  // Ongoing free research API for chat and speech recognition; catalog only.
  | 'typhoon'
  // Permanent free chat aliases, shared fair-use quota; signed catalog only.
  | 'plugsky'
  // Catalog-only free chat gateways; shared monthly/daily account allowances.
  | 'inferbase'
  | 'simplellm'
  // Selected zero-priced routes have daily/rolling quotas without a top-up;
  // signed catalog only. The public roster also includes paid/promotional IDs.
  | 'llmtr'
  // Catalog-managed free routes: monthly request quota / ongoing $0 models.
  | 'gizmo'
  | 'blockrun'
  // Hosted vision API: $5/workspace in recurring monthly credits, shared
  // across models. Signed catalog only; no bundled model seeds.
  | 'moondream'
  | 'jamai'
  // Hosted gateways; model rows are delivered by the signed catalog only.
  // ElectronHub renews weekly credits; Experiential renews monthly credits.
  | 'electronhub'
  | 'experiential'
  // Catalog-managed gateways: monthly shared credits vs daily free-model quota.
  | 'router9'
  | 'septor'
  | 'clod'
  | 'speechify'
  // Self-hosted TTS: shared 30K tokens/day and 1K API requests/month, no card.
  // Model rows arrive only through the signed, freshness-gated catalog.
  | 'freeai'
  | 'blaze'
  // Lucidity Composite, Api.Airforce and DreamPrompting expose daily free
  // allowances; Waterfall and Logfare publish community / fair-use tiers.
  | 'lucidity'
  | 'airforce'
  | 'dreamprompting'
  | 'waterfall'
  | 'logfare'
  // B.AI — OpenAI-compatible gateway. Its catalog row is a live-tested,
  // limited-time 0-credit promotion, not a recurring free allowance.
  | 'bai'
  // AnyAPI — OpenAI-compatible gateway. Free tier is $0/no card/recurring but
  // capped at 100K tokens/day over "free and basic" models only; no RPM/RPD is
  // published. Catalog rows live in the hosted catalog (premium now, free after
  // 30 days).
  | 'anyapi'
  // AMD Radeon Cloud TokenFactory — OpenAI-compatible shared inference. Its
  // rotating public-model roster is free without consuming instance credits,
  // with recurring account-level allowance and request/concurrency controls.
  | 'radeon'
  | 'nvidia'
  | 'mistral'
  | 'sambanova'
  | 'openrouter'
  | 'cohere'
  // Web search APIs (#1174), not chat providers. Their keys serve POST
  // /v1/search (and the agents' web_search tools) through the catalog's
  // searchEngines registry; chat calls to them are rejected. See
  // SEARCH_PLATFORMS below.
  | 'tavily'
  | 'exa'
  | 'firecrawl'
  | 'linkup'
  | 'parallel'
  | 'serpsearch'
  | 'keenable'
  | 'halx'
  | 'aisense'
  | 'brave'
  | 'cloudflare'
  | 'zhipu'
  | 'ollama'
  | 'kilo'
  | 'pollinations'
  | 'llm7'
  | 'huggingface'
  // OpenCode Zen — OpenAI-compatible gateway. Free promotional models require a
  // free (no-card) account key from opencode.ai/auth; see migrateModelsV18.
  | 'opencode'
  // OVHcloud AI Endpoints — OpenAI-compatible, keyless anonymous tier
  // (2 req/min per IP per model); see migrateModelsV26.
  | 'ovh'
  // Agnes AI (Sapiens AI) — OpenAI-compatible (LiteLLM + vLLM backend). Serves
  // its own proprietary Agnes models; the free key comes from
  // platform.agnes-ai.com (no card).
  | 'agnes'
  // Reka — OpenAI-compatible. Native multimodal models (reka-edge takes
  // image/video). New accounts need prepaid credits (#1202); key from
  // platform.reka.ai.
  | 'reka'
  // SiliconFlow — OpenAI-compatible. Registered for its FREE generative-media
  // models (FLUX.1-schnell image, CosyVoice2 TTS) routed via services/media.ts;
  // chat is supported too. Key from siliconflow.com (no card).
  | 'siliconflow'
  // Routeway — OpenAI-compatible aggregator. Free ':free' models ($0) on a
  // rate-limited pool (~5 rpm observed); requires a browser User-Agent (CF
  // blocks others). Key from routeway.ai (no card).
  | 'routeway'
  // BazaarLink — OpenAI-compatible aggregator. Free 'auto:free' route picks an
  // available zero-cost model. Key from bazaarlink.ai (no card).
  | 'bazaarlink'
  // AINative Studio — OpenAI-compatible aggregator. Advertises a recurring
  // ~10M tokens/month free allocation (no card); quota unverified. Key from
  // ainative.studio.
  | 'ainative'
  // Aion Labs — OpenAI-compatible aggregator with a no-card free API key.
  // Catalog rows live in the Oracle catalog (premium now, free after 30 days).
  | 'aion'
  // Requesty — OpenAI-compatible router with no-card free models/credits.
  // Catalog rows live in the Oracle catalog (premium now, free after 30 days).
  | 'requesty'
  // NavyAI — OpenAI-compatible unified API. Free plan is 150K tokens/day and
  // 20 RPM; catalog rows live in the Oracle catalog (premium now, free after 30 days).
  | 'navy'
  // NaraRouter — OpenAI-compatible aggregator. Free account key from
  // router.bynara.id after Telegram channel/link verification; free-plan routes
  // reset daily and are catalog-managed (premium now, free after 30 days).
  | 'nara'
  // SEA-LION (AI Singapore) — OpenAI-compatible first-party API. Free key
  // (Google sign-in, no card, no region wall) at 10 RPM; catalog rows live in
  // the Oracle catalog (premium now, free after 30 days).
  | 'sealion'
  // OrcaRouter — OpenAI-compatible aggregator (api.orcarouter.ai/v1). Free key
  // from orcarouter.ai (no card); recurring rate-limited free aliases at $0
  // (never fall back to paid). Catalog rows live in the Oracle catalog
  // (premium now, free after the 30-day model-age gate).
  | 'orcarouter'
  // UnoRouter (unorouter.com) — OpenAI-compatible aggregator. The web app is a
  // Next.js site at unorouter.com; the API lives at api.unorouter.com/v1. Free
  // key from unorouter.com (no card); free models carry a `:free` suffix and a
  // per-minute rate limit (429 on cap, e.g. "1 request(s) every 1 min").
  // Live-probed 2026-08-23: /v1/models is public without a key but 401s on a
  // wrong key, and /v1/chat/completions 401s without a key, so default key
  // validation works. Catalog rows live in the hosted catalog (premium now,
  // free after the 30-day model-age gate).
  | 'unorouter'
  // xKiro (xkiro.com) — OpenAI-compatible gateway at api.xkiro.com/v1. Free key
  // from xkiro.com (no card); free plan is 5M tokens/day on its free models,
  // paid models 403 on a free key.
  // /v1/models is public (200 with no key), so key validation must probe
  // /v1/usage, which 401s on a missing/invalid ClientApiKey. Catalog rows live
  // in the hosted catalog (premium now, free after the 30-day model-age gate).
  | 'xkiro'
  // ModelScope (魔搭社区, Alibaba) — OpenAI-compatible inference API
  // (api-inference.modelscope.cn/v1). Free tier moved to magic-grain (魔粒)
  // metering in 2026-08: ~250 魔粒/day account-wide at ~2 魔粒 per request
  // (observed 2026-08-14) ≈ 125 requests/day, but calls only work after the
  // ModelScope account is bound to an Alibaba Cloud CHINA-site (cn) account
  // with Chinese real-name verification — tokens mint without binding, then
  // every call 401s. Catalog rows land after community testing confirms
  // per-model behavior (#581).
  | 'modelscope'
  // ── Chinese domestic providers (#922/#923/#924) ────────────────────────────
  // All four need Chinese real-name verification (实名认证) on the cloud account
  // before a key will serve traffic, the same wall ModelScope hits above.
  // LongCat is the exception worth knowing: its platform accepts an email
  // signup from outside mainland China.
  //
  // Baidu Qianfan (百度千帆) — OpenAI-compatible (https://qianfan.baidubce.com/v2).
  // The ERNIE-Speed / ERNIE-Lite / ERNIE-Tiny series are free indefinitely via
  // pay-as-you-go billing rather than a token pool, so the ceiling is rate
  // limits, not a balance. Baidu calls the arrangement "long-term". Real-name
  // auth (individual or enterprise) required.
  | 'qianfan'
  // Volcengine Ark (火山方舟, ByteDance) — OpenAI-compatible
  // (https://ark.cn-beijing.volces.com/api/v3). Individual developers get a
  // RECURRING daily per-model free reward quota (raised from 500K to 2M
  // tokens/day/model), on top of a one-time 500K new-user grant. The strongest
  // recurring free tier of the four.
  | 'volcengine'
  // LongCat (Meituan / 美团) — OpenAI-compatible
  // (https://api.longcat.chat/openai/v1); also exposes an Anthropic-compatible
  // surface at /anthropic. Free tier is daily; the figure quoted at platform
  // launch was 100K tokens/day. Meituan has ANNOUNCED a 50M tokens/day
  // Flash-Lite free tier but it was described as a future plan, so it is not
  // treated as live here.
  | 'longcat'
  // iFlytek Spark (讯飞星火) — OpenAI-compatible
  // (https://spark-api-open.xf-yun.com/v1), Bearer auth using the console's
  // APIPassword (not the APIKey/APISecret pair the older WebSocket API used).
  // The Lite model (model id `lite`) is documented as free to call; iFlytek
  // does not publish a token ceiling or a QPS figure for it, so neither is
  // claimed here.
  | 'xfyun'
  // AI Horde — free, community-powered inference (volunteer workers) via an
  // OpenAI-compatible proxy (https://oai.aihorde.net/v1). Queue-based, so calls
  // can take tens of seconds; no tool support; usage is reported as kudos, not
  // tokens. Anonymous key `0000000000` works (lowest priority); a registered
  // aihorde.net key raises queue priority. Has a dedicated AIHordeProvider that
  // normalizes the proxy's OpenAI divergences. See issue #345.
  | 'aihorde'
  // User-configured OpenAI-compatible endpoint (llama.cpp, LM Studio, vLLM,
  // Ollama, any base_url). The endpoint URL lives on the api_keys row; see #117.
  | 'custom';

/** Platforms whose keys serve web search (POST /v1/search and the agents'
 *  web_search tools), never chat. Search engine rows arrive through the
 *  catalog's `searchEngines` registry. The server keeps its own runtime copy in
 *  server/src/lib/search-platforms.ts (shared is type-only on the server). */
export const SEARCH_PLATFORMS = ['tavily', 'exa', 'firecrawl', 'linkup', 'parallel', 'serpsearch', 'keenable', 'brave'] as const;
export type SearchPlatform = (typeof SEARCH_PLATFORMS)[number];

export function isSearchPlatform(platform: string): platform is SearchPlatform {
  return (SEARCH_PLATFORMS as readonly string[]).includes(platform);
}

export interface Model {
  id: number;
  platform: Platform;
  modelId: string;
  displayName: string;
  intelligenceRank: number;
  speedRank: number;
  sizeLabel: string;
  rpmLimit: number | null;
  rpdLimit: number | null;
  tpmLimit: number | null;
  tpdLimit: number | null;
  monthlyTokenBudget: string;
  contextWindow: number | null;
  enabled: boolean;
  supportsVision: boolean;
  supportsTools: boolean;
  /** 'discovered': fetched from a built-in provider's own /models because the
   *  catalog carries no models for it (#1348). */
  source?: 'catalog' | 'custom' | 'discovered';
  keyId?: number | null;
  endpointScope?: string | null;
}

// ---- Quirks ----
// Structured, reusable notes about catalog models. One quirk is applied to many
// models via selector parameters (see quirk_targets / services/quirks.ts).
export type QuirkSeverity = 'info' | 'warning' | 'blocker';

export interface Quirk {
  slug: string;
  title: string;
  body: string;
  severity: QuirkSeverity;
}

export interface QuirkTarget {
  platform: Platform | null;
  modelGlob: string | null;
}

export interface ModelListRow {
  platform: string;
  model_id: string;
  display_name: string;
  context_window: number | null;
  // 1 when the catalog row is enabled. 1 when an enabled key can serve it
  // (enabled AND a matching enabled api_key exists). SQLite returns 0/1.
  enabled: number;
  available: number;
}

export type KeyStatus = 'healthy' | 'rate_limited' | 'invalid' | 'error' | 'unknown';

export interface ApiKeyModel {
  id: number;
  kind: 'chat' | 'embedding' | 'image' | 'audio' | 'transcription';
  modelId: string;
  displayName: string;
  family?: string | null;
}

/** An active rate-limit cooldown on one model for a key. A key can be healthy
 *  and enabled yet still skipped by the router because of these. */
export interface ApiKeyCooldown {
  modelId: string;
  expiresAtMs: number;
  remainingMs: number;
}

export interface ApiKey {
  id: number;
  platform: Platform;
  label: string;
  maskedKey: string;
  baseUrl: string | null;
  /** Optional custom-endpoint group label (#1176); null = legacy single
   *  "Custom" group. Only set on platform='custom' rows. */
  groupLabel?: string | null;
  status: KeyStatus;
  enabled: boolean;
  /** This row is the anonymous sentinel of a key-optional platform: there is
   *  no credential to copy, scope or reveal. */
  keyless: boolean;
  /** The platform works with or without a key (Kilo, OVH, AI Horde), so a key
   *  can be added to or left off this row (#1331). */
  keyOptional?: boolean;
  /** Whether an export file would actually contain this row. The server decides
   *  it so the dialog's "will export N keys" cannot drift from the export. */
  exportable: boolean;
  createdAt: string;
  lastCheckedAt: string | null;
  /** Last time the key was proven to work — a valid probe verdict or a served
   *  live request (#1493). Unlike lastCheckedAt, a transport error does not
   *  refresh it, so "checked recently but never worked" is visible. */
  lastHealthyAt?: string | null;
  lastHealthError: string | null;
  /** Model ids this key is limited to; null = serves every model of its
   *  platform (#657). */
  modelScope?: string[] | null;
  /** Per-key monthly request cap (0 = unlimited, #1158). */
  monthlyRequestCap?: number;
  /** Per-key monthly token cap (0 = unlimited, #1158). */
  monthlyTokenCap?: number;
  /** Current UTC month's successful usage against the caps above, with the
   *  ISO time of the next monthly reset. */
  monthlyUsage?: { requests: number; tokens: number; resetsAt: string };
  /** The per-key proxy override with its password masked (#590); '' = none. */
  maskedProxyUrl?: string;
  models?: ApiKeyModel[];
  cooldowns?: ApiKeyCooldown[];
  /** True for a built-in provider key whose platform the catalog carries no
   *  models for, so the dashboard offers Fetch models on it (#1348). */
  modelDiscovery?: boolean;
  /** False for a provider that forbids automated key checks (LLMTR, #1459):
   *  its keys turn healthy on their first served request, not on a probe. */
  autoHealthCheck?: boolean;
  /** Only sent when autoHealthCheck is false: how many of the provider's
   *  models this install can route to (0 = nothing to serve yet). */
  modelsAvailable?: number;
}

export interface ApiKeyCreate {
  platform: Platform;
  key: string;
  label?: string;
}

// ---- Fallback Config ----

export interface FallbackEntry {
  modelId: number;
  platform: Platform;
  displayName: string;
  intelligenceRank: number;
  speedRank: number;
  priority: number;
  enabled: boolean;
  // Present when model unification is enabled — identifies the logical model
  // this provider row belongs to so the dashboard can render grouped rows.
  groupKey?: string;
  canonicalId?: string;
  groupLabel?: string;
}

// ---- Model Grouping (unify the same model across providers) ----
// One logical model can be served by several providers (rows in the `models`
// table). When unification is enabled, those rows collapse into a single group
// keyed by a normalized display name; see server/src/services/model-groups.ts.
export interface ModelGroupInfo {
  groupKey: string;     // normalized display name — the grouping identity
  canonicalId: string;  // stable slug advertised on /v1/models
  groupLabel: string;   // human label (suffix-stripped display name)
}

export interface UnifyOverrides {
  // Coalesce several normalized display-names (or exact "platform:model_id"
  // members) into one group keyed by `into`.
  merges: { into: string; keys: string[] }[];
  // Force a specific "platform:model_id" row out of its computed group.
  splits: { member: string; groupKey?: string }[];
}

export interface UnifySettings {
  enabled: boolean;
  overrides: UnifyOverrides;
}

// ---- OpenAI-Compatible Types ----

export interface ChatToolCallFunction {
  name: string;
  arguments: string;
}

export interface ChatToolCall {
  id: string;
  type: 'function';
  function: ChatToolCallFunction;
  thought_signature?: string;
}

export interface ChatToolFunctionDefinition {
  name: string;
  description?: string;
  parameters?: Record<string, unknown>;
  strict?: boolean;
}

export interface ChatToolDefinition {
  type: 'function';
  function: ChatToolFunctionDefinition;
}

export type ChatToolChoice =
  | 'none'
  | 'auto'
  | 'required'
  | {
    type: 'function';
    function: {
      name: string;
    };
  };

// OpenAI's multimodal envelope: clients like opencode / continue.dev send
// content as an array of typed blocks even for text-only messages, and
// Gemini-lineage agents (Qwen Code, AionUI) send part-style `{ text }` blocks
// with no `type` — plus bare strings inside arrays. We accept all of it on
// the wire and flatten to string for providers that don't support arrays
// (Cohere, Cloudflare). See server/src/lib/content.ts. (#200)
export type ChatContentBlock = string | { type?: string; text?: string; [key: string]: unknown };
export type ChatContent = string | null | ChatContentBlock[];

export interface ChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: ChatContent;
  name?: string;
  tool_call_id?: string;
  tool_calls?: ChatToolCall[];
  // The model's thinking trace on an assistant turn. Some thinking models
  // (DeepSeek on OpenCode Zen) require it to be replayed verbatim on the next
  // turn or they 400; the proxy preserves and forwards it. See issue #255.
  reasoning_content?: string;
  // Moonshot's "partial" prefill flag on an assistant turn: when true, the
  // model continues the given text instead of starting a fresh turn. Only
  // forwarded to models that understand it (Moonshot/Kimi); stripped for all
  // other providers. See issue #1038.
  partial?: boolean;
}

export interface ChatCompletionRequest {
  model?: string;
  messages: ChatMessage[];
  temperature?: number;
  max_tokens?: number;
  stream?: boolean;
  stream_options?: {
    include_usage?: boolean;
  };
  top_p?: number;
  stop?: string | string[];
  tools?: ChatToolDefinition[];
  tool_choice?: ChatToolChoice;
  parallel_tool_calls?: boolean;
}

export interface ChatCompletionChoice {
  index: number;
  message: ChatMessage;
  finish_reason: string | null;
  // Present when the client requested logprobs and the provider returned
  // them; passed through verbatim (provider shapes vary slightly).
  logprobs?: unknown;
}

export interface TokenUsage {
  prompt_tokens: number;
  completion_tokens: number;
  total_tokens: number;
  // OpenAI-standard breakdown some providers advertise alongside the totals.
  // Optional because many free-tier endpoints omit it; the proxy falls back
  // to its chars/4 estimate when absent. (#764)
  completion_tokens_details?: { reasoning_tokens?: number };
  prompt_tokens_details?: { cached_tokens?: number };
  // Gateway-synthesized block (upstream never sent usage): flagged so a
  // cost-accounting client can tell it apart from the upstream's real
  // counts. (#1084)
  estimated?: boolean;
}

export interface ChatCompletionResponse {
  id: string;
  object: 'chat.completion';
  created: number;
  model: string;
  choices: ChatCompletionChoice[];
  usage: TokenUsage;
  _routed_via?: {
    platform: Platform;
    model: string;
  };
}

export interface ChatCompletionChunk {
  id: string;
  object: 'chat.completion.chunk';
  created: number;
  model: string;
  choices: {
    index: number;
    delta: {
      role?: 'assistant';
      content?: string;
      reasoning_content?: string;
      tool_calls?: ChatToolCall[];
    };
    finish_reason: string | null;
  }[];
  usage?: TokenUsage;
}

// ---- Analytics Types ----

export interface AnalyticsSummary {
  totalRequests: number;
  successRate: number;
  totalInputTokens: number;
  totalOutputTokens: number;
  avgLatencyMs: number;
  estimatedCostSavings: number;
}

export interface PlatformStats {
  platform: Platform;
  requests: number;
  successRate: number;
  avgLatencyMs: number;
  totalInputTokens: number;
  totalOutputTokens: number;
}

export interface TimelinePoint {
  timestamp: string;
  requests: number;
  successCount: number;
  failureCount: number;
}

export interface RequestLog {
  id: number;
  platform: Platform;
  modelId: string;
  status: 'success' | 'error';
  inputTokens: number;
  outputTokens: number;
  latencyMs: number;
  error: string | null;
  createdAt: string;
}

// ---- Rate Limit Types ----

export interface RateLimitStatus {
  platform: Platform;
  modelId: string;
  rpm: { used: number; limit: number | null };
  rpd: { used: number; limit: number | null };
  tpm: { used: number; limit: number | null };
  available: boolean;
  nextResetAt: string | null;
}

// ---- Provider Quota Observability ----

export type QuotaMetric = 'requests' | 'tokens' | 'credits' | 'neurons';
export type QuotaResetStrategy = 'fixed_calendar' | 'rolling_window' | 'token_bucket' | 'provider_reported' | 'unknown';
export type QuotaObservationSource = 'header' | 'quota_api' | 'error_body' | 'local_usage' | 'documentation' | 'probe';

export interface ProviderQuotaState {
  platform: Platform;
  keyId: number;
  /** The key's operator-facing label, when the row still names a live key. */
  keyLabel?: string | null;
  quotaPoolKey: string;
  metric: QuotaMetric;
  limit: number | null;
  remaining: number | null;
  resetAt: string | null;
  resetStrategy: QuotaResetStrategy;
  source: QuotaObservationSource;
  confidence: number;
  notes: string | null;
  observedAt: string;
  updatedAt: string;
}

export interface ProviderQuotaObservation extends ProviderQuotaState {
  id: string;
  statusCode: number | null;
  retryAfterMs: number | null;
  providerAccountId: string | null;
  modelId: string | null;
  endpoint: string | null;
  rawJson: string | null;
  createdAt: string;
}

export interface QuotaOutlookPool {
  platform: Platform;
  pool: string;
  limit: number | null;
  remaining: number | null;
  remainingPct: number | null;
  observedAt: string | null;
  resetAt: string | null;
  /** Successful requests through this instance during the observation window. */
  recentRequestCount: number;
  ratePerMin: number;
  unavailableReason: 'quota_not_reported' | 'stale_observation' | 'reset_not_reported' | 'low_confidence' | null;
  estimatedExhaustionAt: string | null;
  status: 'unknown' | 'stale' | 'unavailable' | 'insufficient_data' | 'resets_first' | 'forecast' | 'exhausted';
  warning: 'low_balance' | 'exhausting_soon' | null;
}

export interface QuotaOutlookResponse {
  generatedAt: string;
  observationWindowMinutes: number;
  minimumRequests: number;
  pools: QuotaOutlookPool[];
}

// ---- Provider Dashboard Types ----

export type ProviderHealthStatus = 'healthy' | 'issues' | 'rate_limited' | 'unknown' | 'unconfigured';

export interface ProviderSummary {
  platform: Platform;
  name: string;
  totalKeys: number;
  enabledKeys: number;
  healthyKeys: number;
  totalModels: number;
  activeModels: number;
  status: ProviderHealthStatus;
  isConfigured: boolean;
}

export interface GroupedProvider {
  id: string;
  platform: Platform;
  name: string;
  url?: string;
  baseUrl?: string | null;
  endpointScope?: string | null;
  keyless?: boolean;
  keys: ApiKey[];
  models: Model[];
  summary: ProviderSummary;
}

export interface CustomModelCreate {
  platform: Platform;
  modelId: string;
  displayName?: string;
  contextWindow?: number | null;
  rpmLimit?: number | null;
  rpdLimit?: number | null;
  tpmLimit?: number | null;
  tpdLimit?: number | null;
  supportsVision?: boolean;
  supportsTools?: boolean;
}

export interface ModelTestResult {
  success: boolean;
  modelId: string;
  latencyMs: number;
  error?: string;
}
