import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { getPackageRoot } from '../paths';

/**
 * Credential Resolution — multi-source API key + endpoint detection.
 *
 * Precedence:
 *   1. Explicit CLI flags (--api-key, --provider, --base-url)
 *   2. External config files (e.g. OpenClaw auth-profiles, if installed)
 *   3. Environment variables (AI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
 *   4. Local .clawdcursor-config.json
 *
 * External integrations (OpenClaw, etc.) are optional — Clawd Cursor works
 * fully standalone with just env vars or CLI flags.
 */

export interface ResolvedApiConfig {
  provider?: string;
  apiKey: string;
  baseUrl?: string;
  textModel?: string;
  visionModel?: string;
  textApiKey?: string;
  textBaseUrl?: string;
  visionApiKey?: string;
  visionBaseUrl?: string;
  source: 'external' | 'local';
}

interface ModelInfo {
  id: string;
  input: string[];
}

interface ProviderInfo {
  key: string;
  apiKey?: string;
  baseUrl?: string;
  models: ModelInfo[];
}


function normalizeProviderKey(key: string): string {
  const lower = key.toLowerCase().trim();
  if (!lower) return lower;
  return lower.split(':')[0];
}

function normalizeProvider(provider?: string): string | undefined {
  if (!provider) return undefined;
  const p = provider.trim().toLowerCase();
  return p.length > 0 ? p : undefined;
}

function toEnvStyleKey(value: string): string {
  return value
    .replace(/[^a-zA-Z0-9]+/g, '_')
    .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
    .replace(/^_+|_+$/g, '')
    .toUpperCase();
}

function normalizeBaseUrl(url?: string): string | undefined {
  if (!url) return undefined;
  const trimmed = url.trim().replace(/\/+$/, '');
  return trimmed.length > 0 ? trimmed : undefined;
}

function pick(...values: Array<string | undefined>): string | undefined {
  for (const v of values) {
    if (v && v.trim()) return v.trim();
  }
  return undefined;
}

export function inferProviderFromBaseUrl(baseUrl?: string): string | undefined {
  const url = (baseUrl || '').toLowerCase();
  if (!url) return undefined;
  if (url.includes('anthropic')) return 'anthropic';
  if (url.includes('moonshot') || url.includes('kimi')) return 'kimi';
  if (url.includes('11434') || url.includes('ollama')) return 'ollama';
  if (url.includes('openai')) return 'openai';
  if (url.includes('groq')) return 'groq';
  if (url.includes('together')) return 'together';
  if (url.includes('deepseek')) return 'deepseek';
  if (url.includes('nvidia') || url.includes('integrate.api')) return 'nvidia';
  if (url.includes('mistral')) return 'mistral';
  if (url.includes('fireworks')) return 'fireworks';
  if (url.includes('dashscope') || url.includes('alibaba') || url.includes('qwen')) return 'alibaba';
  if (url.includes('cohere')) return 'cohere';
  if (url.includes('perplexity') || url.includes('pplx')) return 'perplexity';
  // Unknown endpoint — still works, just no provider label
  return undefined;
}

