import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createAnthropic } from '@ai-sdk/anthropic';
import type { LanguageModel } from 'ai';
import { getLlamaProviders, parseModelRef } from '../llama-providers.js';
import { GATEWAY_KIND, isGatewayVirtualModel } from '@boocode/contracts/gateway';

// v1.13.1-A: AI SDK provider against llama-swap. baseURL is threaded from
// config.LLAMA_SWAP_URL at call time (not module-load) so tests can stub the
// upstream without touching env vars. No apiKey — llama-swap is unauth in our
// Tailscale topology and exposing it over the public internet is gated by
// Authelia at the Caddy layer, not by API keys.
//
// v2.x: provider-aware resolver (W2). One resolver answers provider identity,
// upstream base URL, final wire model id, and DeepSeek
// special handling. Both upstreamModel() and resolveModelEndpoint() go through
// it. Legacy bare-id prefix heuristics live only in the fallback layer.

const swapCache = new Map<string, ReturnType<typeof createOpenAICompatible>>();

function getSwapProvider(baseURL: string, source?: string): ReturnType<typeof createOpenAICompatible> {
  const cacheKey = source ? `${baseURL}||${source}` : baseURL;
  let provider = swapCache.get(cacheKey);
  if (!provider) {
    const fetchWrapper = source
      ? ((...args: Parameters<typeof fetch>) => {
          const [input, init] = args;
          return fetch(input, {
            ...init,
            headers: {
              ...(init?.headers as Record<string, string> | undefined) ?? {},
              'X-Boo-Source': source,
            },
          });
        })
      : undefined;
    provider = createOpenAICompatible({
      name: 'llama-swap',
      baseURL: baseURL.endsWith('/v1') ? baseURL : `${baseURL}/v1`,
      includeUsage: true,
      ...(fetchWrapper ? { fetch: fetchWrapper } : {}),
    }) as ReturnType<typeof createOpenAICompatible>;
    swapCache.set(cacheKey, provider);
  }
  return provider;
}

const DEEPSEEK_MODEL_PREFIX = 'deepseek-';

/**
 * Legacy prefix check — kept for backward compat with bare "deepseek-*" ids.
 * Composite "deepseek/model" is identified by provider id, not prefix.
 */
export function isDeepSeekModel(modelId: string): boolean {
  return modelId.startsWith(DEEPSEEK_MODEL_PREFIX);
}

// Cache keyed by apiKey+baseURL so a runtime env change (and resetDeepSeekProvider)
// can't hand back a provider built with stale credentials.
const deepseekProviderCache = new Map<string, ReturnType<typeof createDeepSeek>>();

function getDeepSeekProvider(
  apiKey: string,
  baseURL: string,
): ReturnType<typeof createDeepSeek> {
  const key = `${apiKey}||${baseURL}`;
  let provider = deepseekProviderCache.get(key);
  if (!provider) {
    provider = createDeepSeek({ apiKey, baseURL });
    deepseekProviderCache.set(key, provider);
  }
  return provider;
}

// P7: 'gateway' routes to the BooControl auto:* gateway (OpenAI-compatible,
// does its own policy routing + failover). 'gateway_error' is the
// present-but-unhealthy / orphaned-session state: the session selected an
// auto:* model but the gateway provider is missing/disabled, so we surface a
// clean error instead of silently mis-routing to LLAMA_SWAP_URL.
export type InferenceRoute = 'swap' | 'deepseek' | 'gateway' | 'gateway_error' | 'anthropic';

const ANTHROPIC_MODEL_PREFIX = 'claude-';

/** Legacy prefix check for bare "claude-*" ids, mirroring isDeepSeekModel. */
export function isAnthropicModel(modelId: string): boolean {
  return modelId.startsWith(ANTHROPIC_MODEL_PREFIX);
}

// Cache keyed by apiKey+baseURL, same rationale as the DeepSeek cache.
const anthropicProviderCache = new Map<string, ReturnType<typeof createAnthropic>>();

function getAnthropicProvider(apiKey: string, baseURL?: string): ReturnType<typeof createAnthropic> {
  const key = `${apiKey}||${baseURL ?? ''}`;
  let provider = anthropicProviderCache.get(key);
  if (!provider) {
    provider = createAnthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });
    anthropicProviderCache.set(key, provider);
  }
  return provider;
}

/** Invalidate the cached Anthropic provider (e.g. when env vars change at runtime). */
export function resetAnthropicProvider(): void {
  anthropicProviderCache.clear();
}

export { GATEWAY_KIND, isGatewayVirtualModel } from '@boocode/contracts/gateway';

