import {
  EMBEDDING_PROVIDERS,
  buildDynamicEmbeddingProvider,
  type EmbeddingProviderNodeRow,
} from "@omniroute/open-sse/config/embeddingRegistry.ts";
import { getProviderCredentials } from "@/sse/services/auth";
import { getProviderNodes } from "@/lib/localDb";
import type { MemorySettingsExtended } from "@/shared/schemas/memory";
import type {
  EmbeddingResolution,
  EmbeddingResult,
  EmbeddingError,
  EmbeddingProviderListing,
} from "./types";
import { embedRemote } from "./remote";
import { embedStatic } from "./staticPotion";
import { embedTransformers } from "./transformersLocal";
import {
  buildCacheKey,
  get as cacheGet,
  set as cacheSet,
  invalidate as cacheInvalidate,
} from "./cache";

const STATIC_MODEL = process.env.MEMORY_STATIC_MODEL || "minishlab/potion-base-8M";
const TRANSFORMERS_MODEL =
  process.env.MEMORY_TRANSFORMERS_MODEL || "Xenova/all-MiniLM-L6-v2";

/** Build an EmbeddingResolution for "no source available" cases. */
function noSource(reason: string): EmbeddingResolution {
  return {
    source: null,
    model: null,
    dimensions: null,
    signature: "null:null:null",
    reason,
  };
}

/** Build a signature string. */
function makeSignature(
  source: "remote" | "static" | "transformers" | null,
  model: string | null,
  dim: number | null
): string {
  return `${source ?? "null"}:${model ?? "null"}:${dim ?? "null"}`;
}

/**
 * Resolve which embedding source is active for the given settings (D4).
 * Pure: no heavy I/O. Provider key check done via synchronous registry lookup.
 */
export function resolveEmbeddingSource(settings: MemorySettingsExtended): EmbeddingResolution {
  const source = settings.embeddingSource ?? "auto";

  if (source === "remote") {
    // Explicit remote — check if the configured model has a key
    const model = settings.embeddingProviderModel ?? null;
    if (!model) {
      return {
        source: null,
        model: null,
        dimensions: null,
        signature: makeSignature(null, null, null),
        reason: "no_key: embeddingProviderModel não configurado",
      };
    }
    // We can't do async here, so we report it as potentially available
    // and the caller will attempt embed + get no_key error on failure.
    // For resolution purposes, mark as remote (will fail at embed time if no key).
    return {
      source: "remote",
      model,
      dimensions: null,
      signature: makeSignature("remote", model, null),
      reason: `provider remoto configurado: ${model}`,
    };
  }

  if (source === "static") {
    if (settings.staticEnabled !== true) {
      return {
        source: null,
        model: null,
        dimensions: null,
        signature: makeSignature(null, null, null),
        reason: "static desabilitado nas configurações",
      };
    }
    return {
      source: "static",
      model: STATIC_MODEL,
      dimensions: 256,
      signature: makeSignature("static", STATIC_MODEL, 256),
      reason: "static (potion-base-8M) selecionado explicitamente",
    };
  }

  if (source === "transformers") {
    if (settings.transformersEnabled !== true) {
      return {
        source: null,
        model: null,
        dimensions: null,
        signature: makeSignature(null, null, null),
        reason: "transformers desabilitado nas configurações",
      };
    }
    return {
      source: "transformers",
      model: TRANSFORMERS_MODEL,
      dimensions: 384,
      signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
      reason: "transformers.js (MiniLM-L6-v2) selecionado explicitamente",
    };
  }

  // auto: (1) remote if model configured and provider has key in registry
  // (2) static if staticEnabled
  // (3) transformers if transformersEnabled
  // (4) null
  if (source === "auto") {
    // Try remote first — check if embeddingProviderModel is set
    const providerModel = settings.embeddingProviderModel ?? null;
    if (providerModel) {
      const slashIdx = providerModel.indexOf("/");
      const providerId = slashIdx > 0 ? providerModel.slice(0, slashIdx) : null;
      if (providerId && EMBEDDING_PROVIDERS[providerId]) {
        // We defer the actual hasKey check to listEmbeddingProviders (async).
        // For resolveEmbeddingSource (sync), we report "possibly remote" when model is set.
        // If no key, embed will return EmbeddingError{reason:"no_key"}.
        return {
          source: "remote",
          model: providerModel,
          dimensions: null,
          signature: makeSignature("remote", providerModel, null),
          reason: `auto: provider ${providerId} configurado`,
        };
      }
    }

    if (settings.staticEnabled === true) {
      return {
        source: "static",
        model: STATIC_MODEL,
        dimensions: 256,
        signature: makeSignature("static", STATIC_MODEL, 256),
        reason: "auto: potion-base-8M (static) disponível",
      };
    }

    if (settings.transformersEnabled === true) {
      return {
        source: "transformers",
        model: TRANSFORMERS_MODEL,
        dimensions: 384,
        signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
        reason: "auto: transformers.js (MiniLM-L6-v2) disponível",
      };
    }

    return noSource("auto: nenhuma fonte de embedding disponível");
  }

  return noSource("fonte de embedding desconhecida");
}

