// Routing resolution: a routeKey "<agent>/<provider>" → what the orchestrator
// should actually do with the request. Reads the cached routing policy and
// model registry; never touches disk on the proxy path.
//
// Two resolved shapes drive the orchestrator's fast paths:
//   - `model`  → a 1-member chain. Same- or cross-protocol single-model swap.
//   - `chain`  → 2+ members. Failover walk with switch rules.
// A 0-member chain, an unknown model, or a non-translatable client decoder
// falls back to passthrough.

import { logger } from '../../core/logger.ts'
import {
  findModel,
  findProvider,
  type ModelApi,
  type ModelEntry,
  type ProviderEntry,
  resolveApi,
} from '../../core/model-registry.ts'
import type { DecoderKind } from '../../core/routes.ts'
import {
  type AgentRouting,
  type CapabilityFulfillment,
  type CapabilityId,
  routingFor,
  type SwitchRule,
} from '../../core/routing-policy.ts'
import { getSecret } from '../../core/secrets.ts'
import { getModelRegistry, getRoutingPolicy, getSecrets } from '../routing/load.ts'

/** One fully-resolved chain member: a model, its provider, its wire API, and
 *  the conditions that advance the chain past it. */
export type ResolvedMember = {
  model: ModelEntry
  provider: ProviderEntry
  api: ModelApi
  switchOn: SwitchRule[]
}

/** The vision companion shape vision-loop / assessVision speak. Re-exported
 *  here as the canonical type so capture and the loop both pass the same
 *  thing — even though the orchestrator now sources companions from
 *  per-route capabilities, the runtime contract is unchanged. */
export type ResolvedToolModel = {
  kind: 'vision'
  model: ModelEntry
  provider: ProviderEntry
  api: ModelApi
}

/** A capability fulfillment resolved against the registry. The
 *  orchestrator reads this map regardless of whether the route's main
 *  target is a single model or a chain — capability execution is
 *  orthogonal to model routing. */
export type ResolvedCapability =
  | { via: 'companion'; ref: ModelRef }
  | { via: 'local'; providerId?: string }

export type ResolvedCapabilities = Partial<Record<CapabilityId, ResolvedCapability>>

export type ResolvedRoute =
  | { kind: 'passthrough' }
  | {
      kind: 'model'
      /** The wire format the client agent speaks. */
      clientApi: ModelApi
      model: ModelEntry
      provider: ProviderEntry
      /** The target model's wire format — may differ from clientApi. */
      api: ModelApi
      capabilities: ResolvedCapabilities
      configuredTarget: { kind: 'model'; id: string }
      /** Set when this route is a subagent cost-saver downgrade — the model
       *  the client originally requested (e.g. `claude-opus-4-8`), recorded so
       *  capture can show requested-vs-served and the dashboard the savings. */
      downgradedFrom?: string
    }
  | {
      kind: 'chain'
      clientApi: ModelApi
      members: ResolvedMember[]
      capabilities: ResolvedCapabilities
      /** `id` is the chain's entry-point model id — drives the parent
       *  packet's displayed label so the dashboard can show
       *  `chain: <first-member>` for the route decision. */
      configuredTarget: { kind: 'chain'; id: string }
    }

/** A route's decoder → the wire API Thomas can translate, if any. */
export function decoderToApi(decoder: DecoderKind): ModelApi | undefined {
  if (decoder === 'anthropic') return 'anthropic-messages'
  if (decoder === 'openai-chat') return 'openai-chat'
  if (decoder === 'openai-responses') return 'openai-responses'
  return undefined
}

export type ModelRef = { model: ModelEntry; provider: ProviderEntry; api: ModelApi }

/** A seeded provider's managed auth points at a secret in secrets.json. If
 *  that secret is absent — never captured, or rotated out-of-band — fall
 *  back to passthrough for this request rather than injecting an empty
 *  credential, which would 401. The stale secret is re-captured on the
 *  next `thomas wire`. */
function withResolvableAuth(provider: ProviderEntry): ProviderEntry {
  const auth = provider.auth
  // passthrough has nothing to resolve; agent-oauth resolves its token at
  // request time (with its own graceful fallback) — not from secrets.json.
  if (auth.kind === 'passthrough' || auth.kind === 'agent-oauth') return provider
  if (getSecret(getSecrets(), auth.valueRef) !== undefined) return provider
  logger.warn(
    `routing: secret "${auth.valueRef}" missing for provider "${provider.id}", using passthrough`,
  )
  return { ...provider, auth: { kind: 'passthrough' } }
}

