import type { Config, ModelConfig } from '../config/index.js';
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClient, BedrockClient, GitHubModelsClient, SyntheticClient, ModelRouter, DEFAULT_RETRY_CONFIG } from '../models/index.js';
import type { ModelClient, RetryConfig, ModelTier } from '../models/index.js';
import { logger } from '../logger.js';
import { getZaiApiKey } from '../auth/zai.js';
import { getAnthropicApiKey, getAnthropicAuthToken } from '../auth/anthropic.js';
import { getOpenAIApiKey, loadStoredOpenAIAuth } from '../auth/openai.js';

type AuthMode = 'auto' | 'api_key' | 'oauth';

function getEffectiveAuthMode(cfg: ModelConfig): AuthMode {
  if (cfg.auth_mode) {
    return cfg.auth_mode;
  }
  if (cfg.use_oauth) {
    return 'oauth';
  }
  return 'auto';
}

/**
 * Resolve an API key from config or environment variable.
 * Throws a clear error naming the expected env var if neither source provides a key.
 */
function requireApiKey(cfg: ModelConfig, envVar: string): string {
  const key = cfg.api_key ?? process.env[envVar];
  if (!key) {
    throw new Error(
      `API key required for ${cfg.provider}. ` +
      `Set ${envVar} environment variable or provide api_key in config.`,
    );
  }
  return key;
}

function resolveAuthCredential(cfg: ModelConfig, apiKeyEnvVar: string, authTokenEnvVar?: string): string {
  const raw = cfg.api_key
    ?? cfg.auth_token
    ?? process.env[apiKeyEnvVar]
    ?? (authTokenEnvVar ? process.env[authTokenEnvVar] : undefined);

  if (!raw) {
    const envHint = authTokenEnvVar ? `${apiKeyEnvVar} or ${authTokenEnvVar}` : apiKeyEnvVar;
    throw new Error(
      `Credential required for ${cfg.provider}. ` +
      `Set ${envHint} environment variable or provide api_key/auth_token in config.`,
    );
  }

  return raw.startsWith('Bearer ') ? raw.slice('Bearer '.length) : raw;
}

/**
 * Create a ModelClient from a provider config entry.
 * Dispatches on the `provider` field so all tiers and fallback entries
 * use the correct client implementation.
 */
