import {
  getAllCustomModels,
  getAllSyncedAvailableModels,
  getCombos,
  getModelIsHidden,
  getProviderConnections,
  getProviderNodes,
} from "@/lib/localDb";
import { getAccountDisplayName, getProviderDisplayName } from "@/lib/display/names";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
import { getSyncedCapabilities } from "@/lib/modelsDevSync";
import { getModelsByProviderId } from "@/shared/constants/models";
import {
  AI_PROVIDERS,
  NOAUTH_PROVIDERS,
  isAnthropicCompatibleProvider,
  isClaudeCodeCompatibleProvider,
  isOpenAICompatibleProvider,
} from "@/shared/constants/providers";
import type { RegistryModel } from "@omniroute/open-sse/config/providerRegistry.ts";

type JsonRecord = Record<string, unknown>;

type BuilderModelSource = "imported" | "system" | "custom" | "fallback";
type BuilderConnectionStatus = "active" | "inactive" | "rate-limited" | "error";
type ProviderVisual = { icon: string; color: string; source: "system" | "provider-node" };

type CustomModelLike = {
  id?: string;
  name?: string;
  source?: string;
  apiFormat?: string;
  supportedEndpoints?: string[];
  inputTokenLimit?: number;
  outputTokenLimit?: number;
  supportsThinking?: boolean;
  isHidden?: boolean;
};

type SyncedModelLike = {
  id?: string;
  name?: string;
  source?: string;
  supportedEndpoints?: string[];
  inputTokenLimit?: number;
  outputTokenLimit?: number;
  description?: string;
  supportsThinking?: boolean;
};

type ProviderConnectionLike = {
  id?: string;
  provider?: string;
  authType?: string;
  name?: string;
  displayName?: string;
  email?: string;
  priority?: number;
  isActive?: boolean;
  defaultModel?: string;
  rateLimitedUntil?: number | null;
  lastError?: string | null;
  lastTested?: string | null;
  updatedAt?: string | null;
  testStatus?: string | null;
};

type ProviderNodeLike = {
  id?: string;
  type?: string;
  name?: string;
  prefix?: string;
};

export interface ComboBuilderModelOption {
  id: string;
  qualifiedModel: string;
  name: string;
  source: BuilderModelSource;
  sources: BuilderModelSource[];
  supportedEndpoints?: string[];
  apiFormat?: string;
  contextLength?: number;
  outputTokenLimit?: number;
  supportsThinking?: boolean;
}

export interface ComboBuilderConnectionOption {
  id: string;
  label: string;
  type: string;
  status: BuilderConnectionStatus;
  priority: number;
  isActive: boolean;
  defaultModel?: string | null;
  rateLimitedUntil?: number | null;
  lastError?: string | null;
  lastTested?: string | null;
}

export interface ComboBuilderProviderOption {
  providerId: string;
  providerType: string;
  displayName: string;
  alias: string;
  prefix?: string | null;
  icon: string;
  color: string;
  source: "system" | "provider-node";
  acceptsArbitraryModel: boolean;
  connectionCount: number;
  activeConnectionCount: number;
  modelCount: number;
  connections: ComboBuilderConnectionOption[];
  models: ComboBuilderModelOption[];
}

export interface ComboBuilderComboRefOption {
  id: string;
  name: string;
  strategy: string;
  stepCount: number;
  version: number;
  sortOrder?: number;
}

export interface ComboBuilderOptionsPayload {
  schemaVersion: number;
  generatedAt: string;
  providers: ComboBuilderProviderOption[];
  comboRefs: ComboBuilderComboRefOption[];
}

function toStringOrNull(value: unknown): string | null {
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}

function toNumberOrNull(value: unknown): number | null {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim().length > 0) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

function toStringArray(value: unknown): string[] | undefined {
  if (!Array.isArray(value)) return undefined;
  const normalized = value
    .map((item) => toStringOrNull(item))
    .filter((item): item is string => Boolean(item));
  return normalized.length > 0 ? normalized : undefined;
}

function isChatCapable(supportedEndpoints: string[] | undefined): boolean {
  if (!supportedEndpoints || supportedEndpoints.length === 0) return true;
  return supportedEndpoints.includes("chat");
}

function getSourcePriority(source: BuilderModelSource): number {
  switch (source) {
    case "imported":
      return 0;
    case "system":
      return 1;
    case "custom":
      return 2;
    case "fallback":
      return 3;
    default:
      return 99;
  }
}

