import { Context, Layer } from "effect";
import type { LLMProvider, EmbeddingConfig, ObservabilityVerbosity } from "./types.js";

/**
 * LLM service configuration.
 * Provides API keys, default model settings, timeouts, and observability verbosity.
 * Typically constructed from environment variables via llmConfigFromEnv.
 *
 * @example
 * ```typescript
 * const config = LLMConfig.of({
 *   defaultProvider: "anthropic",
 *   defaultModel: "claude-opus-4-20250514",
 *   anthropicApiKey: process.env.ANTHROPIC_API_KEY,
 *   maxRetries: 3,
 *   timeoutMs: 30000
 * });
 * ```
 */
export class LLMConfig extends Context.Tag("LLMConfig")<
  LLMConfig,
  {
    /**
     * Default LLM provider.
     * Used as fallback when a request does not specify a provider.
     *
     * @default "anthropic"
     */
    readonly defaultProvider: LLMProvider;

    /**
     * Default LLM model identifier.
     * Used as fallback when a request does not specify a model.
     *
     * @default From LLM_DEFAULT_MODEL env var, falls back to "claude-sonnet-4-6"
     */
    readonly defaultModel: string;

    /**
     * Anthropic API key.
     * Retrieved from ANTHROPIC_API_KEY environment variable.
     * Required if provider is "anthropic".
     *
     * @default From ANTHROPIC_API_KEY env var (undefined if not set)
     */
    readonly anthropicApiKey?: string;

    /**
     * OpenAI API key.
     * Retrieved from OPENAI_API_KEY environment variable.
     * Required if provider is "openai".
     *
     * @default From OPENAI_API_KEY env var (undefined if not set)
     */
    readonly openaiApiKey?: string;

    /**
     * Google API key.
     * Retrieved from GOOGLE_API_KEY environment variable.
     * Required if provider is "gemini".
     *
     * @default From GOOGLE_API_KEY env var (undefined if not set)
     */
    readonly googleApiKey?: string;

    /**
     * Groq API key (OpenAI-compatible LPU inference).
     * Retrieved from GROQ_API_KEY environment variable.
     * Required if provider is "groq".
     *
     * @default From GROQ_API_KEY env var (undefined if not set)
     */
    readonly groqApiKey?: string;

    /**
     * Base URL override for the Groq OpenAI-compatible endpoint.
     * Retrieved from GROQ_BASE_URL environment variable.
     *
     * @default "https://api.groq.com/openai/v1"
     */
    readonly groqBaseUrl?: string;

    /**
     * xAI API key (OpenAI-compatible Grok models).
     * Retrieved from XAI_API_KEY environment variable.
     * Required if provider is "xai".
     *
     * @default From XAI_API_KEY env var (undefined if not set)
     */
    readonly xaiApiKey?: string;

    /**
     * Base URL override for the xAI OpenAI-compatible endpoint.
     * Retrieved from XAI_BASE_URL environment variable.
     *
     * @default "https://api.x.ai/v1"
     */
    readonly xaiBaseUrl?: string;

    /**
     * Ollama server endpoint.
     * Retrieved from OLLAMA_ENDPOINT environment variable.
     * Used for local model serving.
     *
     * @default "http://localhost:11434"
     */
    readonly ollamaEndpoint?: string;

    /**
     * Ollama API key for cloud-hosted Ollama-compatible endpoints.
     * Retrieved from OLLAMA_API_KEY environment variable.
     * Sent as a Bearer token in the Authorization header.
     * Not needed for local Ollama instances.
     *
     * @default From OLLAMA_API_KEY env var (undefined if not set)
     */
    readonly ollamaApiKey?: string;

    /**
     * Embedding configuration — model, provider, dimensions.
     * Anthropic has no embeddings API; embeddings always route to OpenAI or Ollama.
     * This is the sole embedding config for the entire framework.
     * Used by semantic cache, memory similarity search, and verification layers.
     *
     * @default { model: "text-embedding-3-small", dimensions: 1536, provider: "openai", batchSize: 100 }
     */
    readonly embeddingConfig: EmbeddingConfig;

    /**
     * Enable Anthropic prompt caching.
     * When true, memory context injections and system prompts are wrapped in
     * `cache_control: { type: "ephemeral" }` blocks to reduce costs.
     * Non-Anthropic providers silently ignore cache control directives.
     * Automatically set to true if defaultModel starts with "claude".
     *
     * @default true if defaultModel starts with "claude", false otherwise
     */
    readonly supportsPromptCaching: boolean;

    /**
     * Maximum number of retries for transient LLM request failures.
     * Applied with exponential backoff (2^n seconds between attempts).
     *
     * @default 3
     */
    readonly maxRetries: number;

    /**
     * Request timeout in milliseconds.
     * LLM requests exceeding this duration are aborted.
     *
     * @default 30000 (30 seconds)
     */
    readonly timeoutMs: number;

    /**
     * Per-call hard timeout (ms) for the local (Ollama) provider.
     *
     * Distinct from {@link timeoutMs} on purpose: local models routinely need
     * far longer than a hosted call — a cold model load or a GPU swap under
     * contention can push a single generation past two minutes (observed
     * 2m31s on a dev box, 2026-07-01). When set, this is the ceiling
     * `providers/local.ts` applies to `complete()`; on breach the in-flight
     * Ollama HTTP request is aborted so the server stops generating.
     *
     * Precedence (see `providers/local.ts` resolveLocalTimeoutMs):
     * `request.timeoutMs` → `ollamaTimeoutMs` → cold-load-tolerant default.
     *
     * NOTE: the builder's `.withTimeout()` currently maps to the
     * execution-engine whole-run timeout, NOT to this field — wiring it here is
     * a runtime-side change outside llm-provider's authority. Set via
     * `OLLAMA_TIMEOUT_MS` or a config override until that lands.
     *
     * @default undefined (local provider falls back to its 300s cold-load default)
     */
    readonly ollamaTimeoutMs?: number;

    /**
     * Enable/disable thinking mode for thinking-capable models.
     * - `true` — Always enable thinking (e.g., qwen3.5, DeepSeek-R1)
     * - `false` — Always disable thinking (e.g., cogito:14b that crashes with think:true)
     * - `undefined` — Auto-detect based on model capabilities (Ollama only)
     *
     * @default undefined (auto-detect)
     */
    readonly thinking?: boolean;

    /**
     * Rich thinking configuration (effort level, explicit budget). The
     * `thinking` boolean remains authoritative for on/off — adapters resolve
     * enablement from `config.thinking` via `resolveThinkingEnabled`, then
     * stamp that onto `thinkingOptions.enabled`. `thinkingOptions` carries the
     * secondary knobs (effort, budgetTokens), not the on/off decision.
     * Written by `.withThinking()`. See `@reactive-agents/llm-provider` thinking module.
     */
    readonly thinkingOptions?: import("./thinking/index.js").ThinkingOptions;

    /**
     * Default maximum output tokens for LLM responses.
     * Used if a CompletionRequest does not specify maxTokens.
     * Set lower for faster responses; higher for longer outputs.
     *
     * @default 4096
     */
    readonly defaultMaxTokens: number;

    /**
     * Default sampling temperature (0.0-1.0).
     * Used if a CompletionRequest does not specify temperature.
     * 0.0 = deterministic; 1.0 = maximum randomness.
     *
     * @default 0.7 (good balance of creativity and coherence)
     */
    readonly defaultTemperature: number;

    /**
     * Default context window size (in tokens) for local providers.
     *
     * @deprecated Phase 1 S1.3 introduced the `Capability` port — Ollama
     * `options.num_ctx` now resolves from `capability.recommendedNumCtx`
     * via `resolveCapability(provider, model)` for known models, with a
     * conservative 2048 fallback for unknowns. This `defaultNumCtx` field
     * remains as the lowest-priority fallback for backwards compatibility
     * (precedence: `request.numCtx` → `capability.recommendedNumCtx` →
     * `defaultNumCtx`) but in practice the capability path always supplies
     * a value, making this field effectively dead code. Removal in Phase 2
     * once all callers migrate.
     *
     * @default undefined (capability-driven resolution always wins)
     */
    readonly defaultNumCtx?: number;

    /**
     * User-explicit context window size (in tokens) for local providers.
     *
     * Unlike {@link defaultNumCtx} (a backwards-compat *floor* for unknown
     * models), this is a deliberate override supplied by the caller via
     * `.withModel({ numCtx })` or `agent.run(task, { numCtx })`. It ranks
     * ABOVE `capability.recommendedNumCtx` so a user who knows their GPU has
     * headroom can widen the window past the conservative 32K cap, or narrow
     * it to fit a constrained machine.
     *
     * Precedence (see `providers/local.ts`):
     * `request.numCtx` → `explicitNumCtx` → `capability.recommendedNumCtx` → `defaultNumCtx`.
     *
     * Provider-agnostic: only Ollama/local maps this to `options.num_ctx`;
     * hosted providers (Anthropic/OpenAI/Gemini) have no request-level context
     * knob and ignore it, exactly as they ignore an unsupported field.
     *
     * @default undefined (capability-driven resolution wins)
     */
    readonly explicitNumCtx?: number;

    /**
     * LLM request/response observability verbosity.
     * Determines what data is captured in LLMRequestEvent for observability.
     *
     * - **"full"**: Capture complete request/response payloads (useful for debugging, higher overhead)
     * - **"metadata"**: Capture only timing, token counts, and cost (lightweight, production-safe)
     *
     * @default "full" (capture everything)
     *
     * @example
     * ```typescript
     * // Development: full details
     * observabilityVerbosity: process.env.NODE_ENV === "production" ? "metadata" : "full"
     * ```
     */
    readonly observabilityVerbosity: ObservabilityVerbosity;

    /**
     * Custom pricing registry for calculating token costs.
     * Maps model identifiers to input/output token costs per 1 million tokens.
     * Overrides built-in framework pricing if an exact match is found.
     *
     * @example
     * ```typescript
     * pricingRegistry: {
     *   "my-fine-tuned-model": { input: 0.5, output: 1.5 }
     * }
     * ```
     */
    readonly pricingRegistry?: Record<string, { readonly input: number; readonly output: number }>;
  }
