import {
  AIError,
  createAI,
  type AI,
  type AIConfig,
  type GenerateOptions,
  type Message,
} from "./index.ts";

export interface AIUsage {
  inputTokens?: number;
  outputTokens?: number;
  totalTokens?: number;
  costUsd?: number;
}

export interface AIResult<T = string> {
  value: T;
  provider: string;
  model?: string;
  usage?: AIUsage;
  finishReason?: string;
  toolCalls?: AIToolCall[];
  raw?: unknown;
}

export interface AIProviderCapabilities {
  streaming?: boolean;
  structuredOutput?: boolean;
  tools?: boolean;
  usage?: boolean;
}

export interface AIToolCall {
  id: string;
  name: string;
  arguments: unknown;
}

export interface AITool<TInput = unknown, TOutput = unknown> {
  name: string;
  description?: string;
  validate?: (value: unknown) => value is TInput;
  execute(input: TInput, context: { signal?: AbortSignal }): TOutput | Promise<TOutput>;
}

export interface AIProvider {
  name: string;
  capabilities?: AIProviderCapabilities;
  generate(prompt: string | Message[], options?: GenerateOptions): Promise<AIResult<string>>;
  stream?(
    prompt: string | Message[],
    options?: GenerateOptions,
  ): AsyncGenerator<string, void, unknown>;
}

export interface AIRetryOptions {
  attempts?: number;
  baseDelayMs?: number;
  maxDelayMs?: number;
  retry?: (error: unknown) => boolean;
}

export interface AICircuitBreakerOptions {
  failureThreshold?: number;
  resetAfterMs?: number;
}

export interface AIClientOptions {
  providers: AIProvider[];
  fallback?: boolean;
  retry?: AIRetryOptions;
  circuitBreaker?: AICircuitBreakerOptions;
  /** Receives metadata only; prompts, provider raw responses, and credentials are never emitted. */
  onAttempt?: (event: AIAttemptEvent) => void | Promise<void>;
}

export interface AIAttemptEvent {
  provider: string;
  attempt: number;
  outcome: "start" | "success" | "error" | "circuit-open";
  durationMs?: number;
  error?: { name: string; type?: string; status?: number; message: string };
  usage?: AIUsage;
}

export interface AIClient {
  generate(
    prompt: string | Message[],
    options?: GenerateOptions & { provider?: string },
  ): Promise<AIResult<string>>;
  generateObject<T>(
    prompt: string | Message[],
    options?: GenerateOptions & { provider?: string; validate?: (value: unknown) => value is T },
  ): Promise<AIResult<T>>;
  executeTools(
    result: AIResult,
    tools: AITool[],
    options?: { signal?: AbortSignal },
  ): Promise<Array<{ call: AIToolCall; value: unknown }>>;
  stream(
    prompt: string | Message[],
    options?: GenerateOptions & { provider?: string },
  ): AsyncGenerator<string, void, unknown>;
  capabilities(provider?: string): Record<string, AIProviderCapabilities>;
}

export function anthropicProvider(config: AIConfig = {}): AIProvider {
  const client = createAI(config);
  return {
    name: "anthropic",
    capabilities: { streaming: true, structuredOutput: true },
    async generate(prompt, options) {
      return {
        value: await client.generate(prompt, options),
        provider: "anthropic",
        model: options?.model ?? config.model,
      };
    },
    stream: (prompt, options) => client.stream(prompt, options),
  };
}

export function aiProvider(
  name: string,
  client: AI,
  capabilities: AIProviderCapabilities = {},
): AIProvider {
  return {
    name,
    capabilities: { streaming: true, ...capabilities },
    async generate(prompt, options) {
      return {
        value: await client.generate(prompt, options),
        provider: name,
        model: options?.model,
      };
    },
    stream: (prompt, options) => client.stream(prompt, options),
  };
}

export interface DeterministicAIProviderOptions {
  name?: string;
  responses?: Array<string | AIResult<string>>;
  handler?: (
    prompt: string | Message[],
    options?: GenerateOptions,
  ) => string | AIResult<string> | Promise<string | AIResult<string>>;
}

/** Offline provider for examples and tests. Responses are consumed in order. */
export function deterministicAIProvider(options: DeterministicAIProviderOptions = {}): AIProvider {
  let index = 0;
  const name = options.name ?? "deterministic";
  return {
    name,
    capabilities: { streaming: true, structuredOutput: true, tools: true, usage: true },
    async generate(prompt, callOptions) {
      const selected = options.handler
        ? await options.handler(prompt, callOptions)
        : options.responses?.[index++];
      if (selected === undefined)
        throw new AIError("No deterministic response configured", 0, "provider_error");
      return typeof selected === "string"
        ? { value: selected, provider: name }
        : { ...selected, provider: name };
    },
    async *stream(prompt, callOptions) {
      yield (await this.generate(prompt, callOptions)).value;
    },
  };
}

function jsonText(value: string): string {
  const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(value);
  return (fenced?.[1] ?? value).trim();
}

function safeError(error: unknown): AIAttemptEvent["error"] {
  if (error instanceof AIError)
    return {
      name: error.name,
      type: error.type,
      status: error.status,
      message: error.message.slice(0, 300),
    };
  if (error instanceof Error) return { name: error.name, message: error.message.slice(0, 300) };
  return { name: "Error", message: "Unknown provider error" };
}

function defaultRetry(error: unknown): boolean {
  return (
    error instanceof AIError &&
    (error.status === 408 || error.status === 429 || error.status >= 500)
  );
}

function abortError(): Error {
  return new DOMException("The operation was aborted", "AbortError");
}

