import type { AuthMode, TransportType } from '@apifold/types';

import type { Logger } from '../observability/logger.js';
import type { ServerRegistry, L0ServerMeta } from '../registry/server-registry.js';
import type { ToolDefinition } from '../registry/tool-loader.js';

const VALID_AUTH_MODES = new Set<string>(['none', 'api_key', 'bearer', 'oauth2_authcode', 'oauth2_client_creds']);
const VALID_TRANSPORTS = new Set<string>(['sse', 'streamable-http']);

const BLOCKED_HOSTNAMES = new Set([
  'localhost', '0.0.0.0', '::1', '[::1]',
]);

function validateTransport(value: string | null | undefined): TransportType {
  if (!value || !VALID_TRANSPORTS.has(value)) {
    return 'sse'; // Default to SSE for backward compatibility
  }
  return value as TransportType;
}

function validateAuthMode(value: string): AuthMode {
  if (!VALID_AUTH_MODES.has(value)) {
    throw new Error(`Invalid auth_mode: ${value}`);
  }
  return value as AuthMode;
}

function validateBaseUrl(raw: string): string {
  let parsed: URL;
  try {
    parsed = new URL(raw);
  } catch {
    throw new Error('Invalid base_url');
  }
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
    throw new Error('base_url must use http or https');
  }
  // SSRF protection: block private/internal hosts in production.
  // In development/test, allow local upstreams for testing.
  const allowPrivate = process.env['NODE_ENV'] !== 'production';
  if (!allowPrivate) {
    const hostname = parsed.hostname.toLowerCase();
    if (
      BLOCKED_HOSTNAMES.has(hostname) ||
      hostname.startsWith('127.') ||
      hostname.startsWith('10.') ||
      hostname.startsWith('192.168.') ||
      hostname.startsWith('169.254.') ||
      /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
      hostname.startsWith('fc00:') ||
      hostname.startsWith('fe80:') ||
      /^::ffff:/.test(hostname)
    ) {
      throw new Error('base_url targets a private or internal host');
    }
  }
  return raw;
}

export interface DbClient {
  query<T>(sql: string, params?: readonly unknown[]): Promise<{ readonly rows: readonly T[] }>;
}

export interface PostgresLoaderDeps {
  readonly db: DbClient;
  readonly logger: Logger;
  readonly registry: ServerRegistry;
}

/** Load all active servers into L0 registry on startup. */
export async function loadAllServers(deps: PostgresLoaderDeps): Promise<void> {
  const { db, logger, registry } = deps;

  const { rows } = await db.query<ServerRow>(
    `SELECT id, slug, endpoint_id, user_id, transport, auth_mode, base_url, rate_limit, is_active,
            CASE WHEN domain_verified_at IS NOT NULL THEN custom_domain ELSE NULL END AS custom_domain,
            token_hash,
            is_public
     FROM mcp_servers
     WHERE is_active = true`,
  );

  const servers: L0ServerMeta[] = rows.map((row) =>
    Object.freeze({
      id: row.id,
      slug: row.slug,
      endpointId: row.endpoint_id,
      userId: row.user_id,
      transport: validateTransport(row.transport),
      authMode: validateAuthMode(row.auth_mode),
      baseUrl: validateBaseUrl(row.base_url),
      rateLimit: row.rate_limit,
      isActive: row.is_active,
      customDomain: row.custom_domain ?? null,
      tokenHash: row.token_hash ?? null,
      isPublic: row.is_public,
    }),
  );

  registry.loadAll(servers);
  logger.info({ count: servers.length }, 'Postgres bulk load complete');
}

/** Reload a single server by ID (used during fallback polling and Redis events). */
export async function reloadServer(
  deps: PostgresLoaderDeps,
  serverId: string,
): Promise<L0ServerMeta | null> {
  const { db, registry } = deps;

  const { rows } = await db.query<ServerRow>(
    `SELECT id, slug, endpoint_id, user_id, transport, auth_mode, base_url, rate_limit, is_active,
            CASE WHEN domain_verified_at IS NOT NULL THEN custom_domain ELSE NULL END AS custom_domain,
            token_hash,
            is_public
     FROM mcp_servers
     WHERE id = $1`,
    [serverId],
  );

  const row = rows[0];
  if (!row || !row.is_active) {
    registry.remove(serverId);
    return null;
  }

  const server: L0ServerMeta = Object.freeze({
    id: row.id,
    slug: row.slug,
    endpointId: row.endpoint_id,
    userId: row.user_id,
    transport: validateTransport(row.transport),
    authMode: validateAuthMode(row.auth_mode),
    baseUrl: validateBaseUrl(row.base_url),
    rateLimit: row.rate_limit,
    isActive: row.is_active,
    customDomain: row.custom_domain ?? null,
    tokenHash: row.token_hash ?? null,
    isPublic: row.is_public,
  });

  registry.upsert(server);
  return server;
}