function getCompatibleProviderVisual(providerNodeType: string | null): ProviderVisual {
  if (providerNodeType === "openai-compatible") {
    return { icon: "api", color: "#10A37F", source: "provider-node" };
  }
  if (providerNodeType === "anthropic-compatible") {
    return { icon: "api", color: "#D97757", source: "provider-node" };
  }
  if (providerNodeType === "anthropic-compatible-cc") {
    return { icon: "smart_toy", color: "#D97757", source: "provider-node" };
  }
  return { icon: "api", color: "#6B7280", source: "provider-node" };
}

function getProviderVisual(
  providerId: string,
  providerNode: ProviderNodeLike | null
): ProviderVisual & { alias: string; providerType: string } {
  const providerEntry = AI_PROVIDERS[providerId];
  if (providerEntry) {
    return {
      alias: providerEntry.alias || providerEntry.id,
      providerType: providerEntry.id,
      icon: providerEntry.icon || "hub",
      color: providerEntry.color || "#6B7280",
      source: "system",
    };
  }

  const providerNodeType = toStringOrNull(providerNode?.type);
  const compatibleVisual = getCompatibleProviderVisual(providerNodeType);
  return {
    alias: toStringOrNull(providerNode?.prefix) || providerId,
    providerType: providerNodeType || providerId,
    ...compatibleVisual,
  };
}

function deriveConnectionStatus(connection: ProviderConnectionLike): BuilderConnectionStatus {
  if (connection.isActive === false) return "inactive";
  const rateLimitedUntil = toNumberOrNull(connection.rateLimitedUntil);
  if (typeof rateLimitedUntil === "number" && rateLimitedUntil > Date.now()) {
    return "rate-limited";
  }
  if (typeof connection.testStatus === "string" && /error|fail/i.test(connection.testStatus)) {
    return "error";
  }
  return "active";
}

function buildConnectionOption(
  connection: ProviderConnectionLike
): ComboBuilderConnectionOption | null {
  const id = toStringOrNull(connection.id);
  if (!id) return null;

  return {
    id,
    label: getAccountDisplayName(connection),
    type: toStringOrNull(connection.authType) || "unknown",
    status: deriveConnectionStatus(connection),
    priority: typeof connection.priority === "number" ? connection.priority : 0,
    isActive: connection.isActive !== false,
    defaultModel: toStringOrNull(connection.defaultModel),
    rateLimitedUntil: toNumberOrNull(connection.rateLimitedUntil),
    lastError: toStringOrNull(connection.lastError),
    lastTested: toStringOrNull(connection.lastTested),
  };
}

function addModelOption(
  modelMap: Map<string, ComboBuilderModelOption>,
  providerId: string,
  input: {
    id: string | null;
    name?: string | null;
    source: BuilderModelSource;
    supportedEndpoints?: string[];
    apiFormat?: string | null;
    contextLength?: number | null;
    outputTokenLimit?: number | null;
    supportsThinking?: boolean;
  }
) {
  const modelId = toStringOrNull(input.id);
  if (!modelId) return;
  if (getModelIsHidden(providerId, modelId)) return;
  if (!isChatCapable(input.supportedEndpoints)) return;

  const nextSourcePriority = getSourcePriority(input.source);
  const existing = modelMap.get(modelId);
  if (!existing) {
    modelMap.set(modelId, {
      id: modelId,
      qualifiedModel: `${providerId}/${modelId}`,
      name: toStringOrNull(input.name) || modelId,
      source: input.source,
      sources: [input.source],
      ...(input.supportedEndpoints && input.supportedEndpoints.length > 0
        ? { supportedEndpoints: input.supportedEndpoints }
        : {}),
      ...(toStringOrNull(input.apiFormat) ? { apiFormat: input.apiFormat || undefined } : {}),
      ...(typeof input.contextLength === "number" ? { contextLength: input.contextLength } : {}),
      ...(typeof input.outputTokenLimit === "number"
        ? { outputTokenLimit: input.outputTokenLimit }
        : {}),
      ...(typeof input.supportsThinking === "boolean"
        ? { supportsThinking: input.supportsThinking }
        : {}),
    });
    return;
  }

  const existingPriority = getSourcePriority(existing.source);
  const mergedSources = new Set<BuilderModelSource>([...existing.sources, input.source]);

  if (nextSourcePriority < existingPriority) {
    existing.source = input.source;
  }
  if (!existing.name || existing.name === existing.id) {
    existing.name = toStringOrNull(input.name) || existing.name;
  }
  if (!existing.supportedEndpoints && input.supportedEndpoints?.length) {
    existing.supportedEndpoints = input.supportedEndpoints;
  }
  if (!existing.apiFormat && toStringOrNull(input.apiFormat)) {
    existing.apiFormat = input.apiFormat || undefined;
  }
  if (existing.contextLength == null && typeof input.contextLength === "number") {
    existing.contextLength = input.contextLength;
  }
  if (existing.outputTokenLimit == null && typeof input.outputTokenLimit === "number") {
    existing.outputTokenLimit = input.outputTokenLimit;
  }
  if (existing.supportsThinking == null && typeof input.supportsThinking === "boolean") {
    existing.supportsThinking = input.supportsThinking;
  }
  existing.sources = Array.from(mergedSources).sort(
    (left, right) => getSourcePriority(left) - getSourcePriority(right)
  );
}