function safeReadJson(filePath: string): any | null {
  try {
    if (!fs.existsSync(filePath)) return null;
    const raw = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

function isObject(value: unknown): value is Record<string, any> {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function extractApiKeyLike(value: any): string | undefined {
  if (!value) return undefined;
  if (typeof value === 'string') return value;
  if (isObject(value)) {
    return pick(
      value.apiKey,
      value.api_key,
      value.key,
      value.token,
      value.accessToken,
      value.access_token,
      value.access,
      value.oauthAccessToken,
      value.oauth_access_token,
      value.bearer,
      value.oauth?.accessToken,
      value.oauth?.access_token,
      value.oauth?.access,
    );
  }
  return undefined;
}

function resolveTemplateVars(value: string, envSources: Array<Record<string, any> | undefined>): string | undefined {
  const trimmed = value.trim();
  if (!trimmed) return undefined;

  const hasTemplate = /\$\{[^}]+\}/.test(trimmed);
  if (!hasTemplate) return trimmed;

  let unresolved = false;
  const expanded = trimmed.replace(/\$\{([^}]+)\}/g, (_, key: string) => {
    const normalizedKey = key.trim();
    for (const source of envSources) {
      const candidate = source?.[normalizedKey];
      if (typeof candidate === 'string' && candidate.length > 0) {
        return candidate;
      }
    }
    unresolved = true;
    return '';
  });

  if (unresolved) return undefined;
  return expanded;
}

function extractResolvedApiKey(value: any, envSources: Array<Record<string, any> | undefined>): string | undefined {
  const apiKey = extractApiKeyLike(value);
  if (!apiKey) return undefined;
  return resolveTemplateVars(apiKey, envSources);
}

function extractBaseUrlLike(value: any): string | undefined {
  if (!value) return undefined;
  if (typeof value === 'string') return normalizeBaseUrl(value);
  if (isObject(value)) {
    return normalizeBaseUrl(pick(value.baseUrl, value.base_url, value.endpoint, value.url));
  }
  return undefined;
}

function normalizeModelEntry(model: any): ModelInfo | null {
  if (!model) return null;
  const id = typeof model === 'string'
    ? model
    : pick(model.id, model.model, model.modelId, model.name);
  if (!id) return null;

  const input = Array.isArray(model.input)
    ? model.input.map((x: any) => String(x).toLowerCase())
    : [];

  return { id, input };
}

function getOpenClawRoots(): string[] {
  const home = os.homedir();
  return [
    path.join(home, '.openclaw'),
    path.join(home, '.openclaw-dev'),
  ];
}

function readConfiguredProvider(): string | undefined {
  // Check both the package directory (where the code lives) and cwd
  const pkgConfigPath = path.join(getPackageRoot(), '.clawdcursor-config.json');
  const cwdConfigPath = path.join(process.cwd(), '.clawdcursor-config.json');
  const cfg = safeReadJson(pkgConfigPath) || safeReadJson(cwdConfigPath);
  if (!cfg || !isObject(cfg)) return undefined;

  const provider = pick(cfg.provider, cfg?.pipeline?.provider, cfg?.pipeline?.providerKey);
  return normalizeProvider(provider);
}

function providerEnvCandidates(provider?: string): string[] {
  const normalized = normalizeProvider(provider);
  if (!normalized) return [];

  const bases = [normalized, normalized.split(':')[0]].filter(Boolean);
  const seen = new Set<string>();
  const keys: string[] = [];
  for (const base of bases) {
    const envBase = toEnvStyleKey(base);
    if (!envBase || seen.has(envBase)) continue;
    seen.add(envBase);
    keys.push(`${envBase}_API_KEY`);
  }
  return keys;
}

function loadOpenClawProviderMap(): Record<string, ProviderInfo> {
  const providers: Record<string, ProviderInfo> = {};

  const ensureProvider = (key: string): ProviderInfo => {
    const normalizedKey = normalizeProviderKey(key);
    if (!providers[normalizedKey]) {
      providers[normalizedKey] = { key: normalizedKey, models: [] };
    }
    return providers[normalizedKey];
  };

  for (const root of getOpenClawRoots()) {
    const authCandidates = [
      path.join(root, 'agents', 'main', 'agent', 'auth-profiles.json'),
      path.join(root, 'agents', 'main', 'auth-profiles.json'),
    ];

    const configCandidates = [
      path.join(root, 'openclaw.json'),
      path.join(root, 'agents', 'main', 'openclaw.json'),
      path.join(root, 'agents', 'main', 'agent', 'openclaw.json'),
    ];

    for (const authPath of authCandidates) {
      const auth = safeReadJson(authPath);
      if (!auth) continue;

      const containers = [auth, auth.profiles, auth.providers, auth.authProfiles];
      for (const container of containers) {
        if (!isObject(container)) continue;
        for (const [key, value] of Object.entries(container)) {
          const entry = ensureProvider(key);
          const apiKey = extractApiKeyLike(value);
          const baseUrl = extractBaseUrlLike(value);
          if (apiKey) entry.apiKey = apiKey;
          if (baseUrl) entry.baseUrl = baseUrl;

          const maybeModels = isObject(value) ? (value.models || value.availableModels) : null;
          if (Array.isArray(maybeModels)) {
            for (const m of maybeModels) {
              const normalized = normalizeModelEntry(m);
              if (normalized) entry.models.push(normalized);
            }
          }
        }
      }
    }

    for (const configPath of configCandidates) {
      const cfg = safeReadJson(configPath);
      if (!cfg) continue;

      const envSources = [cfg.env, process.env];

      const providerBlocks = [
        cfg?.models?.providers,
        cfg?.providers,
      ];

      for (const block of providerBlocks) {
        if (!isObject(block)) continue;
        for (const [key, value] of Object.entries(block)) {
          const entry = ensureProvider(key);
          const apiKey = extractResolvedApiKey(value, envSources);
          const baseUrl = extractBaseUrlLike(value);
          if (apiKey && !entry.apiKey) entry.apiKey = apiKey;
          if (baseUrl && !entry.baseUrl) entry.baseUrl = baseUrl;

          if (isObject(value) && Array.isArray(value.models)) {
            for (const m of value.models) {
              const normalized = normalizeModelEntry(m);
              if (normalized) entry.models.push(normalized);
            }
          }
        }
      }
    }
  }

  // De-duplicate models by id
  for (const entry of Object.values(providers)) {
    const seen = new Set<string>();
    entry.models = entry.models.filter(m => {
      if (seen.has(m.id)) return false;
      seen.add(m.id);
      return true;
    });
  }

  return providers;
}

function selectVisionProvider(providers: ProviderInfo[]): ProviderInfo | null {
  for (const p of providers) {
    if (p.models.some(m => m.input.includes('image') || m.input.includes('vision'))) {
      return p;
    }
  }
  return null;
}

function isLikelyLocalProvider(provider: ProviderInfo | undefined): boolean {
  const url = (provider?.baseUrl || '').toLowerCase();
  return provider?.key === 'ollama' || url.includes('localhost') || url.includes('127.0.0.1') || url.includes('11434');
}

function selectProviderWithApiKey(providers: ProviderInfo[]): ProviderInfo | null {
  for (const p of providers) {
    if (p.apiKey) return p;
  }
  return null;
}

function selectTextProvider(providers: ProviderInfo[]): ProviderInfo | null {
  const withTextModel = providers.find(p => p.models.some(m => !m.input.includes('image') && !m.input.includes('vision')));
  if (withTextModel) return withTextModel;
  return providers.find(p => p.models.length > 0) || null;
}

function resolveFromOpenClawFiles(): ResolvedApiConfig | null {
  const providerMap = loadOpenClawProviderMap();
  const providerList = Object.values(providerMap).filter(p => !!(p.apiKey || p.baseUrl || p.models.length > 0));
  if (providerList.length === 0) return null;

  const configuredProvider = readConfiguredProvider();
  const preferredProvider = configuredProvider ? providerMap[normalizeProviderKey(configuredProvider)] : undefined;
  const preferredCandidates = preferredProvider ? [preferredProvider] : providerList;

  const preferredProviderWithKey = selectProviderWithApiKey(preferredCandidates);
  const globalProviderWithKey = selectProviderWithApiKey(providerList);
  const visionProvider = selectVisionProvider(preferredCandidates) || selectVisionProvider(providerList);
  const textProvider = selectTextProvider(preferredCandidates) || selectTextProvider(providerList);

  const selectedProvider = preferredProviderWithKey
    || preferredProvider
    || visionProvider
    || textProvider
    || globalProviderWithKey;
  if (!selectedProvider) return null;

  const rawVisionModel = visionProvider?.models.find(m => m.input.includes('image') || m.input.includes('vision'))?.id
    || textProvider?.models[0]?.id;
  // Find a text-only model. Do NOT fall back to vision models — they may be
  // reasoning models (kimi-k2.5) that reject temperature=0 and cost more.
  const rawTextModel = textProvider?.models.find(m => !m.input.includes('image') && !m.input.includes('vision'))?.id;

  const textModel = rawTextModel;
  const visionModel = rawVisionModel;

  const resolvedApiKey = selectedProvider.apiKey || textProvider?.apiKey || visionProvider?.apiKey || globalProviderWithKey?.apiKey || '';

  if (!resolvedApiKey && !isLikelyLocalProvider(selectedProvider)) {
    return null;
  }

  const resolvedTextApiKey = textProvider?.apiKey || selectedProvider.apiKey || resolvedApiKey;
  const resolvedVisionApiKey = visionProvider?.apiKey || selectedProvider.apiKey || resolvedApiKey;
  const resolvedBaseUrl = selectedProvider.baseUrl || visionProvider?.baseUrl || textProvider?.baseUrl;
  const resolvedTextBaseUrl = textProvider?.baseUrl || selectedProvider.baseUrl || visionProvider?.baseUrl;
  const resolvedVisionBaseUrl = visionProvider?.baseUrl || selectedProvider.baseUrl || textProvider?.baseUrl;

  return {
    apiKey: resolvedApiKey,
    baseUrl: resolvedBaseUrl,
    textModel,
    visionModel,
    textApiKey: resolvedTextApiKey,
    textBaseUrl: resolvedTextBaseUrl,
    visionApiKey: resolvedVisionApiKey,
    visionBaseUrl: resolvedVisionBaseUrl,
    provider: normalizeProvider(selectedProvider.key) || inferProviderFromBaseUrl(selectedProvider.baseUrl),
    source: 'external',
  };
}

/**
 * Resolve key + endpoint + models from all available sources.
 */
export function resolveApiConfig(opts?: {
  apiKey?: string;
  provider?: string;
  baseUrl?: string;
  textModel?: string;
  visionModel?: string;
}): ResolvedApiConfig {
  // Explicit CLI flags always win over auto-detection
  if (opts?.apiKey || opts?.provider || opts?.baseUrl) {
    // If provider is specified but no API key, walk a defined precedence:
    //   1. Provider-scoped env vars (e.g. KIMI_API_KEY)
    //   2. External host auth-profiles (e.g. OpenClaw ~/.openclaw/.../auth-profiles.json)
    //      — same store doctor's scanner reads, so per-provider lookups can never
    //      disagree with what doctor told the user about.
    //   3. Generic AI_API_KEY fallback
    let explicitApiKey = opts.apiKey || '';
    const normalizedProvider = normalizeProvider(opts.provider) || '';
    if (!explicitApiKey && opts.provider) {
      const { PROVIDER_ENV_VARS } = require('./providers');
      const envVarNames = PROVIDER_ENV_VARS[normalizedProvider] || [];
      for (const envVar of envVarNames) {
        if (process.env[envVar]) { explicitApiKey = process.env[envVar]!; break; }
      }
      if (!explicitApiKey && normalizedProvider) {
        // External host (OpenClaw, etc.) — read the requested provider's key
        // from auth-profiles, NOT a generic "best provider" guess. This closes
        // the v0.8.8 bug where start fell back to the wrong provider's key.
        const { getExternalProviderKey } = require('./external-creds');
        const extKey = getExternalProviderKey(normalizedProvider);
        if (extKey) explicitApiKey = extKey;
      }
      // Last resort: generic AI_API_KEY
      if (!explicitApiKey && process.env.AI_API_KEY) explicitApiKey = process.env.AI_API_KEY;
    }
    const explicitBaseUrl = normalizeBaseUrl(opts.baseUrl);
    const explicitTextModel = pick(opts.textModel);
    const explicitVisionModel = pick(opts.visionModel);
    return {
      apiKey: explicitApiKey,
      provider: normalizeProvider(opts.provider) || inferProviderFromBaseUrl(explicitBaseUrl),
      baseUrl: explicitBaseUrl,
      textModel: explicitTextModel,
      visionModel: explicitVisionModel,
      textApiKey: explicitApiKey,
      textBaseUrl: explicitBaseUrl,
      visionApiKey: explicitApiKey,
      visionBaseUrl: explicitBaseUrl,
      source: 'local',
    };
  }

  const fromFiles = resolveFromOpenClawFiles();
  if (fromFiles) {
    return fromFiles;
  }

  // Check for externally-injected runtime env vars (e.g. from orchestration platforms).
  const openClawKey = pick(
    process.env.OPENCLAW_AI_API_KEY,
    process.env.OPENCLAW_API_KEY,
    process.env.OPENCLAW_AGENT_API_KEY,
  ) || '';

  const openClawBaseUrl = normalizeBaseUrl(pick(
    process.env.OPENCLAW_BASE_URL,
    process.env.OPENCLAW_AI_BASE_URL,
    process.env.OPENCLAW_AGENT_BASE_URL,
  ));

  const openClawTextModel = pick(
    process.env.OPENCLAW_TEXT_MODEL,
    process.env.OPENCLAW_AI_TEXT_MODEL,
    process.env.OPENCLAW_MODEL,
  );

  const openClawVisionModel = pick(
    process.env.OPENCLAW_VISION_MODEL,
    process.env.OPENCLAW_AI_VISION_MODEL,
    process.env.OPENCLAW_MODEL,
  );

  const openClawProvider = normalizeProvider(pick(
    process.env.OPENCLAW_PROVIDER,
    process.env.OPENCLAW_AI_PROVIDER,
    process.env.OPENCLAW_AGENT_PROVIDER,
  )) || inferProviderFromBaseUrl(openClawBaseUrl);

  if (openClawKey || openClawBaseUrl || openClawTextModel || openClawVisionModel || openClawProvider) {
    return {
      apiKey: openClawKey,
      provider: openClawProvider,
      baseUrl: openClawBaseUrl,
      textModel: openClawTextModel,
      visionModel: openClawVisionModel,
      textApiKey: openClawKey,
      textBaseUrl: openClawBaseUrl,
      visionApiKey: openClawKey,
      visionBaseUrl: openClawBaseUrl,
      source: 'external',
    };
  }

  const explicitApiKey = opts?.apiKey || '';
  const localProvider = normalizeProvider(opts?.provider) || normalizeProvider(readConfiguredProvider());
  const providerScopedEnvKey = pick(
    ...providerEnvCandidates(localProvider).map((key) => {
      const value = process.env[key];
      return typeof value === 'string' ? value : undefined;
    }),
  );
  const localBaseUrl = normalizeBaseUrl(pick(opts?.baseUrl, process.env.AI_BASE_URL, process.env.OPENAI_BASE_URL));
  const localTextModel = pick(opts?.textModel, process.env.AI_TEXT_MODEL, process.env.AI_MODEL);
  const localVisionModel = pick(opts?.visionModel, process.env.AI_VISION_MODEL, process.env.AI_MODEL);
  const localApiKey = pick(
    explicitApiKey,
    process.env.AI_API_KEY,
    providerScopedEnvKey,
    process.env.ANTHROPIC_API_KEY,
    process.env.OPENAI_API_KEY,
    process.env.KIMI_API_KEY,
    process.env.MOONSHOT_API_KEY,
  ) || '';

  if (localApiKey || localBaseUrl || localTextModel || localVisionModel || opts?.provider) {
    return {
      apiKey: localApiKey,
      provider: normalizeProvider(opts?.provider) || inferProviderFromBaseUrl(localBaseUrl),
      baseUrl: localBaseUrl,
      textModel: localTextModel,
      visionModel: localVisionModel,
      textApiKey: localApiKey,
      textBaseUrl: localBaseUrl,
      visionApiKey: localApiKey,
      visionBaseUrl: localBaseUrl,
      source: 'local',
    };
  }

  return {
    apiKey: localApiKey,
    provider: inferProviderFromBaseUrl(localBaseUrl),
    baseUrl: localBaseUrl,
    textModel: localTextModel,
    visionModel: localVisionModel,
    textApiKey: localApiKey,
    textBaseUrl: localBaseUrl,
    visionApiKey: localApiKey,
    visionBaseUrl: localBaseUrl,
    source: 'local',
  };
}