/**
 * Generate an embedding for the given text using the active source.
 * Caches results in memory (D6).
 */
export async function embed(
  text: string,
  settings: MemorySettingsExtended
): Promise<EmbeddingResult | EmbeddingError> {
  const resolution = resolveEmbeddingSource(settings);

  if (!resolution.source) {
    return {
      source: "remote",
      model: null,
      reason: "unknown",
      message: resolution.reason,
    };
  }

  const cacheKey = buildCacheKey(
    resolution.source,
    resolution.model,
    resolution.dimensions,
    text
  );

  const cached = cacheGet(cacheKey);
  if (cached) {
    return {
      vector: cached,
      source: resolution.source,
      model: resolution.model ?? "",
      dimensions: cached.length,
      latencyMs: 0,
      cached: true,
    };
  }

  let result: EmbeddingResult | EmbeddingError;

  if (resolution.source === "remote") {
    result = await embedRemote(text, resolution.model ?? "");
  } else if (resolution.source === "static") {
    result = await embedStatic(text);
  } else {
    result = await embedTransformers(text);
  }

  if ("vector" in result) {
    cacheSet(cacheKey, result.vector);
  }

  return result;
}

/**
 * List providers that have embedding models, marking which ones have a configured API key.
 * Aggregates from EMBEDDING_PROVIDERS + local provider_nodes.
 */
export async function listEmbeddingProviders(): Promise<EmbeddingProviderListing[]> {
  // Get dynamic local providers
  let dynamicProviders: ReturnType<typeof buildDynamicEmbeddingProvider>[] = [];
  try {
    const nodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[];
    dynamicProviders = (Array.isArray(nodes) ? nodes : [])
      .filter((n) => {
        const validTypes = ["chat", "responses", "embeddings"];
        return validTypes.includes(n.apiType || "");
      })
      .map((n) => {
        try {
          return buildDynamicEmbeddingProvider(n);
        } catch {
          return null;
        }
      })
      .filter((p): p is NonNullable<typeof p> => p !== null);
  } catch {
    // Ignore failures — just return static providers
  }

  const result: EmbeddingProviderListing[] = [];

  // Process hardcoded EMBEDDING_PROVIDERS
  for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
    let hasKey = false;
    try {
      const creds = await getProviderCredentials(providerId);
      hasKey = !!(
        creds &&
        !("allRateLimited" in creds && creds.allRateLimited) &&
        (("apiKey" in creds ? !!creds.apiKey : false) ||
          ("accessToken" in creds ? !!creds.accessToken : false))
      );
    } catch {
      hasKey = false;
    }

    result.push({
      provider: providerId,
      hasKey,
      models: config.models.map((m) => ({
        id: `${providerId}/${m.id}`,
        name: m.name,
        dimensions: m.dimensions ?? null,
      })),
    });
  }

  // Process dynamic providers (local nodes)
  for (const dp of dynamicProviders) {
    // Dynamic local providers typically have authType="none"
    result.push({
      provider: dp.id,
      hasKey: true, // local providers don't need keys
      models: dp.models.map((m) => ({
        id: `${dp.id}/${m.id}`,
        name: m.name,
        dimensions: m.dimensions ?? null,
      })),
    });
  }

  return result;
}

/**
 * Drop the in-memory embedding cache.
 * Called when settings (model/source) change.
 */
export function invalidateEmbeddingCache(): void {
  cacheInvalidate();
}