function compareConnections(
  left: ComboBuilderConnectionOption,
  right: ComboBuilderConnectionOption
): number {
  if (left.isActive !== right.isActive) return left.isActive ? -1 : 1;
  if (left.priority !== right.priority) return left.priority - right.priority;
  return left.label.localeCompare(right.label, undefined, { sensitivity: "base" });
}

function compareModels(left: ComboBuilderModelOption, right: ComboBuilderModelOption): number {
  const sourceDelta = getSourcePriority(left.source) - getSourcePriority(right.source);
  if (sourceDelta !== 0) return sourceDelta;
  const nameDelta = left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
  if (nameDelta !== 0) return nameDelta;
  return left.id.localeCompare(right.id, undefined, { sensitivity: "base" });
}

function compareProviders(
  left: ComboBuilderProviderOption,
  right: ComboBuilderProviderOption
): number {
  if (left.activeConnectionCount !== right.activeConnectionCount) {
    return right.activeConnectionCount - left.activeConnectionCount;
  }
  return left.displayName.localeCompare(right.displayName, undefined, { sensitivity: "base" });
}

function normalizeCustomModels(raw: unknown): CustomModelLike[] {
  return Array.isArray(raw)
    ? raw.filter(
        (model): model is CustomModelLike =>
          Boolean(model) && typeof model === "object" && !Array.isArray(model)
      )
    : [];
}

function normalizeSyncedModels(raw: unknown): SyncedModelLike[] {
  return Array.isArray(raw)
    ? raw.filter(
        (model): model is SyncedModelLike =>
          Boolean(model) && typeof model === "object" && !Array.isArray(model)
      )
    : [];
}

