import { modelCatalog } from "@internal/llm-model-catalog";
import type { CreateEventInput, LlmMetricsData } from "../eventRepository/eventRepository.types";

// Registry interface — matches ModelPricingRegistry from @internal/llm-model-catalog
type CostRegistry = {
  isLoaded: boolean;
  calculateCost(
    responseModel: string,
    usageDetails: Record<string, number>
  ): {
    matchedModelId: string;
    matchedModelName: string;
    pricingTierId: string;
    pricingTierName: string;
    inputCost: number;
    outputCost: number;
    totalCost: number;
    costDetails: Record<string, number>;
  } | null;
};

let _registry: CostRegistry | undefined;

const ENRICHABLE_KINDS = new Set(["INTERNAL", "SERVER", "CLIENT", "CONSUMER", "PRODUCER"]);

export function setLlmPricingRegistry(registry: CostRegistry): void {
  _registry = registry;
}

export function enrichCreatableEvents(events: CreateEventInput[]) {
  return events.map((event) => {
    return enrichCreatableEvent(event);
  });
}

function enrichCreatableEvent(event: CreateEventInput): CreateEventInput {
  const message = formatPythonStyle(event.message, event.properties);

  event.message = message;
  event.style = enrichStyle(event);

  enrichLlmMetrics(event);
  enrichPromptResolve(event);

  return event;
}