>() {}

/**
 * Raw LLMConfig object constructed from environment variables.
 * Reads all config from process.env with sensible defaults.
 * Exported so callers can spread overrides (e.g. change model) on top.
 *
 * Environment variables:
 * - LLM_DEFAULT_MODEL: Model identifier (default: claude-sonnet-4-6)
 * - ANTHROPIC_API_KEY: Anthropic API key
 * - OPENAI_API_KEY: OpenAI API key
 * - GOOGLE_API_KEY: Google API key
 * - OLLAMA_ENDPOINT: Ollama server URL (default: http://localhost:11434)
 * - EMBEDDING_MODEL: Embedding model name (default: text-embedding-3-small)
 * - EMBEDDING_DIMENSIONS: Embedding vector dimensions (default: 1536)
 * - EMBEDDING_PROVIDER: Embedding provider (default: openai)
 * - LLM_MAX_RETRIES: Retry attempts (default: 3)
 * - LLM_TIMEOUT_MS: Request timeout in ms (default: 30000)
 * - LLM_DEFAULT_TEMPERATURE: Sampling temperature (default: 0.7)
 * - LLM_OBSERVABILITY_VERBOSITY: "full" or "metadata" (default: full)
 *
 * @example
 * ```typescript
 * // Use defaults from environment
 * const config = llmConfigFromEnv;
 *
 * // Override specific fields
 * const customConfig = LLMConfig.of({
 *   ...llmConfigFromEnv,
 *   defaultModel: "gpt-4o",
 *   defaultProvider: "openai"
 * });
 * ```
 */
