import type { JSONObject } from "@ai-sdk/provider";

import { APICallError, generateText, streamText } from "ai";
import { isError } from "es-toolkit";

import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { LlmTargetConfig, RawLlmExpectConfig, ResolvedLlmExpectConfig, ResolvedLlmTarget } from "./types";

import { resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { resolveKeyedExpectations } from "../../expect/keyed";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { runExpects } from "./expect";
import {
  buildObservationFromApiCallError,
  buildObservationFromGenerateText,
  buildObservationFromStreamText,
  toPersistedObservation,
} from "./observation";
import { createProviderModel } from "./provider";
import { llmCheckerSchemas } from "./schema";
import { validateLlmConfig } from "./validate";

export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
  readonly configKey = "llm";
  readonly schemas = llmCheckerSchemas;
  readonly type = "llm";

  buildDetail(observation: Record<string, unknown>): null | string {
    const provider = observation["provider"];
    const mode = observation["mode"];
    const parts: string[] = [`LLM ${String(provider)} ${String(mode)}`];

    const http = observation["http"] as null | Record<string, unknown> | undefined;
    if (http && typeof http["status"] === "number") {
      parts.push(String(http["status"]));
    }

    if (typeof observation["finishReason"] === "string") {
      parts.push(`finish=${observation["finishReason"]}`);
    }

    if (typeof observation["rawFinishReason"] === "string") {
      parts.push(`raw=${observation["rawFinishReason"]}`);
    }

    const stream = observation["stream"] as null | Record<string, unknown> | undefined;
    if (stream && typeof stream["firstTokenMs"] === "number") {
      parts.push(`firstToken=${stream["firstTokenMs"]}ms`);
    }

    if (typeof observation["outputLength"] === "number") {
      parts.push(`output=${observation["outputLength"]} chars`);
    }

    const usage = observation["usage"] as null | Record<string, unknown> | undefined;
    if (usage) {
      const inputTokens = typeof usage["inputTokens"] === "number" ? usage["inputTokens"] : 0;
      const outputTokens = typeof usage["outputTokens"] === "number" ? usage["outputTokens"] : 0;
      parts.push(`usage=${inputTokens}/${outputTokens} tokens`);
    }

    return parts.join(", ");
  }

  async execute(t: ResolvedLlmTarget, ctx: CheckerContext): Promise<CheckResult> {
    const timestamp = new Date().toISOString();
    const expect = t.expect;
    const start = performance.now();

    try {
      const { http: httpMeta, model } = createProviderModel(t.llm);

      if (t.llm.mode === "stream") {
        return await this.executeStream(t, model, httpMeta, expect, ctx, timestamp, start);
      }

      return await this.executeHttp(t, model, httpMeta, expect, ctx, timestamp, start);
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);

      if (error instanceof APICallError) {
        const observation = buildObservationFromApiCallError(error, t.llm.provider, t.llm.model, t.llm.mode);

        if (observation.http === null) {
          return {
            detail: null,
            durationMs,
            failure: errorFailure("request", "request", error.message),
            matched: false,
            observation: null,
            targetId: t.id,
            timestamp,
          };
        }

        const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
          message: "durationMs mismatch",
          path: "durationMs",
          phase: "duration",
        });
        const expectResult = runExpects(observation, expect);
        const failure = expectResult.failure ?? durationResult.failure;

        return {
          detail: null,
          durationMs,
          failure,
          matched: failure === null,
          observation: toPersistedObservation(observation),
          targetId: t.id,
          timestamp,
        };
      }

      const isTimeout = ctx.signal.aborted || (error instanceof DOMException && error.name === "AbortError");
      return {
        detail: null,
        durationMs,
        failure: errorFailure(
          "request",
          "request",
          isTimeout ? `请求超时 (${t.timeoutMs}ms)` : isError(error) ? error.message : String(error),
        ),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }
  }

  resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget {
    const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" };
    const llmDefaults = context.defaults["llm"] as
      | undefined
      | {
          headers?: Record<string, string>;
          ignoreSSL?: boolean;
          mode?: string;
          options?: Record<string, unknown>;
          providerOptions?: Record<string, Record<string, unknown>>;
        };

    const resolvedConfig = {
      authToken: t.llm.authToken,
      headers: { ...(llmDefaults?.headers ?? {}), ...(t.llm.headers ?? {}) },
      ignoreSSL: t.llm.ignoreSSL ?? llmDefaults?.ignoreSSL ?? false,
      key: t.llm.key ?? "",
      mode: (t.llm.mode ?? llmDefaults?.mode ?? "http") as "http" | "stream",
      model: t.llm.model,
      options: {
        frequencyPenalty:
          t.llm.options?.frequencyPenalty ?? (llmDefaults?.options?.["frequencyPenalty"] as number | undefined),
        maxOutputTokens:
          t.llm.options?.maxOutputTokens ?? (llmDefaults?.options?.["maxOutputTokens"] as number | undefined) ?? 16,
        presencePenalty:
          t.llm.options?.presencePenalty ?? (llmDefaults?.options?.["presencePenalty"] as number | undefined),
        seed: t.llm.options?.seed ?? (llmDefaults?.options?.["seed"] as number | undefined),
        stopSequences:
          t.llm.options?.stopSequences ?? (llmDefaults?.options?.["stopSequences"] as string[] | undefined),
        temperature: t.llm.options?.temperature ?? (llmDefaults?.options?.["temperature"] as number | undefined) ?? 0,
        topK: t.llm.options?.topK ?? (llmDefaults?.options?.["topK"] as number | undefined),
        topP: t.llm.options?.topP ?? (llmDefaults?.options?.["topP"] as number | undefined),
      },
      prompt: t.llm.prompt,
      provider: t.llm.provider,
      providerOptions: {
        ...((llmDefaults?.providerOptions ?? {}) as Record<string, JSONObject>),
        ...(t.llm.providerOptions ?? {}),
      },
      url: t.llm.url,
    };

    const rawExpect = target.expect as RawLlmExpectConfig | undefined;
    const resolvedExpect: ResolvedLlmExpectConfig = rawExpect
      ? {
          durationMs: resolveValueExpectation(rawExpect.durationMs),
          finishReason: resolveValueExpectation(rawExpect.finishReason),
          headers: resolveKeyedExpectations(rawExpect.headers),
          output: resolveContentExpectations(rawExpect.output),
          rawFinishReason: resolveValueExpectation(rawExpect.rawFinishReason),
          status: rawExpect.status ?? [200],
          stream: rawExpect.stream
            ? {
                completed: rawExpect.stream.completed ?? true,
                firstTokenMs: resolveValueExpectation(rawExpect.stream.firstTokenMs),
              }
            : undefined,
          usage: rawExpect.usage
            ? {
                inputTokens: resolveValueExpectation(rawExpect.usage.inputTokens),
                outputTokens: resolveValueExpectation(rawExpect.usage.outputTokens),
                totalTokens: resolveValueExpectation(rawExpect.usage.totalTokens),
              }
            : undefined,
        }
      : { status: [200] };

    return {
      description: (target.description as null | string) ?? null,
      expect: resolvedExpect,
      group: target.group ?? "default",
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      llm: resolvedConfig,
      name: (target.name as null | string) ?? null,
      rawExpect,
      timeoutMs: context.defaultTimeoutMs,
      type: "llm",
    } satisfies ResolvedLlmTarget;
  }

  serialize(t: ResolvedLlmTarget): { config: string; target: string } {
    return {
      config: JSON.stringify({
        headers: t.llm.headers,
        ignoreSSL: t.llm.ignoreSSL,
        key: t.llm.key ? "***" : "",
        mode: t.llm.mode,
        model: t.llm.model,
        options: t.llm.options,
        prompt: t.llm.prompt,
        provider: t.llm.provider,
        providerOptions: t.llm.providerOptions,
        url: t.llm.url,
      }),
      target: `${t.llm.provider}:${t.llm.model} @ ${t.llm.url}`,
    };
  }

  validate(input: CheckerValidationInput) {
    return validateLlmConfig(input);
  }

  private async executeHttp(
    t: ResolvedLlmTarget,
    model: ReturnType<typeof createProviderModel>["model"],
    httpMeta: null | { headers: Record<string, string>; status: number; statusText: string },
    expect: ResolvedLlmExpectConfig | undefined,
    ctx: CheckerContext,
    timestamp: string,
    start: number,
  ): Promise<CheckResult> {
    const result = await generateText({
      abortSignal: ctx.signal,
      maxRetries: 0,
      model,
      prompt: t.llm.prompt,
      providerOptions: t.llm.providerOptions,
      ...buildSdkOptions(t.llm),
    });

    const respHeaders = result.response?.headers;
    const http = httpMeta ?? {
      headers: respHeaders ? Object.fromEntries(Object.entries(respHeaders)) : {},
      status: 200,
      statusText: "",
    };

    const observation = buildObservationFromGenerateText(
      t.llm.provider,
      t.llm.model,
      t.llm.mode,
      {
        finishReason: result.finishReason,
        rawFinishReason: result.rawFinishReason,
        text: result.text,
        usage: {
          inputTokens: result.usage.inputTokens ?? 0,
          outputTokens: result.usage.outputTokens ?? 0,
          totalTokens: result.usage.totalTokens,
        },
        warnings: result.warnings?.map((w) =>
          w.type === "unsupported"
            ? `unsupported: ${w.feature}`
            : ((w as Record<string, string>)["message"] ?? JSON.stringify(w)),
        ),
      },
      http,
    );

    const durationMs = Math.round(performance.now() - start);
    const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
      message: "durationMs mismatch",
      path: "durationMs",
      phase: "duration",
    });
    const expectResult = runExpects(observation, expect);
    const failure = expectResult.failure ?? durationResult.failure;

    return {
      detail: null,
      durationMs,
      failure,
      matched: failure === null,
      observation: toPersistedObservation(observation),
      targetId: t.id,
      timestamp,
    };
  }

  private async executeStream(
    t: ResolvedLlmTarget,
    model: ReturnType<typeof createProviderModel>["model"],
    httpMeta: null | { headers: Record<string, string>; status: number; statusText: string },
    expect: ResolvedLlmExpectConfig | undefined,
    ctx: CheckerContext,
    timestamp: string,
    start: number,
  ): Promise<CheckResult> {
    const stream = streamText({
      abortSignal: ctx.signal,
      maxRetries: 0,
      model,
      prompt: t.llm.prompt,
      providerOptions: t.llm.providerOptions,
      ...buildSdkOptions(t.llm),
    });

    const observation = await buildObservationFromStreamText(
      t.llm.provider,
      t.llm.model,
      t.llm.mode,
      stream.fullStream,
      httpMeta,
      start,
    );

    const durationMs = Math.round(performance.now() - start);
    const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
      message: "durationMs mismatch",
      path: "durationMs",
      phase: "duration",
    });
    const expectResult = runExpects(observation, expect);
    const failure = expectResult.failure ?? durationResult.failure;

    return {
      detail: null,
      durationMs,
      failure,
      matched: failure === null,
      observation: toPersistedObservation(observation),
      targetId: t.id,
      timestamp,
    };
  }
}

function buildSdkOptions(config: ResolvedLlmTarget["llm"]): Record<string, unknown> {
  const options: Record<string, unknown> = {};
  const opts = config.options;
  if (opts.maxOutputTokens !== undefined) options["maxOutputTokens"] = opts.maxOutputTokens;
  if (opts.temperature !== undefined) options["temperature"] = opts.temperature;
  if (opts.topP !== undefined) options["topP"] = opts.topP;
  if (opts.topK !== undefined) options["topK"] = opts.topK;
  if (opts.presencePenalty !== undefined) options["presencePenalty"] = opts.presencePenalty;
  if (opts.frequencyPenalty !== undefined) options["frequencyPenalty"] = opts.frequencyPenalty;
  if (opts.stopSequences !== undefined) options["stopSequences"] = opts.stopSequences;
  if (opts.seed !== undefined) options["seed"] = opts.seed;
  return options;
}