function enrichLlmMetrics(event: CreateEventInput): void {
  const props = event.properties;
  if (!props) return;

  // Only enrich span-like events (INTERNAL, SERVER, CLIENT, CONSUMER, PRODUCER — not LOG, UNSPECIFIED)
  if (!ENRICHABLE_KINDS.has(event.kind as string)) return;

  // Skip partial spans (they don't have final token counts)
  if (event.isPartial) return;

  // Only use gen_ai.* attributes for model resolution to avoid double-counting.
  // The Vercel AI SDK emits both a parent span (ai.streamText with ai.usage.*)
  // and a child span (ai.streamText.doStream with gen_ai.*). We only enrich the
  // child span that has the canonical gen_ai.response.model attribute.
  const responseModel =
    typeof props["gen_ai.response.model"] === "string"
      ? props["gen_ai.response.model"]
      : typeof props["gen_ai.request.model"] === "string"
        ? props["gen_ai.request.model"]
        : null;

  if (!responseModel) {
    return;
  }

  // Extract usage details, normalizing attribute names
  const usageDetails = extractUsageDetails(props);

  // Need at least some token usage
  const hasTokens = Object.values(usageDetails).some((v) => v > 0);
  if (!hasTokens) {
    return;
  }

  // Add style accessories for model and tokens (even without cost data)
  const inputTokens = usageDetails["input"] ?? 0;
  const outputTokens = usageDetails["output"] ?? 0;
  const totalTokens = usageDetails["total"] ?? inputTokens + outputTokens;

  const pillItems: Array<{ text: string; icon: string }> = [
    { text: responseModel, icon: "tabler-cube" },
    { text: formatTokenCount(totalTokens), icon: "tabler-hash" },
  ];

  // Provider-reported cost (gateway/openrouter) is the exact per-request bill and already
  // reflects cache-read discounts and the real per-provider rate, so prefer it and only fall
  // back to catalog pricing when it is absent. The registry handles prefix stripping (e.g.
  // "mistral/mistral-large-3" → "mistral-large-3") for gateway/openrouter models in match().
  const providerCost = extractProviderCost(props);

  let cost: ReturnType<NonNullable<typeof _registry>["calculateCost"]> | null = null;
  if (!providerCost && _registry?.isLoaded) {
    cost = _registry.calculateCost(responseModel, usageDetails);
  }

  if (cost) {
    // Add trigger.llm.* attributes to the span from our pricing registry
    event.properties = {
      ...props,
      "trigger.llm.input_cost": cost.inputCost,
      "trigger.llm.output_cost": cost.outputCost,
      "trigger.llm.total_cost": cost.totalCost,
      "trigger.llm.cached_cost": cost.costDetails["input_cached_tokens"] ?? 0,
      "trigger.llm.cache_creation_cost": cost.costDetails["cache_creation_input_tokens"] ?? 0,
      "trigger.llm.matched_model": cost.matchedModelName,
      "trigger.llm.matched_model_id": cost.matchedModelId,
      "trigger.llm.pricing_tier": cost.pricingTierName,
      "trigger.llm.pricing_tier_id": cost.pricingTierId,
    };

    pillItems.push({ text: formatCost(cost.totalCost), icon: "tabler-currency-dollar" });
  } else if (providerCost) {
    // Use provider-reported cost as fallback (no input/output breakdown available)
    event.properties = {
      ...props,
      "trigger.llm.total_cost": providerCost.totalCost,
      "trigger.llm.cost_source": providerCost.source,
    };

    pillItems.push({ text: formatCost(providerCost.totalCost), icon: "tabler-currency-dollar" });
  }

  event.style = {
    ...(event.style as Record<string, unknown> | undefined),
    accessory: {
      style: "pills",
      items: pillItems,
    },
  } as unknown as typeof event.style;

  // Only write llm_metrics when cost data is available
  if (!cost && !providerCost) return;

  // Build metadata map from run tags and ai.telemetry.metadata.*
  const metadata: Record<string, string> = {};

  if (event.runTags) {
    for (const tag of event.runTags) {
      const colonIdx = tag.indexOf(":");
      if (colonIdx > 0) {
        metadata[tag.substring(0, colonIdx)] = tag.substring(colonIdx + 1);
      }
    }
  }

  for (const [key, value] of Object.entries(props)) {
    if (key.startsWith("ai.telemetry.metadata.") && typeof value === "string") {
      metadata[key.slice("ai.telemetry.metadata.".length)] = value;
    }
  }

  // Extract new performance/behavioral fields.
  // v6 emits ai.response.finishReason (plain string); v7 (@ai-sdk/otel) emits
  // gen_ai.response.finish_reasons as a JSON array string (e.g. `["stop"]`).
  const finishReason = readFinishReason(props);
  const operationId =
    typeof props["ai.operationId"] === "string"
      ? props["ai.operationId"]
      : typeof props["gen_ai.operation.name"] === "string"
        ? props["gen_ai.operation.name"]
        : typeof props["operation.name"] === "string"
          ? props["operation.name"]
          : "";
  const msToFirstChunk =
    typeof props["ai.response.msToFirstChunk"] === "number"
      ? props["ai.response.msToFirstChunk"]
      : 0;
  const avgTokensPerSec =
    typeof props["ai.response.avgOutputTokensPerSecond"] === "number"
      ? props["ai.response.avgOutputTokensPerSecond"]
      : 0;
  const costSource = cost ? "registry" : providerCost ? providerCost.source : "";
  const providerCostValue = providerCost?.totalCost ?? 0;

  // Set _llmMetrics side-channel for dual-write to llm_metrics_v1
  const llmMetrics: LlmMetricsData = {
    genAiSystem:
      typeof props["gen_ai.system"] === "string"
        ? props["gen_ai.system"]
        : typeof props["gen_ai.provider.name"] === "string"
          ? props["gen_ai.provider.name"]
          : "unknown",
    requestModel:
      typeof props["gen_ai.request.model"] === "string"
        ? props["gen_ai.request.model"]
        : responseModel,
    responseModel,
    baseResponseModel: modelCatalog[responseModel]?.baseModelName ?? responseModel,
    matchedModelId: cost?.matchedModelId ?? "",
    operationId,
    finishReason,
    costSource,
    pricingTierId: cost?.pricingTierId ?? (providerCost ? `provider:${providerCost.source}` : ""),
    pricingTierName:
      cost?.pricingTierName ?? (providerCost ? `${providerCost.source} reported` : ""),
    inputTokens: usageDetails["input"] ?? 0,
    outputTokens: usageDetails["output"] ?? 0,
    totalTokens:
      usageDetails["total"] ?? (usageDetails["input"] ?? 0) + (usageDetails["output"] ?? 0),
    usageDetails,
    inputCost: cost?.inputCost ?? 0,
    outputCost: cost?.outputCost ?? 0,
    totalCost: cost?.totalCost ?? providerCost?.totalCost ?? 0,
    costDetails: cost?.costDetails ?? {},
    providerCost: providerCostValue,
    msToFirstChunk,
    tokensPerSecond: avgTokensPerSec,
    metadata,
    promptSlug: metadata["prompt.slug"] ?? "",
    promptVersion: parseInt(metadata["prompt.version"] ?? "0", 10) || 0,
  };

  event._llmMetrics = llmMetrics;
}