export function createClientFromConfig(cfg: ModelConfig): ModelClient {
  switch (cfg.provider) {
    case 'anthropic':
      {
        const authMode = getEffectiveAuthMode(cfg);

        if (authMode === 'oauth') {
          const token = cfg.auth_token ?? getAnthropicAuthToken();
          if (!token) {
            throw new Error(
              'Anthropic auth token not configured (auth_mode: oauth). ' +
              'Set ANTHROPIC_AUTH_TOKEN, run `flynn anthropic-auth --token`, or provide auth_token in config.',
            );
          }
          return new AnthropicClient({
            model: cfg.model,
            authToken: token,
          });
        }

        if (authMode === 'api_key') {
          const apiKey = cfg.api_key ?? getAnthropicApiKey();
          if (!apiKey) {
            throw new Error(
              'Anthropic API key not configured (auth_mode: api_key). ' +
              'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key in config.',
            );
          }
          return new AnthropicClient({
            model: cfg.model,
            apiKey,
          });
        }

        // auto: prefer API key, then token
        const apiKey = cfg.api_key ?? getAnthropicApiKey();
        if (apiKey) {
          return new AnthropicClient({
            model: cfg.model,
            apiKey,
          });
        }

        const token = cfg.auth_token ?? getAnthropicAuthToken();
        if (token) {
          return new AnthropicClient({
            model: cfg.model,
            authToken: token,
          });
        }

        throw new Error(
          'Anthropic credentials not configured (auth_mode: auto). ' +
          'Set ANTHROPIC_API_KEY (or run `flynn anthropic-auth`), ' +
          'or set ANTHROPIC_AUTH_TOKEN (or run `flynn anthropic-auth --token`).',
        );
      }
    case 'openai':
      {
        const authMode = getEffectiveAuthMode(cfg);

        if (authMode === 'oauth') {
          const existing = loadStoredOpenAIAuth();
          if (!existing) {
            throw new Error(
              'OpenAI OAuth is not configured (auth_mode: oauth). ' +
              'Run `flynn openai-auth` to authenticate.',
            );
          }
          return new OpenAIClient({
            model: cfg.model,
            useOAuth: true,
          });
        }

        if (authMode === 'api_key') {
          const apiKey = cfg.api_key ?? getOpenAIApiKey();
          if (!apiKey) {
            throw new Error(
              'OpenAI API key not configured (auth_mode: api_key). ' +
              'Set OPENAI_API_KEY, run `flynn openai-key`, or provide api_key in config.',
            );
          }
          return new OpenAIClient({
            model: cfg.model,
            apiKey,
          });
        }

        // auto: prefer API key, then OAuth
        const apiKey = cfg.api_key ?? getOpenAIApiKey();
        if (apiKey) {
          return new OpenAIClient({
            model: cfg.model,
            apiKey,
          });
        }

        const existing = loadStoredOpenAIAuth();
        if (existing) {
          return new OpenAIClient({
            model: cfg.model,
            useOAuth: true,
          });
        }

        throw new Error(
          'OpenAI credentials not configured (auth_mode: auto). ' +
          'Set OPENAI_API_KEY (or run `flynn openai-key`), ' +
          'or run `flynn openai-auth` for OAuth.',
        );
      }
    case 'ollama':
      return new OllamaClient({
        model: cfg.model,
        host: cfg.endpoint,
        numGpu: cfg.num_gpu,
      });
    case 'llamacpp':
      return new LlamaCppClient({
        endpoint: cfg.endpoint ?? 'http://localhost:8080',
        model: cfg.model,
        authToken: cfg.auth_token,
      });
    case 'gemini':
      return new GeminiClient({
        model: cfg.model,
        apiKey: cfg.api_key,
      });
    case 'openrouter':
      return new OpenAIClient({
        model: cfg.model,
        apiKey: requireApiKey(cfg, 'OPENROUTER_API_KEY'),
        baseURL: cfg.endpoint ?? 'https://openrouter.ai/api/v1',
      });
    case 'vercel':
      return new OpenAIClient({
        model: cfg.model,
        apiKey: requireApiKey(cfg, 'AI_GATEWAY_API_KEY'),
        baseURL: cfg.endpoint ?? 'https://ai-gateway.vercel.sh/v1',
      });
    case 'zhipuai':
      if (cfg.use_oauth) {
        const apiKey = getZaiApiKey();
        if (!apiKey) {
          throw new Error(
            'Z.AI credential not configured. ' +
            'Run `flynn zai-auth` or set ZAI_API_KEY / ZHIPUAI_API_KEY / ZHIPUAI_AUTH_TOKEN.',
          );
        }
        return new OpenAIClient({
          model: cfg.model,
          apiKey,
          baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4',
        });
      }

      return new OpenAIClient({
        model: cfg.model,
        apiKey: resolveAuthCredential(cfg, 'ZHIPUAI_API_KEY', 'ZHIPUAI_AUTH_TOKEN'),
        baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4',
      });
    case 'xai':
      return new OpenAIClient({
        model: cfg.model,
        apiKey: requireApiKey(cfg, 'XAI_API_KEY'),
        baseURL: cfg.endpoint ?? 'https://api.x.ai/v1',
      });
    case 'bedrock':
      return new BedrockClient({
        model: cfg.model,
        region: cfg.endpoint,
        accessKeyId: cfg.api_key,
        secretAccessKey: cfg.auth_token,
      });
    case 'github':
      return new GitHubModelsClient({
        model: anthropicToGitHubModel(cfg.model) ?? cfg.model,
        apiKey: cfg.api_key,
        endpoint: cfg.endpoint,
        onLoginRequired: async () => {
          const { loginGitHub } = await import('../auth/index.js');
          return loginGitHub((userCode, verificationUri) => {
            console.log(`GitHub login required. Visit: ${verificationUri}`);
            console.log(`Enter code: ${userCode}`);
          });
        },
      });
    case 'synthetic':
      return new SyntheticClient({ model: cfg.model });
    default:
      throw new Error(`Unknown model provider: ${(cfg as Record<string, unknown>).provider}`);
  }
}

