import { getLlamaProviders, type LlamaProvider } from './llama-providers.js';
import { GATEWAY_KIND } from '@boocode/contracts/gateway';
import { loadConfig } from '../config.js';

export interface RunningModel {
  model: string;
  compositeId: string;
  state: string;
  cmd: string;
  proxy: string;
  ttl: number;
  name: string;
  description: string;
}

export interface CloudModelMeta {
  id: string;
  name?: string;
  contextLength?: number;
  maxOutputTokens?: number;
  pricing?: { input: number; output: number; cached?: number };
  modalities?: string[];
  supportsToolCalling?: boolean;
  supportsReasoning?: boolean;
}

export interface RateLimitInfo {
  requestsRemaining?: number;
  requestsLimit?: number;
  tokensRemaining?: number;
  tokensLimit?: number;
  resetAt?: number;
}

export interface GpuInfo {
  index: number;
  name: string;
  temperature: number;
  utilizationPct: number;
  memoryUsedMb: number;
  memoryTotalMb: number;
  powerDrawW: number;
}

export interface ProviderStatus {
  id: string;
  label: string;
  baseUrl: string;
  kind: string;
  healthy: boolean;
  healthyError?: string;
  running: RunningModel[];
  cloudModels?: CloudModelMeta[];
  rateLimits?: RateLimitInfo;
  gpus?: GpuInfo[];
  hostLoad?: { cpuSat: number; memSat: number; load1: number };
  fetchError?: string;
}

export interface ProviderStatusResponse {
  providers: ProviderStatus[];
  timestamp: number;
}

const HEALTH_CACHE_TTL_MS = 30_000;
const RUNNING_CACHE_TTL_MS = 5_000;
const CLOUD_MODELS_CACHE_TTL_MS = 120_000;
const METRICS_CACHE_TTL_MS = 10_000;
const FETCH_TIMEOUT_MS = 3_000;
const CLOUD_FETCH_TIMEOUT_MS = 8_000;

const healthCache = new Map<string, { value: boolean; error?: string; at: number }>();
const runningCache = new Map<string, { value: RunningModel[]; error?: string; at: number }>();
const cloudModelsCache = new Map<string, { value: CloudModelMeta[]; at: number }>();
const metricsCache = new Map<string, { gpus: GpuInfo[]; hostLoad?: ProviderStatus['hostLoad']; at: number }>();

async function fetchJson(url: string, timeoutMs = FETCH_TIMEOUT_MS, headers?: Record<string, string>): Promise<unknown> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, { signal: controller.signal, headers });
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