export interface ResolvedModel {
  /** Routing destination. */
  route: InferenceRoute;
  /** Upstream base URL for the provider (DeepSeek API base or llama-swap). */
  baseUrl: string;
  /** Wire model id to send upstream (bare, no provider prefix). */
  wireModelId: string;
  /** Whether the input was a legacy bare id resolved through defaultProvider. */
  isLegacyBareId: boolean;
  /** Provider identity (e.g. "sam-desktop", "embedding", "deepseek"). */
  providerId: string;
  /** For route 'gateway_error': why the gateway is unavailable. */
  gatewayReason?: 'offline' | 'unhealthy';
}

interface AgentLike {
  // reserved for future per-agent routing attributes
}

interface ConfigLike {
  LLAMA_SWAP_URL: string;
  DEEPSEEK_API_KEY?: string;
  DEEPSEEK_BASE_URL?: string;
  DEEPSEEK_BETA_BASE_URL?: string;
  ANTHROPIC_API_KEY?: string;
  ANTHROPIC_BASE_URL?: string;
}

/**
 * Provider-aware model resolver. Given a (possibly bare) model id, answers:
 * provider identity, upstream base URL, final bare wire model id, and
 * DeepSeek special handling.
 *
 * Bare ids resolve via defaultProvider (D-2). Composite "provider/model" ids
 * look up the named provider directly. DeepSeek is identified by provider id
 * "deepseek" or by the legacy bare "deepseek-" prefix when DEEPSEEK_API_KEY
 * is configured.
 */