export async function getComboBuilderOptions(): Promise<ComboBuilderOptionsPayload> {
  getSyncedCapabilities();
  const [connections, providerNodes, customModelsMap, syncedModelsMap, combos] = await Promise.all([
    getProviderConnections(),
    getProviderNodes(),
    getAllCustomModels(),
    getAllSyncedAvailableModels(),
    getCombos(),
  ]);

  const providerNodeMap = new Map<string, ProviderNodeLike>();
  for (const providerNode of providerNodes as ProviderNodeLike[]) {
    const providerId = toStringOrNull(providerNode.id);
    if (!providerId) continue;
    providerNodeMap.set(providerId, providerNode);
  }

  const connectionsByProvider = new Map<string, ProviderConnectionLike[]>();
  for (const connection of connections as ProviderConnectionLike[]) {
    const providerId = toStringOrNull(connection.provider);
    if (!providerId) continue;
    const list = connectionsByProvider.get(providerId) || [];
    list.push(connection);
    connectionsByProvider.set(providerId, list);
  }

  const providers: ComboBuilderProviderOption[] = [];

  for (const [providerId, providerConnections] of connectionsByProvider) {
    const providerNode = providerNodeMap.get(providerId) || null;
    const providerVisual = getProviderVisual(providerId, providerNode);
    const modelMap = new Map<string, ComboBuilderModelOption>();
    const builtInModels = getModelsByProviderId(providerId);
    const syncedModels = normalizeSyncedModels(
      (syncedModelsMap as Record<string, unknown>)[providerId]
    );
    const customModels = normalizeCustomModels(
      (customModelsMap as Record<string, unknown>)[providerId]
    );
    const fallbackModels = getCompatibleFallbackModels(providerId, builtInModels);
    const acceptsArbitraryModel =
      Boolean((AI_PROVIDERS[providerId] as JsonRecord | undefined)?.passthroughModels) ||
      isOpenAICompatibleProvider(providerId) ||
      isAnthropicCompatibleProvider(providerId) ||
      isClaudeCodeCompatibleProvider(providerId);

    for (const model of syncedModels) {
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source: "imported",
        supportedEndpoints: toStringArray(model.supportedEndpoints),
        contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow,
        outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens,
        supportsThinking:
          typeof model.supportsThinking === "boolean"
            ? model.supportsThinking
            : (resolved.supportsThinking ?? undefined),
      });
    }

    for (const model of builtInModels as RegistryModel[]) {
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source: "system",
        contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow,
        outputTokenLimit: resolved.maxOutputTokens,
        supportsThinking: resolved.supportsThinking ?? undefined,
      });
    }

    for (const model of customModels) {
      if (model.isHidden === true) continue;
      const source = ["api-sync", "auto-sync", "imported"].includes(
        toStringOrNull(model.source)?.toLowerCase() || ""
      )
        ? "imported"
        : ("custom" as BuilderModelSource);
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source,
        supportedEndpoints: toStringArray(model.supportedEndpoints),
        apiFormat: toStringOrNull(model.apiFormat),
        contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow,
        outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens,
        supportsThinking:
          typeof model.supportsThinking === "boolean"
            ? model.supportsThinking
            : (resolved.supportsThinking ?? undefined),
      });
    }

    if (Array.isArray(fallbackModels)) {
      for (const model of fallbackModels) {
        const resolved = getResolvedModelCapabilities({
          provider: providerId,
          model: toStringOrNull(model.id),
        });
        addModelOption(modelMap, providerId, {
          id: toStringOrNull(model.id),
          name: toStringOrNull(model.name),
          source: "fallback",
          contextLength:
            typeof (model as { contextLength?: number }).contextLength === "number"
              ? (model as { contextLength?: number }).contextLength || null
              : resolved.contextWindow,
          outputTokenLimit: resolved.maxOutputTokens,
          supportsThinking: resolved.supportsThinking ?? undefined,
        });
      }
    }

    const normalizedConnections = providerConnections
      .map((connection) => buildConnectionOption(connection))
      .filter((connection): connection is ComboBuilderConnectionOption => Boolean(connection))
      .sort(compareConnections);

    const activeConnectionCount = normalizedConnections.filter(
      (connection) => connection.isActive
    ).length;
    const displayName = (providerEntryName(providerId) ||
      getProviderDisplayName(providerId, providerNode) ||
      providerId) as string;

    providers.push({
      providerId,
      providerType: providerVisual.providerType,
      displayName,
      alias: providerVisual.alias,
      prefix: toStringOrNull(providerNode?.prefix),
      icon: providerVisual.icon,
      color: providerVisual.color,
      source: providerVisual.source,
      acceptsArbitraryModel,
      connectionCount: normalizedConnections.length,
      activeConnectionCount,
      modelCount: modelMap.size,
      connections: normalizedConnections,
      models: Array.from(modelMap.values()).sort(compareModels),
    });
  }

  // No-auth providers have no rows in provider_connections, so they are never included in the
  // connectionsByProvider loop above. Add them here so they appear in the combo builder picker.
  for (const noAuthProvider of Object.values(NOAUTH_PROVIDERS)) {
    const providerId = noAuthProvider.id;
    // Skip if already covered (defensive: shouldn't happen for true no-auth providers)
    if (connectionsByProvider.has(providerId)) continue;

    const providerVisual = getProviderVisual(providerId, null);
    const modelMap = new Map<string, ComboBuilderModelOption>();
    const builtInModels = getModelsByProviderId(providerId);
    const syncedModels = normalizeSyncedModels(
      (syncedModelsMap as Record<string, unknown>)[providerId]
    );
    const customModels = normalizeCustomModels(
      (customModelsMap as Record<string, unknown>)[providerId]
    );
    const fallbackModels = getCompatibleFallbackModels(providerId, builtInModels);
    const acceptsArbitraryModel =
      Boolean((AI_PROVIDERS[providerId] as JsonRecord | undefined)?.passthroughModels) ||
      isOpenAICompatibleProvider(providerId) ||
      isAnthropicCompatibleProvider(providerId) ||
      isClaudeCodeCompatibleProvider(providerId);

    for (const model of syncedModels) {
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source: "imported",
        supportedEndpoints: toStringArray(model.supportedEndpoints),
        contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow,
        outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens,
        supportsThinking:
          typeof model.supportsThinking === "boolean"
            ? model.supportsThinking
            : (resolved.supportsThinking ?? undefined),
      });
    }

    for (const model of builtInModels as RegistryModel[]) {
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source: "system",
        contextLength: toNumberOrNull(model.contextLength) ?? resolved.contextWindow,
        outputTokenLimit: resolved.maxOutputTokens,
        supportsThinking: resolved.supportsThinking ?? undefined,
      });
    }

    for (const model of customModels) {
      if (model.isHidden === true) continue;
      const source = ["api-sync", "auto-sync", "imported"].includes(
        toStringOrNull(model.source)?.toLowerCase() || ""
      )
        ? "imported"
        : ("custom" as BuilderModelSource);
      const resolved = getResolvedModelCapabilities({
        provider: providerId,
        model: toStringOrNull(model.id),
      });
      addModelOption(modelMap, providerId, {
        id: toStringOrNull(model.id),
        name: toStringOrNull(model.name),
        source,
        supportedEndpoints: toStringArray(model.supportedEndpoints),
        apiFormat: toStringOrNull(model.apiFormat),
        contextLength: toNumberOrNull(model.inputTokenLimit) ?? resolved.contextWindow,
        outputTokenLimit: toNumberOrNull(model.outputTokenLimit) ?? resolved.maxOutputTokens,
        supportsThinking:
          typeof model.supportsThinking === "boolean"
            ? model.supportsThinking
            : (resolved.supportsThinking ?? undefined),
      });
    }

    if (Array.isArray(fallbackModels)) {
      for (const model of fallbackModels) {
        const resolved = getResolvedModelCapabilities({
          provider: providerId,
          model: toStringOrNull(model.id),
        });
        addModelOption(modelMap, providerId, {
          id: toStringOrNull(model.id),
          name: toStringOrNull(model.name),
          source: "fallback",
          contextLength:
            typeof (model as { contextLength?: number }).contextLength === "number"
              ? (model as { contextLength?: number }).contextLength || null
              : resolved.contextWindow,
          outputTokenLimit: resolved.maxOutputTokens,
          supportsThinking: resolved.supportsThinking ?? undefined,
        });
      }
    }

    // #2901: no-auth providers must route under their alias (e.g. "oc"), not
    // their id — "opencode/<model>" misroutes to the opencode-zen api-key tier
    // (manual ALIAS_TO_PROVIDER_ID override), while "oc/<model>" resolves to the
    // no-auth "opencode" provider. Rewrite qualifiedModel to the alias prefix.
    const routingPrefix = noAuthProvider.alias || providerId;
    if (routingPrefix !== providerId) {
      for (const opt of modelMap.values()) {
        opt.qualifiedModel = `${routingPrefix}/${opt.id}`;
      }
    }

    const displayName = (providerEntryName(providerId) ||
      getProviderDisplayName(providerId, null) ||
      providerId) as string;

    providers.push({
      providerId,
      providerType: providerVisual.providerType,
      displayName,
      alias: providerVisual.alias,
      prefix: null,
      icon: providerVisual.icon,
      color: providerVisual.color,
      source: providerVisual.source,
      acceptsArbitraryModel,
      connectionCount: 0,
      activeConnectionCount: 0,
      modelCount: modelMap.size,
      connections: [],
      models: Array.from(modelMap.values()).sort(compareModels),
    });
  }

  const comboRefs = (combos as JsonRecord[])
    .filter((combo) => combo.isHidden !== true && combo.isActive !== false)
    .map((combo) => ({
      id: toStringOrNull(combo.id) || toStringOrNull(combo.name) || "combo",
      name: toStringOrNull(combo.name) || "combo",
      strategy: toStringOrNull(combo.strategy) || "priority",
      stepCount: Array.isArray(combo.models) ? combo.models.length : 0,
      version: typeof combo.version === "number" ? combo.version : 2,
      ...(typeof combo.sortOrder === "number" ? { sortOrder: combo.sortOrder } : {}),
    }))
    .sort((left, right) => {
      const leftSort =
        typeof left.sortOrder === "number" ? left.sortOrder : Number.MAX_SAFE_INTEGER;
      const rightSort =
        typeof right.sortOrder === "number" ? right.sortOrder : Number.MAX_SAFE_INTEGER;
      if (leftSort !== rightSort) return leftSort - rightSort;
      return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
    });

  return {
    schemaVersion: 2,
    generatedAt: new Date().toISOString(),
    providers: providers.sort(compareProviders),
    comboRefs,
  };
}

function providerEntryName(providerId: string): string | null {
  const providerEntry = AI_PROVIDERS[providerId] as { name?: string } | undefined;
  return toStringOrNull(providerEntry?.name);
}