async function delay(ms: number, signal?: AbortSignal): Promise<void> {
  if (signal?.aborted) throw signal.reason ?? abortError();
  await new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    signal?.addEventListener(
      "abort",
      () => {
        clearTimeout(timer);
        reject(signal.reason ?? abortError());
      },
      { once: true },
    );
  });
}

export function createAIClient(options: AIClientOptions): AIClient {
  if (!options.providers.length) throw new Error("WRN-AI-NO-PROVIDERS");
  const duplicate = options.providers.find(
    (provider, index) =>
      options.providers.findIndex((other) => other.name === provider.name) !== index,
  );
  if (duplicate) throw new Error(`WRN-AI-DUPLICATE-PROVIDER:${duplicate.name}`);
  const circuits = new Map<string, { failures: number; openedAt?: number }>();
  const select = (name?: string) =>
    name ? options.providers.filter((provider) => provider.name === name) : options.providers;
  const attempts = Math.max(1, options.retry?.attempts ?? 1);
  const failureThreshold = Math.max(1, options.circuitBreaker?.failureThreshold ?? 5);
  const resetAfterMs = Math.max(0, options.circuitBreaker?.resetAfterMs ?? 30_000);

  const generate: AIClient["generate"] = async (prompt, callOptions = {}) => {
    const providers = select(callOptions.provider);
    if (!providers.length)
      throw new AIError(`Unknown AI provider: ${callOptions.provider}`, 0, "provider_error");
    let last: unknown;
    for (const provider of providers) {
      const circuit = circuits.get(provider.name) ?? { failures: 0 };
      if (circuit.openedAt !== undefined && Date.now() - circuit.openedAt < resetAfterMs) {
        await options.onAttempt?.({ provider: provider.name, attempt: 0, outcome: "circuit-open" });
        last = new AIError(`Circuit is open for provider: ${provider.name}`, 0, "circuit_open");
        if (callOptions.provider) throw last;
        continue;
      }
      if (circuit.openedAt !== undefined) {
        circuit.failures = 0;
        circuit.openedAt = undefined;
      }
      for (let attempt = 1; attempt <= attempts; attempt++) {
        if (callOptions.signal?.aborted) throw callOptions.signal.reason ?? abortError();
        const started = Date.now();
        await options.onAttempt?.({ provider: provider.name, attempt, outcome: "start" });
        try {
          const result = await provider.generate(prompt, callOptions);
          circuits.set(provider.name, { failures: 0 });
          await options.onAttempt?.({
            provider: provider.name,
            attempt,
            outcome: "success",
            durationMs: Date.now() - started,
            usage: result.usage,
          });
          return result;
        } catch (error) {
          last = error;
          circuit.failures++;
          if (circuit.failures >= failureThreshold) circuit.openedAt = Date.now();
          circuits.set(provider.name, circuit);
          await options.onAttempt?.({
            provider: provider.name,
            attempt,
            outcome: "error",
            durationMs: Date.now() - started,
            error: safeError(error),
          });
          const retryable = (options.retry?.retry ?? defaultRetry)(error);
          if (!retryable || attempt === attempts || callOptions.signal?.aborted) break;
          const backoff = Math.min(
            options.retry?.maxDelayMs ?? 5_000,
            (options.retry?.baseDelayMs ?? 100) * 2 ** (attempt - 1),
          );
          await delay(backoff, callOptions.signal);
        }
      }
      if (options.fallback === false || callOptions.provider) throw last;
    }
    throw last;
  };

  return {
    generate,
    async generateObject<T>(
      prompt: string | Message[],
      callOptions: GenerateOptions & {
        provider?: string;
        validate?: (value: unknown) => value is T;
      } = {},
    ) {
      const candidates = select(callOptions.provider);
      if (
        candidates.length &&
        candidates.every((provider) => provider.capabilities?.structuredOutput === false)
      )
        throw new AIError(
          "Selected provider does not support structured output",
          0,
          "capability_error",
        );
      const result = await generate(prompt, callOptions);
      let value: unknown;
      try {
        value = JSON.parse(jsonText(result.value));
      } catch {
        throw new AIError("AI response was not valid JSON", 0, "structured_output_error");
      }
      if (callOptions.validate && !callOptions.validate(value))
        throw new AIError(
          "AI response failed structured output validation",
          0,
          "structured_output_error",
        );
      return { ...result, value: value as T };
    },
    async executeTools(result, tools, toolOptions = {}) {
      const registry = new Map(tools.map((tool) => [tool.name, tool]));
      const output: Array<{ call: AIToolCall; value: unknown }> = [];
      for (const call of result.toolCalls ?? []) {
        if (toolOptions.signal?.aborted) throw toolOptions.signal.reason ?? abortError();
        const tool = registry.get(call.name);
        if (!tool) throw new AIError(`Unknown AI tool: ${call.name}`, 0, "tool_error");
        if (tool.validate && !tool.validate(call.arguments))
          throw new AIError(`Invalid arguments for AI tool: ${call.name}`, 0, "tool_error");
        output.push({
          call,
          value: await tool.execute(call.arguments, { signal: toolOptions.signal }),
        });
      }
      return output;
    },
    async *stream(prompt, callOptions = {}) {
      const providers = select(callOptions.provider);
      let last: unknown;
      for (const provider of providers) {
        if (!provider.stream || provider.capabilities?.streaming === false) continue;
        try {
          yield* provider.stream(prompt, callOptions);
          return;
        } catch (error) {
          last = error;
          if (options.fallback === false || callOptions.provider) throw error;
        }
      }
      if (last) throw last;
      const result = await generate(prompt, callOptions);
      yield result.value;
    },
    capabilities(provider) {
      return Object.fromEntries(
        select(provider).map((item) => [item.name, { ...item.capabilities }]),
      );
    },
  };
}
