'use strict';

const {
  codingCapabilityReason,
  normalizeCapabilities,
} = require('./coding-availability');
const { connectionKind, connectionLabel } = require('./portkey');

const SESSION_MODEL_PROVIDER_TYPES = new Set([
  'anthropic',
  'openai',
  'google',
  'deepseek',
  'moonshot',
  'ollama',
  'mlx',
]);

const PROVIDER_LABELS = Object.freeze({
  anthropic: 'Anthropic',
  openai: 'OpenAI',
  google: 'Google Gemini',
  deepseek: 'DeepSeek',
  moonshot: 'Moonshot / Kimi',
  ollama: 'Ollama',
  mlx: 'MLX',
});

function buildCodingModelCatalog({
  providerEntries = [],
  liveAvailability = null,
  includeUnavailable = true,
  includeVersioned = false,
  now = () => new Date(),
} = {}) {
  const generatedAt = now().toISOString();
  const liveModels = Array.isArray(liveAvailability?.models) ? liveAvailability.models : null;
  const liveProviderResults = new Map((liveAvailability?.providers || []).map(provider => [provider.id, provider]));

  const models = [];
  const providers = [];

  for (const entry of providerEntries || []) {
    const provider = normalizeProviderEntry(entry);
    if (!provider.id) continue;
    const liveProvider = liveProviderResults.get(provider.id);
    const providerStatus = liveProvider?.status || (liveModels ? 'not_listed' : 'manifest_fallback');
    const providerError = liveProvider?.error || null;

    const exactLiveModels = liveModels
      ? liveModels.filter(model => model.provider_id === provider.id || model.providerId === provider.id)
      : null;
    const sourceModels = exactLiveModels && exactLiveModels.length > 0
      ? exactLiveModels.map(model => normalizeCatalogModel(model, provider, {
        source: 'provider-api',
        available: model.available !== false,
        unavailableReason: model.unavailable_reason || model.unavailableReason || '',
        generatedAt,
      }))
      : manifestModelsForProvider(provider, { generatedAt });

    let codingCount = 0;
    for (const model of sourceModels) {
      if (!includeVersioned && isVersionedModel(model.modelId) && model.metadata.versioned !== false) {
        continue;
      }
      const capability = codingCatalogReason(model, provider);
      const available = model.available && capability.ok && providerStatus !== 'error' && providerStatus !== 'unavailable';
      const normalized = {
        ...model,
        available,
        purpose: mergePurposes(model.purpose, capability.ok ? ['coding'] : []),
        unavailableReason: available
          ? ''
          : (model.unavailableReason || providerError || (providerStatus !== 'available' ? providerStatus : '') || capability.reason),
        codingReason: capability.reason,
      };
      if (normalized.available) codingCount += 1;
      if (normalized.available || includeUnavailable) models.push(normalized);
    }

    providers.push({
      providerId: provider.id,
      providerType: provider.type,
      displayName: provider.displayName,
      source: exactLiveModels && exactLiveModels.length > 0 ? 'provider-api' : 'manifest',
      status: codingCount > 0 ? 'available' : providerStatus,
      codingCount,
      error: providerError,
    });
  }

  models.sort((a, b) => {
    const providerCmp = a.displayNameProvider.localeCompare(b.displayNameProvider);
    if (providerCmp) return providerCmp;
    return a.displayName.localeCompare(b.displayName, undefined, { numeric: true });
  });

  return {
    generatedAt,
    purpose: 'coding',
    models,
    providers,
  };
}