function extractUsageDetails(props: Record<string, unknown>): Record<string, number> {
  const details: Record<string, number> = {};

  // Only map gen_ai.usage.* attributes — NOT ai.usage.* from parent spans.
  // This prevents double-counting when both parent (ai.streamText) and child
  // (ai.streamText.doStream) spans carry token counts.
  const mappings: Record<string, string> = {
    "gen_ai.usage.input_tokens": "input",
    "gen_ai.usage.output_tokens": "output",
    "gen_ai.usage.prompt_tokens": "input",
    "gen_ai.usage.completion_tokens": "output",
    "gen_ai.usage.total_tokens": "total",
    "gen_ai.usage.cache_read_input_tokens": "input_cached_tokens",
    "gen_ai.usage.input_tokens_cache_read": "input_cached_tokens",
    // AI SDK 7 (@ai-sdk/otel) nests cache token counts: gen_ai.usage.cache_read.input_tokens
    "gen_ai.usage.cache_read.input_tokens": "input_cached_tokens",
    "gen_ai.usage.cache_creation_input_tokens": "cache_creation_input_tokens",
    "gen_ai.usage.input_tokens_cache_write": "cache_creation_input_tokens",
    "gen_ai.usage.cache_creation.input_tokens": "cache_creation_input_tokens",
    "gen_ai.usage.reasoning_tokens": "reasoning_tokens",
  };

  for (const [attrKey, usageKey] of Object.entries(mappings)) {
    const value = props[attrKey];
    if (typeof value === "number" && value > 0) {
      // Don't overwrite if already set (first mapping wins)
      if (details[usageKey] === undefined) {
        details[usageKey] = value;
      }
    }
  }

  return details;
}

/**
 * Resolve the finish reason across AI SDK majors. v6 emits
 * `ai.response.finishReason` as a plain string; v7 (@ai-sdk/otel) emits
 * `gen_ai.response.finish_reasons` as a JSON array string (e.g. `["stop"]`).
 */
function readFinishReason(props: Record<string, unknown>): string {
  const v6 = props["ai.response.finishReason"];
  if (typeof v6 === "string" && v6) return v6;

  const v7 = props["gen_ai.response.finish_reasons"];
  if (typeof v7 === "string" && v7) {
    const trimmed = v7.trim();
    if (trimmed.startsWith("[")) {
      try {
        const parsed = JSON.parse(trimmed);
        if (Array.isArray(parsed)) {
          const first = parsed.find((r) => typeof r === "string");
          if (typeof first === "string") return first;
        }
      } catch {
        // fall through to the raw value
      }
    }
    return v7;
  }

  return "";
}

function enrichStyle(event: CreateEventInput) {
  const baseStyle = event.style ?? {};
  const props = event.properties;

  if (!props) {
    return baseStyle;
  }

  const system = props["gen_ai.system"] ?? props["gen_ai.provider.name"];
  const modelId = props["gen_ai.request.model"] ?? props["ai.model.id"];

  const provider = resolveAiProvider(
    typeof system === "string" ? system : undefined,
    typeof modelId === "string" ? modelId : undefined
  );

  if (provider) {
    return { ...baseStyle, icon: `ai-provider-${provider}` };
  }

  // Agent workflow check
  const name = props["name"];
  if (typeof name === "string" && name.includes("Agent workflow")) {
    return { ...baseStyle, icon: "tabler-brain" };
  }

  const message = event.message;

  if (typeof message === "string" && message === "ai.toolCall") {
    return { ...baseStyle, icon: "hero-wrench" };
  }

  if (typeof message === "string" && message.startsWith("ai.")) {
    return { ...baseStyle, icon: "hero-sparkles" };
  }

  return baseStyle;
}

function formatTokenCount(tokens: number): string {
  if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
  if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
  return tokens.toString();
}

/**
 * Extract provider-reported cost from ai.response.providerMetadata.
 * Gateway and OpenRouter include per-request cost in their metadata.
 */
