/**
 * Per-provider model-list resolver — fetches and normalizes model catalogs
 * for google / openai-compat / mock provider types.
 */

import { captureCaught } from '@/server/observability/capture.server.js';
import { assertSafeFetchUrl, UnsafeFetchUrlError } from '@/server/security/safe-fetch-url';

export interface ModelOption {
  id: string;
  label: string;
}

export class ProviderModelsError extends Error {
  constructor(
    public status: number,
    message: string,
  ) {
    super(message);
    this.name = 'ProviderModelsError';
  }
}

export const MOCK_MODELS: ModelOption[] = [
  { id: 'TEST', label: 'TEST (success)' },
  { id: 'TEST-RATELIMIT', label: 'TEST-RATELIMIT' },
  { id: 'TEST-TRANSIENT', label: 'TEST-TRANSIENT' },
  { id: 'TEST-QUOTA', label: 'TEST-QUOTA' },
  { id: 'TEST-AUTH', label: 'TEST-AUTH' },
  { id: 'TEST-ERROR', label: 'TEST-ERROR' },
];

const GEMINI_MODELS_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/models';

const GOOGLE_MODEL_DENY = /embedding|aqa|vision|imagen|veo|tts|image-generation/i;

function isUsableGoogleModel(id: string): boolean {
  return !GOOGLE_MODEL_DENY.test(id);
}

async function fetchModelsJson(url: string, apiKey: string): Promise<{ data: { id: string }[] }> {
  let res: Response;
  try {
    res = await fetch(url, {
      redirect: 'manual',
      headers: { Authorization: `Bearer ${apiKey}` },
      signal: AbortSignal.timeout(8000),
    });
  } catch (err) {
    if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) {
      throw new ProviderModelsError(504, 'provider timeout');
    }
    const msg = err instanceof Error ? err.message : String(err);
    throw new ProviderModelsError(502, msg);
  }

  if (res.status >= 300 && res.status < 400) {
    throw new ProviderModelsError(502, 'provider returned a redirect');
  }

  if (!res.ok) {
    const body = await res.text();
    throw new ProviderModelsError(res.status, body || 'provider error');
  }

  try {
    const json: unknown = await res.json();
    if (
      typeof json !== 'object' ||
      json === null ||
      !('data' in json) ||
      !Array.isArray((json as { data: unknown }).data)
    ) {
      throw new ProviderModelsError(502, 'invalid provider response');
    }

    const data = (json as { data: unknown[] }).data.filter(
      (m): m is { id: string } => typeof (m as { id?: unknown })?.id === 'string',
    );

    return { data };
  } catch (err) {
    if (err instanceof ProviderModelsError) {
      throw err;
    }
    captureCaught(err, { scope: 'server.ai.providers.listModels', severity: 'warning' });
    throw new ProviderModelsError(502, 'invalid provider response');
  }
}

export async function fetchProviderModels(
  type: string,
  apiKey: string | null,
  baseUrl: string | null,
): Promise<ModelOption[]> {
  if (type === 'mock') {
    return MOCK_MODELS;
  }

  if (type === 'google') {
    if (!apiKey) {
      throw new ProviderModelsError(400, 'API key required');
    }

    const json = await fetchModelsJson(GEMINI_MODELS_URL, apiKey);

    return json.data
      .map((m) => {
        const bare = m.id.replace(/^models\//, '');
        return { id: bare, label: bare };
      })
      .filter((m) => isUsableGoogleModel(m.id))
      .sort((a, b) => a.label.localeCompare(b.label));
  }

  if (type === 'openai-compat') {
    if (!baseUrl) {
      throw new ProviderModelsError(400, 'base URL required');
    }
    if (!apiKey) {
      throw new ProviderModelsError(400, 'API key required');
    }

    try {
      assertSafeFetchUrl(baseUrl);
    } catch (err) {
      if (err instanceof UnsafeFetchUrlError)
        throw new ProviderModelsError(400, 'invalid base URL');
      throw err;
    }

    const url = `${baseUrl.replace(/\/$/, '')}/models`;
    const json = await fetchModelsJson(url, apiKey);

    return json.data
      .map((m) => ({
        id: m.id,
        label: m.id,
      }))
      .sort((a, b) => a.label.localeCompare(b.label));
  }

  throw new ProviderModelsError(400, 'unsupported provider type: ' + type);
}