function buildSessionModelCatalog({
  providers = [],
  models = [],
  liveAvailability = null,
  now = () => new Date(),
} = {}) {
  const generatedAt = now().toISOString();
  const providerById = new Map();
  const liveProviders = new Map((liveAvailability?.providers || []).map(row => [String(row.id || ''), row]));
  const liveModels = new Map((liveAvailability?.models || []).map(row => [
    `${String(row.provider_id || row.providerId || '')}|${String(row.model_id || row.modelId || row.id || '')}`,
    row,
  ]));

  for (const provider of providers || []) {
    if (!provider || provider.enabled === 0 || provider.enabled === false) continue;
    providerById.set(String(provider.id || ''), normalizeSessionProvider(provider));
  }

  const byCanonical = new Map();
  for (const row of models || []) {
    if (!row || row.enabled === 0 || row.enabled === false) continue;
    const provider = providerById.get(String(row.provider_id || row.providerId || ''));
    if (!provider) continue;
    const providerType = canonicalProviderType(row.model_id || row.modelId || row.id, provider.type);
    if (!SESSION_MODEL_PROVIDER_TYPES.has(providerType)) continue;
    const modelId = canonicalModelId(row.model_id || row.modelId || row.id, providerType);
    if (!modelId || isVersionedModel(modelId)) continue;
    const capabilities = inferSessionCapabilities(row.capabilities, providerType, modelId);
    const reason = codingCapabilityReason({ id: modelId, name: row.display_name || row.displayName || modelId, capabilities }, providerType);
    if (!reason.ok) continue;

    const live = liveModels.get(`${provider.id}|${row.model_id || row.modelId || row.id}`) || null;
    const liveProvider = liveProviders.get(provider.id) || null;
    const route = {
      registry_id: row.id || row.registry_id || row.registryId || '',
      provider_id: provider.id,
      provider_name: provider.name,
      provider_type: provider.type,
      model_id: row.model_id || row.modelId || row.id,
      connection_kind: provider.connection_kind,
      connection_label: connectionLabel(provider.connection_kind),
      route_policy: provider.route_policy,
      route_rank: routeRank(provider),
      source: row.source || 'registry',
      verification_status: row.verification_status || row.verificationStatus || '',
      has_key: !!provider.has_key,
      is_catalog: !!provider.is_catalog,
      live_status: live ? 'available' : (liveProvider?.status || 'not_checked'),
      live_error: liveProvider?.error || null,
    };

    const key = `${providerType}|${modelId}`;
    const existing = byCanonical.get(key);
    const routeList = existing ? existing.routes.concat(route) : [route];
    routeList.sort((a, b) => a.route_rank - b.route_rank
      || catalogRouteRank(a) - catalogRouteRank(b)
      || String(a.provider_name).localeCompare(String(b.provider_name))
      || String(a.registry_id).localeCompare(String(b.registry_id)));
    const preferred = routeList[0];
    byCanonical.set(key, {
      id: key,
      catalog_id: key,
      model_id: modelId,
      display_name: displayNameForSessionModel(row, modelId),
      provider_type: providerType,
      provider_name: PROVIDER_LABELS[providerType] || providerType,
      capabilities,
      enabled: 1,
      selectable: true,
      available: true,
      coding_capable: true,
      coding_reason: reason.reason,
      source: live ? 'live+registry' : 'registry',
      availability_state: live ? 'available' : routeAvailabilityState(preferred),
      route_mode: 'policy',
      route_policy: preferred.route_policy,
      route_provider_id: preferred.provider_id,
      route_provider_name: preferred.provider_name,
      route_connection_kind: preferred.connection_kind,
      route_label: `routes via ${preferred.connection_label}`,
      preferred_registry_id: preferred.registry_id,
      live_available: !!live,
      last_seen_at: live?.last_seen_at || row.last_seen_at || row.lastSeenAt || generatedAt,
      routes: routeList,
    });
  }

  const out = Array.from(byCanonical.values()).sort((a, b) => {
    const providerCmp = providerSort(a.provider_type) - providerSort(b.provider_type);
    if (providerCmp) return providerCmp;
    return a.display_name.localeCompare(b.display_name, undefined, { numeric: true });
  });

  return {
    source: 'catalog',
    generated_at: generatedAt,
    models: out,
    providers: Array.from(providerById.values()).map(provider => ({
      id: provider.id,
      name: provider.name,
      type: provider.type,
      connection_kind: provider.connection_kind,
      route_policy: provider.route_policy,
      status: liveProviders.get(provider.id)?.status || 'not_checked',
      error: liveProviders.get(provider.id)?.error || null,
    })),
    counts: {
      models: out.length,
      providers: new Set(out.map(model => model.provider_type)).size,
      configured_routes: out.reduce((sum, model) => sum + model.routes.length, 0),
      live: out.filter(model => model.live_available).length,
    },
  };
}