function extractProviderCost(
  props: Record<string, unknown>
): { totalCost: number; source: string } | null {
  const rawMeta = props["ai.response.providerMetadata"];
  if (typeof rawMeta !== "string") return null;

  // Cheap guard: providerMetadata can be large for reasoning models (it carries the full
  // reasoning_details text), and this now runs on every AI span. Skip the JSON parse when
  // there is no cost field to find.
  if (!rawMeta.includes('"cost"')) return null;

  let meta: Record<string, unknown>;
  try {
    meta = JSON.parse(rawMeta) as Record<string, unknown>;
  } catch {
    return null;
  }

  if (!meta || typeof meta !== "object") return null;

  // Gateway: { gateway: { cost: "0.0006615" } }
  const gateway = meta.gateway;
  if (gateway && typeof gateway === "object") {
    const gw = gateway as Record<string, unknown>;
    const cost = parseFloat(String(gw.cost ?? "0"));
    if (cost > 0) return { totalCost: cost, source: "gateway" };
  }

  // OpenRouter: { openrouter: { usage: { cost: 0.000135 } } }
  const openrouter = meta.openrouter;
  if (openrouter && typeof openrouter === "object") {
    const or = openrouter as Record<string, unknown>;
    const usage = or.usage;
    if (usage && typeof usage === "object") {
      const cost = Number((usage as Record<string, unknown>).cost ?? 0);
      if (cost > 0) return { totalCost: cost, source: "openrouter" };
    }
  }

  return null;
}

function formatCost(cost: number): string {
  if (cost >= 1) return `$${cost.toFixed(2)}`;
  if (cost >= 0.01) return `$${cost.toFixed(4)}`;
  return `$${cost.toFixed(6)}`;
}

function repr(value: any): string {
  if (typeof value === "string") {
    return `'${value}'`;
  }
  return String(value);
}

function formatPythonStyle(template: string, values: Record<string, any>): string {
  // Early return if template is too long
  if (template.length >= 256) {
    return template;
  }

  // Early return if no template variables present
  if (!template.includes("{")) {
    return template;
  }

  return template.replace(/\{([^}]+?)(?:!r)?\}/g, (match, key) => {
    const hasRepr = match.endsWith("!r}");
    const actualKey = hasRepr ? key : key;
    const value = values?.[actualKey];

    if (value === undefined) {
      return match;
    }

    return hasRepr ? repr(value) : String(value);
  });
}

type AiProvider =
  | "anthropic"
  | "openai"
  | "gemini"
  | "llama"
  | "deepseek"
  | "xai"
  | "perplexity"
  | "cerebras"
  | "azure"
  | "mistral";

const systemToProvider: Record<string, AiProvider> = {
  anthropic: "anthropic",
  openai: "openai",
  azure: "azure",
  "google.generative-ai": "gemini",
  google: "gemini",
  xai: "xai",
  deepseek: "deepseek",
  cerebras: "cerebras",
  perplexity: "perplexity",
  "meta-llama": "llama",
  mistral: "mistral",
};

const modelPatterns: [RegExp, AiProvider][] = [
  [/\banthropic\b|claude/i, "anthropic"],
  [/\bopenai\b|gpt-|o[134]-|chatgpt/i, "openai"],
  [/gemini/i, "gemini"],
  [/llama/i, "llama"],
  [/deepseek/i, "deepseek"],
  [/grok/i, "xai"],
  [/sonar/i, "perplexity"],
  [/cerebras/i, "cerebras"],
  [/mistral|mixtral|codestral|pixtral/i, "mistral"],
];

function resolveAiProvider(
  system: string | undefined,
  modelId: string | undefined
): AiProvider | undefined {
  if (modelId) {
    if (modelId.includes("/")) {
      const prefix = modelId.split("/")[0].toLowerCase();
      const fromPrefix = systemToProvider[prefix];
      if (fromPrefix) return fromPrefix;
    }

    for (const [pattern, provider] of modelPatterns) {
      if (pattern.test(modelId)) return provider;
    }
  }

  if (system) {
    const normalized = system.toLowerCase().split(".")[0];
    return systemToProvider[system] ?? systemToProvider[normalized];
  }

  return undefined;
}

function enrichPromptResolve(event: CreateEventInput): void {
  const props = event.properties;
  if (!props) return;

  const slug = props["prompt.slug"];
  const version = props["prompt.version"];

  if (typeof slug !== "string") return;

  const style = (event.style ?? {}) as Record<string, unknown>;
  const accessory = style.accessory as Record<string, unknown> | undefined;
  const existingItems =
    accessory && "items" in accessory
      ? (accessory.items as Array<{ text: string; icon?: string; variant?: string }>)
      : [];

  const items = [
    ...existingItems,
    {
      text: `${slug}${typeof version === "number" ? ` v${version}` : ""}`,
      icon: "tabler-file-text-ai",
    },
  ];

  event.style = {
    ...style,
    icon: style.icon ?? "tabler-file-text-ai",
    accessory: { style: "pills" as const, items },
  } as unknown as typeof event.style;
}