/**
 * Map an Anthropic model identifier to its GitHub Models equivalent.
 * Returns undefined if no mapping is known.
 *
 * Anthropic uses hyphens and date suffixes:  claude-sonnet-4-5-20250929
 * GitHub Copilot uses dots, no dates:        claude-sonnet-4.5
 */
export function anthropicToGitHubModel(anthropicModel: string): string | undefined {
  // Explicit mappings for known models
  const MAPPINGS: Record<string, string> = {
    // Sonnet family
    'claude-sonnet-4-20250514': 'claude-sonnet-4',
    'claude-sonnet-4-5-20250929': 'claude-sonnet-4.5',
    // Opus family
    'claude-opus-4-20250514': 'claude-opus-4',
    'claude-opus-4-5-20250918': 'claude-opus-4.5',
    'claude-opus-4-6-20250715': 'claude-opus-4.6',
    // Haiku family
    'claude-3-5-haiku-20241022': 'claude-haiku-4.5',
    'claude-haiku-4-5-20251001': 'claude-haiku-4.5',
  };

  if (MAPPINGS[anthropicModel]) {return MAPPINGS[anthropicModel];}

  // Generic fallback: strip date suffix, then convert trailing -N to .N
  // only when preceded by another digit (i.e. "4-5" → "4.5", not "sonnet-5" → "sonnet.5")
  // e.g. "claude-sonnet-4-7-20260301" → "claude-sonnet-4-7" → "claude-sonnet-4.7"
  const dateMatch = anthropicModel.match(/^(.+)-\d{8}$/);
  if (dateMatch) {
    const base = dateMatch[1];
    // Convert "claude-sonnet-4-5" → "claude-sonnet-4.5" (digit-hyphen-digit at end)
    const dotted = base.replace(/(\d)-(\d+)$/, '$1.$2');
    return dotted;
  }

  return undefined;
}

/**
 * For a given tier config using the Anthropic provider, create a GitHub Models
 * client with the equivalent model as an automatic same-model fallback.
 * Returns undefined if no mapping exists or the tier isn't Anthropic.
 */
export function createAutoFallbackClient(tierConfig: { provider: string; model: string }): ModelClient | undefined {
  if (tierConfig.provider !== 'anthropic') {return undefined;}

  const githubModel = anthropicToGitHubModel(tierConfig.model);
  if (!githubModel) {return undefined;}

  return new GitHubModelsClient({
    model: githubModel,
    onLoginRequired: async () => {
      const { loginGitHub } = await import('../auth/index.js');
      return loginGitHub((userCode, verificationUri) => {
        console.log(`GitHub login required. Visit: ${verificationUri}`);
        console.log(`Enter code: ${userCode}`);
      });
    },
  });
}