/** Resolve a model id (optionally scoped to a provider) into its model,
 *  provider, and effective wire API. The providerId disambiguates when
 *  the same model id exists under multiple providers (e.g. Xiangxin-2XL
 *  harvested under hermes and also added by hand under a custom
 *  provider). Omitting it falls back to first-match-by-id so legacy
 *  routing-policy entries still resolve. */
export function resolveModelRef(
  modelId: string,
  providerId?: string,
): ModelRef | undefined {
  const reg = getModelRegistry()
  const model = findModel(reg, modelId, providerId)
  if (!model) return undefined
  const provider = findProvider(reg, model.providerId)
  if (!provider) return undefined
  const api = resolveApi(reg, model)
  if (!api) return undefined
  return { model, provider: withResolvableAuth(provider), api }
}

/** Best-effort: find any vision-capable model in the registry to use
 *  as an implicit companion when the user hasn't explicitly configured
 *  one. Excludes a specific (modelId, providerId) so we never pick the
 *  routed model itself (it being text-only is why we're here). Returns
 *  the first match; preference can be added later if needed. */
export function findAnyVisionModelRef(exclude?: {
  modelId: string
  providerId: string
}): ModelRef | undefined {
  const reg = getModelRegistry()
  for (const m of reg.models) {
    if (!m.input || !m.input.includes('image')) continue
    if (exclude && m.id === exclude.modelId && m.providerId === exclude.providerId) {
      continue
    }
    const provider = findProvider(reg, m.providerId)
    if (!provider) continue
    const api = resolveApi(reg, m)
    if (!api) continue
    return { model: m, provider: withResolvableAuth(provider), api }
  }
  return undefined
}

export function resolveRoute(routeKey: string, decoder: DecoderKind): ResolvedRoute {
  const routing = routingFor(getRoutingPolicy(), routeKey)
  const target = routing.target
  if (target.kind === 'passthrough') return { kind: 'passthrough' }

  const clientApi = decoderToApi(decoder)
  if (!clientApi) return { kind: 'passthrough' }

  const capabilities = resolveCapabilities(routeKey, routing)

  // target.kind === 'chain' — resolve each member; drop unresolvable ones.
  const members: ResolvedMember[] = []
  for (const m of target.members) {
    const ref = resolveModelRef(m.modelId, m.providerId)
    if (!ref) {
      const where = m.providerId
        ? `model "${m.providerId}/${m.modelId}"`
        : `model "${m.modelId}"`
      logger.warn(
        `routing: ${routeKey} chain member ${where} is unresolvable, skipping`,
      )
      continue
    }
    members.push({ ...ref, switchOn: m.switchOn ?? [] })
  }
  if (members.length === 0) {
    logger.warn(`routing: ${routeKey} chain has no resolvable members, passing through`)
    return { kind: 'passthrough' }
  }

  // 1-member chain → the single-model fast path. switchOn on a lone member
  // can never fire (nothing to switch to), so it's safely dropped.
  if (members.length === 1) {
    const only = members[0]!
    return {
      kind: 'model',
      clientApi,
      model: only.model,
      provider: only.provider,
      api: only.api,
      capabilities,
      configuredTarget: { kind: 'model', id: only.model.id },
    }
  }

  return {
    kind: 'chain',
    clientApi,
    members,
    capabilities,
    configuredTarget: { kind: 'chain', id: members[0]!.model.id },
  }
}

function resolveCapabilities(
  routeKey: string,
  routing: AgentRouting,
): ResolvedCapabilities {
  const raw = routing.capabilities
  if (!raw) return {}
  const out: ResolvedCapabilities = {}
  for (const [id, fulfillment] of Object.entries(raw)) {
    const resolved = resolveOneCapability(routeKey, id as CapabilityId, fulfillment)
    if (resolved) out[id as CapabilityId] = resolved
  }
  return out
}

function resolveOneCapability(
  routeKey: string,
  capabilityId: CapabilityId,
  f: CapabilityFulfillment,
): ResolvedCapability | undefined {
  if (f.via === 'local') {
    return { via: 'local', ...(f.providerId ? { providerId: f.providerId } : {}) }
  }
  const ref = resolveModelRef(f.modelId, f.providerId)
  if (!ref) {
    const where = f.providerId
      ? `model "${f.providerId}/${f.modelId}"`
      : `model "${f.modelId}"`
    logger.warn(
      `routing: ${routeKey} capability ${capabilityId} companion ${where} is unresolvable, ignoring`,
    )
    return undefined
  }
  return { via: 'companion', ref }
}