async function fetchHealth(provider: LlamaProvider): Promise<{ healthy: boolean; error?: string }> {
  const cacheKey = `health:${provider.id}/${provider.baseUrl}`;
  const cached = healthCache.get(cacheKey);
  if (cached && Date.now() - cached.at < HEALTH_CACHE_TTL_MS) {
    return { healthy: cached.value, error: cached.error };
  }

  try {
    const url = `${provider.baseUrl.replace(/\/+$/, '')}/health`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
    try {
      const res = await fetch(url, { signal: controller.signal });
      const healthy = res.ok;
      const entry = { value: healthy, error: res.ok ? undefined : `HTTP ${res.status}`, at: Date.now() };
      healthCache.set(cacheKey, entry);
      return { healthy, error: entry.error };
    } finally {
      clearTimeout(timer);
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    healthCache.set(cacheKey, { value: false, error: msg, at: Date.now() });
    return { healthy: false, error: msg };
  }
}

async function fetchRunning(provider: LlamaProvider): Promise<{ running: RunningModel[]; error?: string }> {
  const cacheKey = `running:${provider.id}/${provider.baseUrl}`;
  const cached = runningCache.get(cacheKey);
  if (cached && Date.now() - cached.at < RUNNING_CACHE_TTL_MS) {
    return { running: cached.value, error: cached.error };
  }

  try {
    const data = (await fetchJson(`${provider.baseUrl.replace(/\/+$/, '')}/running`)) as {
      running: Omit<RunningModel, 'compositeId'>[];
    };
    const raw = data?.running ?? [];
    const running: RunningModel[] = raw.map((m) => ({
      ...m,
      compositeId: `${provider.id}/${m.model}`,
    }));
    runningCache.set(cacheKey, { value: running, at: Date.now() });
    return { running };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    const cachedFallback = runningCache.get(cacheKey);
    const fallback = cachedFallback?.value ?? [];
    runningCache.set(cacheKey, { value: fallback, error: msg, at: Date.now() });
    return { running: fallback, error: msg };
  }
}

async function fetchCloudModels(
  provider: LlamaProvider,
  apiKey?: string,
): Promise<CloudModelMeta[]> {
  const cacheKey = `cloud:${provider.id}/${provider.baseUrl}`;
  const cached = cloudModelsCache.get(cacheKey);
  if (cached && Date.now() - cached.at < CLOUD_MODELS_CACHE_TTL_MS) return cached.value;

  try {
    let models: CloudModelMeta[];
    const cleanBase = provider.baseUrl.replace(/\/+$/, '');

    if (provider.kind === 'openrouter') {
      const headers: Record<string, string> = {};
      if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
      const data = (await fetchJson(`${cleanBase}/api/v1/models`, CLOUD_FETCH_TIMEOUT_MS, headers)) as {
        data?: Array<{
          id: string; name?: string;
          context_length?: number; top_provider?: { max_completion_tokens?: number };
          pricing?: { prompt?: string; completion?: string };
          architecture?: { modality?: string; tokenizer?: string };
          description?: string;
        }>;
      };
      models = (data.data ?? []).map((m) => ({
        id: `${provider.id}/${m.id}`,
        name: m.name || m.id,
        contextLength: m.context_length,
        maxOutputTokens: m.top_provider?.max_completion_tokens,
        pricing: {
          input: parseFloatPricing(m.pricing?.prompt),
          output: parseFloatPricing(m.pricing?.completion),
        },
        modalities: m.architecture?.modality ? [m.architecture.modality] : ['text'],
        supportsToolCalling: (m.description ?? '').toLowerCase().includes('tool'),
        supportsReasoning:
          (m.description ?? '').toLowerCase().includes('reason') ||
          (m.description ?? '').toLowerCase().includes('think'),
      }));
    } else if (provider.kind === 'deepseek') {
      models = [
        {
          id: `${provider.id}/deepseek-v4-flash`,
          name: 'DeepSeek V4 Flash',
          contextLength: 1_000_000,
          maxOutputTokens: 384_000,
          pricing: { input: 0.14, output: 0.28, cached: 0.0028 },
          modalities: ['text'],
          supportsToolCalling: true,
          supportsReasoning: true,
        },
        {
          id: `${provider.id}/deepseek-v4-pro`,
          name: 'DeepSeek V4 Pro',
          contextLength: 1_000_000,
          maxOutputTokens: 384_000,
          pricing: { input: 0.435, output: 0.87, cached: 0.003625 },
          modalities: ['text'],
          supportsToolCalling: true,
          supportsReasoning: true,
        },
      ];
    } else {
      models = [];
    }

    cloudModelsCache.set(cacheKey, { value: models, at: Date.now() });
    return models;
  } catch {
    return [];
  }
}

function parseFloatPricing(val?: string): number {
  if (!val) return 0;
  const n = parseFloat(val);
  return isNaN(n) ? 0 : n;
}

async function fetchMetrics(provider: LlamaProvider): Promise<{
  gpus: GpuInfo[];
  hostLoad?: ProviderStatus['hostLoad'];
}> {
  const cacheKey = `metrics:${provider.id}/${provider.baseUrl}`;
  const cached = metricsCache.get(cacheKey);
  if (cached && Date.now() - cached.at < METRICS_CACHE_TTL_MS) return cached;

  try {
    const text = await fetch(`${provider.baseUrl.replace(/\/+$/, '')}/metrics`)
      .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))));
    const gpus = parsePrometheusGpuMetrics(text);
    const hostLoad = parsePrometheusHostMetrics(text);
    const entry = { gpus, hostLoad, at: Date.now() };
    metricsCache.set(cacheKey, entry);
    return entry;
  } catch {
    return { gpus: [] };
  }
}

function parsePrometheusGpuMetrics(text: string): GpuInfo[] {
  const gpuMap = new Map<number, Partial<GpuInfo>>();
  // GPU name extraction: parse label from type line that follows the value line.
  const nameRe = /nvidia_gpu_name\{gpu="(\d+)"[^}]*name="([^"]+)"/g;
  let nm: RegExpExecArray | null;
  while ((nm = nameRe.exec(text)) !== null) {
    if (!nm[1] || !nm[2]) continue;
    const idx = parseInt(nm[1], 10);
    if (!gpuMap.has(idx)) gpuMap.set(idx, { index: idx } as GpuInfo);
    (gpuMap.get(idx) as Record<string, unknown>)['name'] = nm[2];
  }

  const patterns: Array<{ regex: RegExp; field: keyof GpuInfo; scale?: number }> = [
    { regex: /nvidia_gpu_temperature_celsius\{gpu="(\d+)"\}\s+([\d.]+)/, field: 'temperature' },
    { regex: /nvidia_gpu_utilization_ratio\{gpu="(\d+)"\}\s+([\d.]+)/, field: 'utilizationPct', scale: 100 },
    { regex: /nvidia_gpu_memory_used_bytes\{gpu="(\d+)"\}\s+([\d.]+)/, field: 'memoryUsedMb', scale: 1 / (1024 * 1024) },
    { regex: /nvidia_gpu_memory_total_bytes\{gpu="(\d+)"\}\s+([\d.]+)/, field: 'memoryTotalMb', scale: 1 / (1024 * 1024) },
    { regex: /nvidia_gpu_power_draw_watts\{gpu="(\d+)"\}\s+([\d.]+)/, field: 'powerDrawW' },
  ];

  for (const { regex, field, scale } of patterns) {
    let match: RegExpExecArray | null;
    const re = new RegExp(regex.source, regex.flags);
    while ((match = re.exec(text)) !== null) {
      if (!match[1] || match[2] === undefined) continue;
      const idx = parseInt(match[1], 10);
      const raw = parseFloat(match[2]);
      const val = scale != null ? raw * scale : raw;
      if (!gpuMap.has(idx)) gpuMap.set(idx, { index: idx } as GpuInfo);
      (gpuMap.get(idx) as Record<string, unknown>)[field] = val;
    }
  }

  return Array.from(gpuMap.values()).filter((g) => g.name != null) as GpuInfo[];
}