export function createModelRouter(config: Config): ModelRouter {
  const models = config.models;

  const defaultClient = createClientFromConfig(models.default);

  const fastClient = models.fast ? createClientFromConfig(models.fast) : undefined;
  const complexClient = models.complex ? createClientFromConfig(models.complex) : undefined;
  const localClient = models.local ? createClientFromConfig(models.local) : undefined;

  // Build fallback chain — each entry references a tier name or 'local'
  const fallbackChain: ModelClient[] = [];
  for (const providerName of models.fallback_chain) {
    if (providerName === 'local' && localClient) {
      fallbackChain.push(localClient);
    } else if (providerName === 'default') {
      // Allows re-trying the default provider in the chain
      fallbackChain.push(defaultClient);
    } else if (providerName === 'fast' && fastClient) {
      fallbackChain.push(fastClient);
    } else if (providerName === 'complex' && complexClient) {
      fallbackChain.push(complexClient);
    } else if (models.local_providers?.[providerName]) {
      // Named provider from local_providers map
      fallbackChain.push(createClientFromConfig(models.local_providers[providerName]));
    } else {
      logger.warn(`Fallback chain entry "${providerName}" not found — skipping`);
    }
  }

  // Build per-tier fallbacks from inline fallback config + auto same-model fallbacks.
  // Auto-fallback: when a tier uses Anthropic, automatically insert a GitHub Models
  // client for the same model *before* any user-configured inline fallbacks.
  // This ensures the same model is tried via an alternative provider before
  // degrading to the global fallback chain (which may be a much weaker local model).
  const tierFallbacks = new Map<ModelTier, ModelClient[]>();

  const tierConfigs: { tier: ModelTier; cfg: typeof models.default | undefined }[] = [
    { tier: 'default', cfg: models.default },
    { tier: 'fast', cfg: models.fast },
    { tier: 'complex', cfg: models.complex },
    { tier: 'local', cfg: models.local },
  ];

  const autoFallbackTiers: string[] = [];
  for (const { tier, cfg } of tierConfigs) {
    if (!cfg) {continue;}

    const fallbackList: ModelClient[] = [];

    // Auto same-model fallback (only when user hasn't configured an inline fallback)
    if (!cfg.fallback) {
      const autoClient = createAutoFallbackClient(cfg);
      if (autoClient) {
        fallbackList.push(autoClient);
        autoFallbackTiers.push(tier);
      }
    }

    // User-configured inline fallback
    if (cfg.fallback) {
      fallbackList.push(createClientFromConfig(cfg.fallback));
    }

    if (fallbackList.length > 0) {
      tierFallbacks.set(tier, fallbackList);
    }
  }

  if (tierFallbacks.size > 0) {
    const tierNames = Array.from(tierFallbacks.keys()).join(', ');
    logger.info(`Per-tier fallbacks configured for: ${tierNames}`);
  }
  if (autoFallbackTiers.length > 0) {
    logger.info(`Auto same-model fallback (via GitHub Models) for: ${autoFallbackTiers.join(', ')}`);
  }

  logger.info(`Model router: default=${models.default.provider}/${models.default.model}, ` +
    `fallback=[${models.fallback_chain.join(', ')}]`);

  // Build retry config if enabled
  const retryConfig: RetryConfig | undefined = config.retry.enabled ? {
    maxRetries: config.retry.max_retries,
    initialDelayMs: config.retry.initial_delay_ms,
    backoffMultiplier: config.retry.backoff_multiplier,
    maxDelayMs: config.retry.max_delay_ms,
    nonRetryablePatterns: DEFAULT_RETRY_CONFIG.nonRetryablePatterns,
  } : undefined;

  if (retryConfig) {
    logger.info(`Retry policy: max_retries=${retryConfig.maxRetries}, initial_delay=${retryConfig.initialDelayMs}ms`);
  }

  return new ModelRouter({
    default: defaultClient,
    fast: fastClient,
    complex: complexClient,
    local: localClient,
    fallbackChain,
    tierFallbacks,
    retryConfig,
    labels: {
      default: `${models.default.provider}/${models.default.model}`,
      ...(models.fast ? { fast: `${models.fast.provider}/${models.fast.model}` } : {}),
      ...(models.complex ? { complex: `${models.complex.provider}/${models.complex.model}` } : {}),
      ...(models.local ? { local: `${models.local.provider}/${models.local.model}` } : {}),
    },
  });
}