export function resolveModelProvider(
  modelId: string,
  config: ConfigLike,
): ResolvedModel {
  const providers = getLlamaProviders();
  const parsed = parseModelRef(modelId);
  const { providerId, wireModelId, isLegacyBareId } = parsed;

  const deepseekConfigured = !!config.DEEPSEEK_API_KEY;
  const deepseekBaseUrl = (config.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com').replace(/\/+$/, '');

  if (providerId === 'deepseek' && deepseekConfigured) {
    return {
      route: 'deepseek',
      baseUrl: deepseekBaseUrl,
      wireModelId,
      isLegacyBareId,
      providerId: 'deepseek',
    };
  }

  // Bare legacy "deepseek-*" prefix (only when DEEPSEEK_API_KEY is set) →
  // legacy fallback layer — DeepSeek SDK.
  if (isLegacyBareId && isDeepSeekModel(wireModelId) && deepseekConfigured) {
    return {
      route: 'deepseek',
      baseUrl: deepseekBaseUrl,
      wireModelId,
      isLegacyBareId: true,
      providerId: 'deepseek',
    };
  }

  // Hosted Anthropic Claude (different wire: /v1/messages, x-api-key). Routed
  // by provider id "anthropic" or, for bare ids, the legacy "claude-*" prefix
  // when ANTHROPIC_API_KEY is configured. baseUrl is the API base for the
  // streaming path; resolveModelEndpoint (direct OpenAI-shaped fetch) does NOT
  // support this wire and throws.
  const anthropicConfigured = !!config.ANTHROPIC_API_KEY;
  const anthropicBaseUrl = (config.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com').replace(/\/+$/, '');
  if (providerId === 'anthropic' && anthropicConfigured) {
    return { route: 'anthropic', baseUrl: anthropicBaseUrl, wireModelId, isLegacyBareId, providerId: 'anthropic' };
  }
  if (isLegacyBareId && isAnthropicModel(wireModelId) && anthropicConfigured) {
    return { route: 'anthropic', baseUrl: anthropicBaseUrl, wireModelId, isLegacyBareId: true, providerId: 'anthropic' };
  }

  if (isLegacyBareId && isGatewayVirtualModel(wireModelId)) {
    const gw = providers.providers.find((p) => p.kind === GATEWAY_KIND);
    if (gw) {
      return { route: 'gateway', baseUrl: gw.baseUrl, wireModelId, isLegacyBareId, providerId: gw.id };
    }
    return { route: 'gateway_error', baseUrl: '', wireModelId, isLegacyBareId, providerId, gatewayReason: 'offline' };
  }

  const provider = providers.providers.find((p) => p.id === providerId);

  if (provider && provider.kind === GATEWAY_KIND) {
    return {
      route: 'gateway',
      baseUrl: provider.baseUrl,
      wireModelId,
      isLegacyBareId,
      providerId: provider.id,
    };
  }

  if (!provider) {
    // Orphaned auto:* session: the model still looks like a gateway virtual
    // model but no gateway provider is configured. Resolve to a clean
    // gateway_error — NEVER the silent LLAMA_SWAP_URL fallback (design §8).
    if (isGatewayVirtualModel(wireModelId)) {
      return {
        route: 'gateway_error',
        baseUrl: '',
        wireModelId,
        isLegacyBareId,
        providerId,
        gatewayReason: 'offline',
      };
    }
    // Unknown provider — fall back to legacy LLAMA_SWAP_URL for bare ids.
    if (isLegacyBareId) {
      return {
        route: 'swap',
        baseUrl: config.LLAMA_SWAP_URL,
        wireModelId,
          isLegacyBareId: true,
        providerId: 'llama-swap',
      };
    }
    // Composite id with unknown provider — still route to LLAMA_SWAP_URL as
    // a best-effort fallback (the wire model id carries provider intent but
    // the config is incomplete).
    return {
      route: 'swap',
      baseUrl: config.LLAMA_SWAP_URL,
      wireModelId,
      isLegacyBareId: false,
      providerId,
    };
  }

  return {
    route: 'swap',
    baseUrl: provider.baseUrl,
    wireModelId,
    isLegacyBareId,
    providerId: provider.id,
  };
}

/**
 * @deprecated Use resolveModelProvider() for full routing info. Kept for
 * backward compat with resolveRoute() callers that only need the route tag.
 */
export function resolveRoute(
  agent: AgentLike | null,
  config?: ConfigLike,
  modelId?: string,
): { route: InferenceRoute } {
  if (!modelId || !config) return { route: 'swap' };
  const resolved = resolveModelProvider(modelId, config);
  return { route: resolved.route };
}

export function upstreamModel(
  config: ConfigLike,
  modelId: string,
  agent?: AgentLike | null,
  source?: string,
  useBeta?: boolean,
): LanguageModel {
  const resolved = resolveModelProvider(modelId, config);
  if (resolved.route === 'deepseek') {
    const baseUrl = useBeta
      ? (config.DEEPSEEK_BETA_BASE_URL ?? 'https://api.deepseek.com/beta')
      : resolved.baseUrl;
    return getDeepSeekProvider(
      config.DEEPSEEK_API_KEY!,
      baseUrl,
    ).chat(resolved.wireModelId);
  }

  if (resolved.route === 'anthropic') {
    const baseURL = config.ANTHROPIC_BASE_URL
      ? `${config.ANTHROPIC_BASE_URL.replace(/\/+$/, '')}/v1`
      : undefined;
    return getAnthropicProvider(config.ANTHROPIC_API_KEY!, baseURL)(resolved.wireModelId);
  }

  // P7: gateway is OpenAI-compatible — same adapter as swap, pointed at the
  // gateway baseUrl. The gateway resolves the policy + forwards X-Boo-Source.
  if (resolved.route === 'gateway') {
    return getSwapProvider(resolved.baseUrl, source).chatModel(resolved.wireModelId);
  }

  // P7: orphaned auto:* session with no gateway configured — fail loud rather
  // than silently mis-route to LLAMA_SWAP_URL.
  if (resolved.route === 'gateway_error') {
    throw new Error(
      `routing gateway offline (${resolved.gatewayReason ?? 'unavailable'}): ${modelId}`,
    );
  }

  return getSwapProvider(resolved.baseUrl, source).chatModel(resolved.wireModelId);
}

/** Resolve the API endpoint for non-streaming calls (compaction, task-model).
 *  Returns the URL + model + optional auth header for direct fetch() usage. */
export function resolveModelEndpoint(
  config: ConfigLike,
  modelId: string,
): { url: string; model: string; headers: Record<string, string> } {
  const resolved = resolveModelProvider(modelId, config);
  const baseHeaders: Record<string, string> = { 'Content-Type': 'application/json' };

  if (resolved.route === 'deepseek') {
    return {
      url: resolved.baseUrl,
      model: resolved.wireModelId,
      headers: { ...baseHeaders, Authorization: `Bearer ${config.DEEPSEEK_API_KEY}` },
    };
  }

  // Anthropic speaks /v1/messages (x-api-key, blocks content) — the OpenAI-shaped
  // direct fetch these callers use cannot target it. Compaction guards against
  // this by summarizing with a local model; surface a clear error if anything
  // else direct-fetches a Claude model.
  if (resolved.route === 'anthropic') {
    throw new Error(
      `anthropic wire has no OpenAI-compatible direct-fetch endpoint (compaction/task-model): ${modelId}`,
    );
  }

  // P7: orphaned auto:* session with no gateway — fail loud (no swap fallback).
  if (resolved.route === 'gateway_error') {
    throw new Error(
      `routing gateway offline (${resolved.gatewayReason ?? 'unavailable'}): ${modelId}`,
    );
  }

  // P7: gateway uses the same unauthenticated OpenAI-compatible shape as swap.
  // X-Boo-Source forwarding for direct-fetch callers happens at their own header
  // layer (compaction.ts / task-model.ts); the gateway re-forwards it onward.
  return {
    url: resolved.baseUrl.replace(/\/+$/, ''),
    model: resolved.wireModelId,
    headers: baseHeaders,
  };
}

/** Invalidate the cached DeepSeek provider (e.g. when env vars change at runtime). */
export function resetDeepSeekProvider(): void {
  deepseekProviderCache.clear();
}
