///|
pub(all) enum ModelProvider {
  MoonGate
  OpenAICompatible
  FixtureProvider
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) enum ModelHealth {
  Ready
  Degraded
  Unavailable
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) enum ProviderErrorKind {
  ProviderAuthFailed
  ProviderQuotaExhausted
  ProviderRateLimited
  ProviderUnavailable
  ProviderBadRequest
  ProviderUnknown
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ModelCatalogEntry {
  id : String
  owner : String?
  context_window : Int?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ModelUsageSnapshot {
  prompt_tokens_used : Int
  completion_tokens_used : Int
  total_tokens_used : Int
  quota_remaining : Int?
  quota_exhausted : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct SuiteIntegrationContract {
  name : String
  path : String
  ready : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ModelGateway {
  provider : ModelProvider
  base_url : String
  openai_base_url : String
  default_model : String
  health : ModelHealth
  catalog : Array[ModelCatalogEntry]
  usage : ModelUsageSnapshot
  contracts : Array[SuiteIntegrationContract]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ProviderError {
  kind : ProviderErrorKind
  status_code : Int?
  retryable : Bool
  normalized_message : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ModelCallRequest {
  run_id : @domain.RunId
  stage : @routine.ModelStage
  model : String
  base_url : String
  schema_id : String
  messages : Array[@prompt.PromptMessage]
  required_tools : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ModelCallResponse {
  request : ModelCallRequest
  output : @routine.StageOutput
  health : ModelHealth
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn model_catalog_entry(
  id : String,
  owner? : String,
  context_window? : Int,
) -> ModelCatalogEntry {
  { id, owner, context_window }
}

///|
pub fn model_usage_snapshot(
  prompt_tokens_used? : Int = 0,
  completion_tokens_used? : Int = 0,
  quota_remaining? : Int,
) -> ModelUsageSnapshot {
  let total_tokens_used = prompt_tokens_used + completion_tokens_used
  {
    prompt_tokens_used,
    completion_tokens_used,
    total_tokens_used,
    quota_remaining,
    quota_exhausted: quota_remaining is Some(0),
  }
}

///|
pub fn suite_integration_contract(
  name : String,
  path : String,
  ready? : Bool = true,
) -> SuiteIntegrationContract {
  { name, path, ready }
}

///|
fn default_contracts() -> Array[SuiteIntegrationContract] {
  [
    suite_integration_contract(
      "openai-compatible-chat", "contracts/model/openai-compatible-chat",
    ),
    suite_integration_contract(
      "usage-metering", "contracts/model/usage-metering",
    ),
  ]
}

///|
fn gateway_health(
  ready : Bool,
  usage : ModelUsageSnapshot,
  contracts : Array[SuiteIntegrationContract],
  requested : ModelHealth,
) -> ModelHealth {
  if !ready || usage.quota_exhausted {
    Unavailable
  } else if requested is Unavailable {
    Unavailable
  } else if requested is Degraded ||
    contracts.any(fn(contract) { !contract.ready }) {
    Degraded
  } else {
    Ready
  }
}

///|
pub fn moongate_gateway(
  openai_base_url : String,
  default_model : String,
  health? : ModelHealth = Ready,
  catalog? : Array[ModelCatalogEntry] = [],
  usage? : ModelUsageSnapshot = model_usage_snapshot(),
  contracts? : Array[SuiteIntegrationContract] = default_contracts(),
) -> ModelGateway {
  {
    provider: MoonGate,
    base_url: openai_base_url,
    openai_base_url,
    default_model,
    health: gateway_health(
      openai_base_url != "" && default_model != "",
      usage,
      contracts,
      health,
    ),
    catalog,
    usage,
    contracts,
  }
}

///|
pub fn fixture_gateway(model? : String = "fixture-model") -> ModelGateway {
  {
    provider: FixtureProvider,
    base_url: "fixture://model",
    openai_base_url: "fixture://model/v1",
    default_model: model,
    health: Ready,
    catalog: [model_catalog_entry(model)],
    usage: model_usage_snapshot(),
    contracts: default_contracts(),
  }
}

///|
pub fn normalize_provider_error(
  message : String,
  status_code? : Int,
) -> ProviderError {
  let kind = match status_code {
    Some(401) | Some(403) => ProviderAuthFailed
    Some(402) => ProviderQuotaExhausted
    Some(429) => ProviderRateLimited
    Some(400) | Some(422) => ProviderBadRequest
    Some(code) =>
      if code >= 500 {
        ProviderUnavailable
      } else if message.contains("quota") || message.contains("insufficient") {
        ProviderQuotaExhausted
      } else {
        ProviderUnknown
      }
    None =>
      if message.contains("quota") || message.contains("insufficient") {
        ProviderQuotaExhausted
      } else if message.contains("rate") {
        ProviderRateLimited
      } else if message.contains("auth") || message.contains("key") {
        ProviderAuthFailed
      } else if message.contains("unavailable") || message.contains("timeout") {
        ProviderUnavailable
      } else {
        ProviderUnknown
      }
  }
  {
    kind,
    status_code,
    retryable: kind is ProviderRateLimited || kind is ProviderUnavailable,
    normalized_message: match kind {
      ProviderAuthFailed => "provider authentication failed"
      ProviderQuotaExhausted => "provider quota exhausted"
      ProviderRateLimited => "provider rate limited the request"
      ProviderUnavailable => "provider gateway unavailable"
      ProviderBadRequest => "provider rejected the request"
      ProviderUnknown => "provider returned an unclassified error"
    },
  }
}

///|
pub fn model_call_request(
  run_id : @domain.RunId,
  gateway : ModelGateway,
  packet : @prompt.PromptPacket,
  model? : String = gateway.default_model,
) -> ModelCallRequest {
  {
    run_id,
    stage: packet.stage,
    model,
    base_url: gateway.openai_base_url,
    schema_id: packet.schema_id,
    messages: packet.messages,
    required_tools: packet.required_tools,
  }
}

///|
pub fn model_call_response(
  request : ModelCallRequest,
  content : String,
  reasoning_content? : String,
  usage? : @routine.ModelUsage,
  health? : ModelHealth = Ready,
) -> ModelCallResponse {
  {
    request,
    output: @routine.stage_output(
      request.stage,
      content,
      reasoning_content,
      usage,
    ),
    health,
  }
}