function parsePrometheusHostMetrics(text: string): ProviderStatus['hostLoad'] {
  const cpuMatch = /node_cpu_seconds_total/.test(text) ? /cpu_usage_active\{[^}]*\}\s+([\d.]+)/.exec(text) : null;
  const memMatch = /node_memory_MemAvailable_bytes\s+([\d.]+)/.exec(text);
  const memTotalMatch = /node_memory_MemTotal_bytes\s+([\d.]+)/.exec(text);
  const load1Match = /node_load1\s+([\d.]+)/.exec(text);

  if (!memMatch?.[1] || !memTotalMatch?.[1]) return undefined;

  const memAvail = parseFloat(memMatch[1]);
  const memTotal = parseFloat(memTotalMatch[1]);
  return {
    cpuSat: cpuMatch?.[1] ? parseFloat(cpuMatch[1]) / 100 : 0,
    memSat: 1 - memAvail / memTotal,
    load1: load1Match?.[1] ? parseFloat(load1Match[1]) : 0,
  };
}

export async function getProviderStatus(): Promise<ProviderStatusResponse> {
  const config = loadConfig();
  const registry = getLlamaProviders();
  const statuses = await Promise.all(
    registry.providers.map(async (provider): Promise<ProviderStatus> => {
      if (provider.kind === GATEWAY_KIND) {
        return { id: provider.id, label: provider.label, baseUrl: provider.baseUrl, kind: provider.kind, healthy: true, running: [] };
      }

      const cloudKinds = new Set(['openrouter', 'deepseek']);
      if (cloudKinds.has(provider.kind)) {
        const health = await fetchHealth(provider);
        let apiKey: string | undefined;
        if (provider.kind === 'openrouter') {
          const raw = process.env['OPENROUTER_API_KEY'];
          apiKey = raw ?? undefined;
        } else if (provider.kind === 'deepseek') {
          apiKey = config.DEEPSEEK_API_KEY;
        }

        const cloudModels = await fetchCloudModels(provider, apiKey);
        return {
          id: provider.id,
          label: provider.label,
          baseUrl: provider.baseUrl,
          kind: provider.kind,
          healthy: health.healthy,
          healthyError: health.error,
          running: [],
          cloudModels,
        };
      }

      const [health, running, metrics] = await Promise.all([
        fetchHealth(provider),
        fetchRunning(provider),
        fetchMetrics(provider).catch(() => ({ gpus: [] as GpuInfo[], hostLoad: undefined })),
      ]);

      return {
        id: provider.id,
        label: provider.label,
        baseUrl: provider.baseUrl,
        kind: provider.kind,
        healthy: health.healthy,
        healthyError: health.error,
        running: running.running,
        gpus: metrics.gpus,
        hostLoad: metrics.hostLoad,
        fetchError: running.error,
      };
    }),
  );

  return { providers: statuses, timestamp: Date.now() };
}

export async function unloadProvider(providerId: string): Promise<boolean> {
  const registry = getLlamaProviders();
  const provider = registry.providers.find((p) => p.id === providerId);
  if (!provider || provider.kind === GATEWAY_KIND || !provider.kind.endsWith('swap')) return false;

  try {
    const url = `${provider.baseUrl.replace(/\/+$/, '')}/api/models/unload`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10_000);
    try {
      const res = await fetch(url, { method: 'POST', signal: controller.signal });
      if (res.ok) runningCache.delete(`running:${provider.id}/${provider.baseUrl}`);
      return res.ok;
    } finally {
      clearTimeout(timer);
    }
  } catch {
    return false;
  }
}

export async function unloadModel(providerId: string, modelId: string): Promise<boolean> {
  const registry = getLlamaProviders();
  const provider = registry.providers.find((p) => p.id === providerId);
  if (!provider || provider.kind === GATEWAY_KIND || !provider.kind.endsWith('swap')) return false;

  try {
    const url = `${provider.baseUrl.replace(/\/+$/, '')}/api/models/unload/${encodeURIComponent(modelId)}`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10_000);
    try {
      const res = await fetch(url, { method: 'POST', signal: controller.signal });
      if (res.ok) runningCache.delete(`running:${provider.id}/${provider.baseUrl}`);
      return res.ok;
    } finally {
      clearTimeout(timer);
    }
  } catch {
    return false;
  }
}

export function invalidateProviderCache(): void {
  healthCache.clear();
  runningCache.clear();
  cloudModelsCache.clear();
  metricsCache.clear();
}