export const llmConfigFromEnv = LLMConfig.of({
  defaultProvider: "anthropic",
  defaultModel:
    process.env.LLM_DEFAULT_MODEL || "claude-sonnet-4-6",
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
  openaiApiKey: process.env.OPENAI_API_KEY,
  googleApiKey: process.env.GOOGLE_API_KEY,
  groqApiKey: process.env.GROQ_API_KEY,
  ...(process.env.GROQ_BASE_URL !== undefined
    ? { groqBaseUrl: process.env.GROQ_BASE_URL }
    : {}),
  xaiApiKey: process.env.XAI_API_KEY,
  ...(process.env.XAI_BASE_URL !== undefined
    ? { xaiBaseUrl: process.env.XAI_BASE_URL }
    : {}),
  ollamaEndpoint:
    process.env.OLLAMA_ENDPOINT ?? "http://localhost:11434",
  ollamaApiKey: process.env.OLLAMA_API_KEY,
  embeddingConfig: {
    model: process.env.EMBEDDING_MODEL ?? "text-embedding-3-small",
    dimensions: Number(process.env.EMBEDDING_DIMENSIONS ?? 1536),
    provider:
      (process.env.EMBEDDING_PROVIDER as "openai" | "ollama") ?? "openai",
    batchSize: 100,
  },
  supportsPromptCaching: (() => {
    const m = process.env.LLM_DEFAULT_MODEL || "claude-sonnet-4-6";
    // Anthropic: explicit cache_control on system prompts + tools (manual)
    // Gemini: automatic implicit caching (server-side, 75% discount on cached tokens)
    // OpenAI: automatic caching (server-side, 50% discount on cached tokens)
    return m.startsWith("claude") || m.startsWith("gemini") || m.startsWith("gpt");
  })(),
  maxRetries: Number(process.env.LLM_MAX_RETRIES ?? 3),
  timeoutMs: Number(process.env.LLM_TIMEOUT_MS ?? 30_000),
  ...(process.env.OLLAMA_TIMEOUT_MS !== undefined
    ? { ollamaTimeoutMs: Number(process.env.OLLAMA_TIMEOUT_MS) }
    : {}),
  defaultMaxTokens: 4096,
  defaultTemperature: Number(process.env.LLM_DEFAULT_TEMPERATURE ?? 0.7),
  ...(process.env.LLM_DEFAULT_NUM_CTX !== undefined
    ? { defaultNumCtx: Number(process.env.LLM_DEFAULT_NUM_CTX) }
    : {}),
  observabilityVerbosity: (process.env.LLM_OBSERVABILITY_VERBOSITY as ObservabilityVerbosity | undefined) ?? "full",
  pricingRegistry: {},
});

/**
 * Effect-TS Layer that provides LLMConfig from environment variables.
 * Use this layer to automatically populate LLMConfig from process.env.
 * Can be overridden with a custom layer for testing or custom configuration.
 *
 * @example
 * ```typescript
 * const effect = Effect.gen(function* () {
 *   const config = yield* LLMConfig;
 *   console.log(config.defaultModel);
 * }).pipe(Effect.provide(LLMConfigFromEnv));
 *
 * Effect.runPromise(effect);
 * ```
 *
 * @see llmConfigFromEnv
 */
export const LLMConfigFromEnv = Layer.succeed(LLMConfig, llmConfigFromEnv);