function normalizeProviderEntry(entry = {}) {
  const manifest = entry.manifest || entry;
  return {
    id: entry.id || manifest.id || '',
    type: manifest.type || manifest.providerType || manifest.id || entry.id || '',
    displayName: manifest.label || manifest.name || entry.name || manifest.id || entry.id || '',
    manifest,
  };
}

function normalizeSessionProvider(provider = {}) {
  const kind = provider.connection_kind || connectionKind({
    authMethod: provider.auth_method,
    baseUrl: provider.base_url || provider.baseUrl,
    customHeaders: provider.custom_headers || provider.customHeaders,
  });
  return {
    id: String(provider.id || ''),
    name: provider.name || provider.type || 'Provider',
    type: String(provider.type || '').toLowerCase(),
    connection_kind: kind,
    route_policy: provider.route_policy || 'auto',
    has_key: provider.has_key === true || provider.hasKey === true || !!provider.api_key_encrypted,
    is_catalog: provider.is_catalog === true || provider.catalog_only === true,
    is_default_instance: provider.is_default_instance === true || /-(default|auto)$/.test(String(provider.id || '')),
  };
}

function canonicalProviderType(modelId = '', providerType = '') {
  const id = String(modelId || '').toLowerCase();
  const type = String(providerType || '').toLowerCase();
  const namespaced = /^@([^/]+)\//.exec(id);
  if (namespaced && SESSION_MODEL_PROVIDER_TYPES.has(namespaced[1])) return namespaced[1];
  if (id.startsWith('claude-')) return 'anthropic';
  if (/^(gpt-|o[0-9]|codex|chatgpt-)/.test(id)) return 'openai';
  if (id.startsWith('gemini-')) return 'google';
  if (id.startsWith('deepseek-')) return 'deepseek';
  if (id.startsWith('kimi-') || id.startsWith('moonshot-')) return 'moonshot';
  if (SESSION_MODEL_PROVIDER_TYPES.has(type)) return type;
  return type;
}

function canonicalModelId(modelId = '', providerType = '') {
  let id = String(modelId || '').trim();
  const namespaced = /^@([^/]+)\/(.+)$/.exec(id);
  if (namespaced && SESSION_MODEL_PROVIDER_TYPES.has(namespaced[1].toLowerCase())) {
    id = namespaced[2];
  }
  if (providerType === 'bedrock') {
    id = id.replace(/^anthropic\./, '');
  }
  id = stripSessionModelAlias(id);
  return id;
}

function stripSessionModelAlias(modelId = '') {
  return String(modelId || '').trim()
    .replace(/@(default|latest)$/i, '')
    .replace(/@20\d{6}$/i, '');
}

function inferSessionCapabilities(raw, providerType = '', modelId = '') {
  const caps = new Set(normalizeCapabilities(raw, providerType));
  const family = canonicalProviderType(modelId, providerType);
  if (SESSION_MODEL_PROVIDER_TYPES.has(family)) {
    caps.add('chat');
    caps.add('tools');
  }
  const parsed = parseCapabilitiesObject(raw);
  if (parsed?.code_execution?.supported) caps.add('code');
  if (parsed?.image_input?.supported || parsed?.vision?.supported) caps.add('vision');
  if (/coder|code|gpt-|claude-|deepseek-|kimi-|gemini-/i.test(modelId)) caps.add('code');
  return Array.from(caps).sort();
}