/** Fetch tool definitions for a server (used by ToolLoader). */
export async function fetchToolsForServer(
  db: DbClient,
  serverId: string,
): Promise<readonly ToolDefinition[]> {
  const { rows } = await db.query<ToolRow>(
    `SELECT id, name, description, input_schema, http_method, http_path, param_map
     FROM mcp_tools
     WHERE server_id = $1 AND is_active = true`,
    [serverId],
  );

  return rows.map((row) =>
    Object.freeze({
      id: row.id,
      name: row.name,
      description: row.description,
      inputSchema: row.input_schema as Record<string, unknown>,
      httpMethod: row.http_method ?? undefined,
      httpPath: row.http_path ?? undefined,
      paramMap: row.param_map as Record<string, 'path' | 'query' | 'header' | 'body'> ?? undefined,
    }),
  );
}

export type EncryptFn = (plaintext: string) => string;

/** Fetch decrypted credential headers for a server, auto-refreshing OAuth tokens when expired. */
export async function fetchCredentialHeaders(
  db: DbClient,
  serverId: string,
  decryptFn: (encrypted: string) => string,
  deps?: { readonly logger?: Logger; readonly encryptFn?: EncryptFn; readonly redis?: unknown },
): Promise<Readonly<Record<string, string>>> {
  const { rows } = await db.query<OAuthCredentialRow>(
    `SELECT id, encrypted_key, auth_type, expires_at,
            token_endpoint, client_id, encrypted_client_secret,
            encrypted_refresh_token, token_expires_at
     FROM credentials
     WHERE server_id = $1
     ORDER BY created_at DESC
     LIMIT 1`,
    [serverId],
  );

  const row = rows[0];
  if (!row) {
    return Object.freeze({});
  }

  if (row.expires_at && new Date(row.expires_at) < new Date()) {
    return Object.freeze({});
  }

  // OAuth token types — check token_expires_at and auto-refresh if needed
  if (row.auth_type === 'oauth2_authcode' || row.auth_type === 'oauth2_client_creds') {
    return fetchOAuthHeaders(db, row, decryptFn, deps);
  }

  let plaintext: string;
  try {
    plaintext = decryptFn(row.encrypted_key);
  } catch {
    throw new Error('Credential decryption failed');
  }

  // Sanitize decrypted credential — prevent CRLF injection into HTTP headers
  const sanitized = plaintext.replace(/[\r\n]/g, '');

  if (row.auth_type === 'bearer') {
    return Object.freeze({ Authorization: `Bearer ${sanitized}` });
  }

  if (row.auth_type === 'api_key') {
    return Object.freeze({ 'X-API-Key': sanitized });
  }

  throw new Error(`Unsupported auth type: ${row.auth_type}`);
}

async function fetchOAuthHeaders(
  db: DbClient,
  row: OAuthCredentialRow,
  decryptFn: (encrypted: string) => string,
  deps?: { readonly logger?: Logger; readonly encryptFn?: EncryptFn; readonly redis?: unknown },
): Promise<Readonly<Record<string, string>>> {
  const { isTokenExpired, refreshAndUpdateToken, CredentialExpiredError } = await import('../oauth/token-refresher.js');

  // Check if token needs refresh
  if (isTokenExpired(row.token_expires_at)) {
    const logger = deps?.logger;
    const encryptFn = deps?.encryptFn;
    const redis = deps?.redis as import('ioredis').Redis | null | undefined;
    if (!logger || !encryptFn) {
      throw new Error('OAuth token expired and refresh deps not available');
    }

    try {
      const newToken = await refreshAndUpdateToken(
        { db, logger, decryptFn, encryptFn, redis: redis ?? null },
        row,
      );
      const sanitized = newToken.replace(/[\r\n]/g, '');
      return Object.freeze({ Authorization: `Bearer ${sanitized}` });
    } catch (err) {
      if (err instanceof CredentialExpiredError) {
        throw err; // Let the caller handle expired credentials
      }
      throw new Error('OAuth token refresh failed');
    }
  }

  // Token still valid — decrypt and return
  let plaintext: string;
  try {
    plaintext = decryptFn(row.encrypted_key);
  } catch {
    throw new Error('Credential decryption failed');
  }

  const sanitized = plaintext.replace(/[\r\n]/g, '');
  return Object.freeze({ Authorization: `Bearer ${sanitized}` });
}

interface ServerRow {
  readonly id: string;
  readonly slug: string;
  readonly endpoint_id: string;
  readonly user_id: string;
  readonly transport: string | null;
  readonly auth_mode: string;
  readonly base_url: string;
  readonly rate_limit: number;
  readonly is_active: boolean;
  readonly custom_domain: string | null;
  readonly token_hash: string | null;
  readonly is_public: boolean;
}

interface ToolRow {
  readonly id: string;
  readonly name: string;
  readonly description: string | null;
  readonly input_schema: unknown;
  readonly http_method: string | null;
  readonly http_path: string | null;
  readonly param_map: unknown;
}

interface OAuthCredentialRow {
  readonly id: string;
  readonly encrypted_key: string;
  readonly auth_type: string;
  readonly expires_at: string | null;
  readonly token_endpoint: string | null;
  readonly client_id: string | null;
  readonly encrypted_client_secret: string | null;
  readonly encrypted_refresh_token: string | null;
  readonly token_expires_at: string | null;
}