function parseCapabilitiesObject(value) {
  if (!value) return null;
  if (typeof value === 'object' && !Array.isArray(value)) return value;
  if (typeof value !== 'string') return null;
  try {
    const parsed = JSON.parse(value);
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function displayNameForSessionModel(row = {}, modelId = '') {
  const name = String(row.display_name || row.displayName || row.name || modelId || '').trim();
  return stripSessionModelAlias(name.replace(/^@[^/]+\//, '')) || modelId;
}

function routeRank(provider = {}) {
  const policy = provider.route_policy || 'auto';
  const isPortkey = provider.connection_kind === 'portkey';
  let policyRank = 50;
  if (policy === 'portkey') policyRank = isPortkey ? 0 : 10;
  else if (policy === 'direct') policyRank = isPortkey ? 10 : 0;
  else policyRank = isPortkey ? 10 : 0;
  const credentialRank = provider.has_key ? 0 : 20;
  const defaultRank = provider.is_default_instance ? 0 : 2;
  return policyRank + credentialRank + defaultRank;
}

function catalogRouteRank(route = {}) {
  return route.is_catalog || route.source === 'portkey_catalog' || route.verification_status === 'catalog' ? 5 : 0;
}

function routeAvailabilityState(route = {}) {
  if (route.live_status === 'available') return 'available';
  if (route.live_status === 'error') return 'route_issue';
  if (route.live_status === 'unavailable') return 'route_issue';
  return 'configured';
}

function providerSort(providerType = '') {
  const order = { anthropic: 1, openai: 2, google: 3, deepseek: 4, moonshot: 5, ollama: 6, mlx: 7 };
  return order[String(providerType || '').toLowerCase()] || 20;
}

function manifestModelsForProvider(provider, { generatedAt } = {}) {
  const manifest = provider.manifest || {};
  const declared = Array.isArray(manifest.models) ? manifest.models : [];
  const fallback = declared.length > 0
    ? declared
    : (manifest.defaultModel ? [{ id: manifest.defaultModel, name: manifest.defaultModel, capabilities: manifest.capabilities }] : []);
  return fallback.map(model => normalizeCatalogModel(model, provider, {
    source: 'manifest',
    available: true,
    generatedAt,
  }));
}

function normalizeCatalogModel(model = {}, provider = {}, {
  source = 'manifest',
  available = true,
  unavailableReason = '',
  generatedAt = new Date().toISOString(),
} = {}) {
  const modelId = model.modelId || model.model_id || model.id || model.name || '';
  const capabilities = normalizeCapabilities(model.capabilities || model.capability || provider.manifest?.capabilities, provider.type);
  return {
    providerId: model.providerId || model.provider_id || provider.id,
    providerType: model.providerType || model.provider_type || provider.type,
    modelId,
    displayName: model.displayName || model.display_name || model.name || modelId,
    displayNameProvider: provider.displayName || provider.id,
    purpose: mergePurposes(model.purpose || model.taskTypes || model.task_types, []),
    supportsTools: model.supportsTools ?? model.supports_tools ?? capabilities.includes('tools'),
    supportsStreaming: model.supportsStreaming ?? model.supports_streaming ?? capabilities.includes('streaming'),
    supportsJson: model.supportsJson ?? model.supports_json ?? capabilities.includes('json'),
    contextWindow: model.contextWindow || model.context_window || model.max_context_tokens || model.maxContextTokens || null,
    pricing: model.pricing || null,
    capabilities,
    source,
    available: Boolean(available),
    unavailableReason,
    lastCheckedAt: model.lastCheckedAt || model.last_seen_at || generatedAt,
    metadata: {
      versioned: model.versioned,
      size: model.size || null,
    },
  };
}

function codingCatalogReason(model = {}, provider = {}) {
  const reason = codingCapabilityReason({
    id: model.modelId,
    name: model.displayName,
    capabilities: {
      chat: true,
      tools: model.supportsTools,
      streaming: model.supportsStreaming,
      ...(model.capabilities || []).reduce((acc, cap) => {
        acc[cap] = true;
        return acc;
      }, {}),
    },
  }, provider.type);
  if (!reason.ok) return reason;
  if (!model.supportsTools) return { ok: false, reason: 'missing tool-call capability' };
  return { ok: true, reason: 'chat+tools' };
}

function mergePurposes(...values) {
  const out = new Set();
  for (const value of values) {
    const list = Array.isArray(value) ? value : (value ? [value] : []);
    for (const item of list) {
      if (item) out.add(String(item).toLowerCase());
    }
  }
  return [...out].sort();
}

function isVersionedModel(modelId = '') {
  return /(?:^|[-_@])20\d{6}(?:$|[-_])/.test(String(modelId));
}

module.exports = {
  buildCodingModelCatalog,
  buildSessionModelCatalog,
  canonicalModelId,
  canonicalProviderType,
  codingCatalogReason,
  isVersionedModel,
  manifestModelsForProvider,
  normalizeCatalogModel,
  stripSessionModelAlias,
};
