/**
 * Language Model Well-Known Binding
 *
 * Defines the interface for AI model providers.
 * Any MCP that implements this binding can provide AI models and streaming endpoints.
 *
 * This binding includes:
 * - LLM operations (metadata, stream, generate)
 * - Collection bindings for LIST and GET operations (read-only)
 * - Streaming endpoint information is included directly in the model entity schema.
 */

import { z } from "zod";
import { bindingClient, type ToolBinder } from "../core/binder";
import {
  BaseCollectionEntitySchema,
  createCollectionBindings,
} from "./collections";

/**
 * JSON Value Schema
 * Represents any valid JSON value: null, string, number, boolean, object, or array
 */
const JSONValueSchema: z.ZodType<unknown> = z.lazy(() =>
  z.union([
    z.null(),
    z.string(),
    z.number(),
    z.boolean(),
    z.record(z.string(), JSONValueSchema),
    z.array(JSONValueSchema),
  ]),
);

/**
 * Provider Options Schema
 * Additional provider-specific options passed through to the provider
 */
const ProviderOptionsSchema = z
  .record(z.string(), z.record(z.string(), JSONValueSchema))
  .optional()
  .describe(
    "Additional provider-specific options. Outer record keyed by provider name, inner by option key",
  );

/**
 * Text Part Schema
 * Text content part of a prompt
 */
const TextPartSchema = z.object({
  type: z.literal("text"),
  text: z.string().describe("The text content"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Text Output Part Schema
 * Text content generated by the model (uses providerMetadata instead of providerOptions)
 */
const TextOutputPartSchema = z.object({
  type: z.literal("text"),
  text: z.string().describe("The text content"),
  providerMetadata: z
    .any()
    .optional()
    .describe("Additional provider-specific metadata"),
});

/**
 * Data Content Schema
 * File data can be a URL string or a base64 encoded string
 */
const DataContentSchema = z.string().describe("File data as URL string");

/**
 * File Part Schema
 * File content part of a prompt
 */
const FilePartSchema = z.object({
  type: z.literal("file"),
  filename: z.string().optional().describe("Optional filename of the file"),
  data: DataContentSchema,
  mediaType: z
    .string()
    .describe("IANA media type of the file (e.g., image/png, audio/mp3)"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * File Output Part Schema
 * File content generated by the model
 */
const FileOutputPartSchema = z.object({
  type: z.literal("file"),
  mediaType: z
    .string()
    .describe("IANA media type of the file (e.g., image/png, audio/mp3)"),
  data: z.string().describe("Generated file data as base64 encoded string"),
});

/**
 * Reasoning Part Schema
 * Reasoning content part of a prompt
 */
const ReasoningPartSchema = z.object({
  type: z.literal("reasoning"),
  text: z.string().describe("The reasoning text"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Reasoning Output Part Schema
 * Reasoning content generated by the model
 */
const ReasoningOutputPartSchema = z.object({
  type: z.literal("reasoning"),
  text: z.string().describe("The reasoning text"),
  providerMetadata: z
    .any()
    .optional()
    .describe("Additional provider-specific metadata"),
});

/**
 * Tool Call Part Schema
 * Tool call content part of a prompt (usually generated by the AI model)
 */
const ToolCallPartSchema = z.object({
  type: z.literal("tool-call"),
  toolCallId: z
    .string()
    .describe("ID of the tool call, used to match with tool result"),
  toolName: z.string().describe("Name of the tool being called"),
  input: z
    .string()
    .describe(
      "Arguments of the tool call (JSON-serializable object matching tool input schema)",
    ),
  providerExecuted: z
    .boolean()
    .optional()
    .describe("Whether the tool call will be executed by the provider"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Tool Call Output Part Schema
 * Tool call content generated by the model
 */
const ToolCallOutputPartSchema = z.object({
  type: z.literal("tool-call"),
  toolCallId: z.string().describe("ID of the tool call"),
  toolName: z.string().describe("Name of the tool being called"),
  input: z
    .string()
    .describe("Stringified JSON object with the tool call arguments"),
  providerExecuted: z
    .boolean()
    .optional()
    .describe("Whether the tool call will be executed by the provider"),
  providerMetadata: z
    .any()
    .optional()
    .describe("Additional provider-specific metadata"),
});

/**
 * Tool Result Output Schema
 * The output of a tool result.
 * Accepts typed objects (text, json, error-text, error-json, content),
 * execution-denied (from AI SDK tool approval flow), and raw strings
 * (JSON-serialized outputs from mapToolResultOutput).
 */
const ToolResultOutputSchema = z.union([
  z.object({
    type: z.literal("text"),
    value: z.string(),
  }),
  z.object({
    type: z.literal("json"),
    value: JSONValueSchema,
  }),
  z.object({
    type: z.literal("error-text"),
    value: z.string(),
  }),
  z.object({
    type: z.literal("error-json"),
    value: JSONValueSchema,
  }),
  z.object({
    type: z.literal("content"),
    value: z.array(
      z.union([
        z.object({
          type: z.literal("text"),
          text: z.string().describe("Text content"),
        }),
        z.object({
          type: z.literal("media"),
          data: z.string().describe("Base-64 encoded media data"),
          mediaType: z.string().describe("IANA media type"),
        }),
      ]),
    ),
  }),
  z.object({
    type: z.literal("execution-denied"),
    reason: z.string().optional(),
  }),
  z
    .string()
    .describe(
      "Raw or JSON-serialized output (e.g. from AI SDK mapToolResultOutput)",
    ),
]);

/**
 * Tool Result Part Schema
 * Tool result content part of a prompt
 */
const ToolResultPartSchema = z.object({
  type: z.literal("tool-result"),
  toolCallId: z
    .string()
    .describe("ID of the tool call that this result is associated with"),
  toolName: z.string().describe("Name of the tool that generated this result"),
  output: ToolResultOutputSchema.describe("Result of the tool call"),
  result: z.unknown().describe("Unknown result of the tool call"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Tool Result Output Part Schema
 * Tool result content generated by the model (provider-executed tools)
 */
const ToolResultOutputPartSchema = z.object({
  type: z.literal("tool-result"),
  toolCallId: z
    .string()
    .describe("ID of the tool call that this result is associated with"),
  toolName: z.string().describe("Name of the tool that generated this result"),
  result: z.any().describe("Result of the tool call (JSON-serializable)"),
  isError: z
    .boolean()
    .optional()
    .describe("Whether the result is an error or error message"),
  providerExecuted: z
    .boolean()
    .optional()
    .describe("Whether the tool result was generated by the provider"),
  providerMetadata: z
    .any()
    .optional()
    .describe("Additional provider-specific metadata"),
});

/**
 * Source Part Schema
 * Source content generated by the model (references to web or document sources)
 */
const SourcePartSchema = z.union([
  z.object({
    type: z.literal("source"),
    sourceType: z.literal("url"),
    id: z.string().describe("The ID of the source"),
    url: z.string().describe("The URL of the source"),
    title: z.string().optional().describe("The title of the source"),
    providerMetadata: z
      .any()
      .optional()
      .describe("Additional provider-specific metadata"),
  }),
  z.object({
    type: z.literal("source"),
    sourceType: z.literal("document"),
    id: z.string().describe("The ID of the source"),
    mediaType: z
      .string()
      .describe("IANA media type of the document (e.g., application/pdf)"),
    title: z.string().describe("The title of the document"),
    filename: z
      .string()
      .optional()
      .describe("Optional filename of the document"),
    providerMetadata: z
      .any()
      .optional()
      .describe("Additional provider-specific metadata"),
  }),
]);

/**
 * System Message Schema
 */
const SystemMessageSchema = z.object({
  role: z.literal("system"),
  content: z.string().describe("System message content"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * User Message Schema
 */
const UserMessageSchema = z.object({
  role: z.literal("user"),
  content: z
    .array(z.union([TextPartSchema, FilePartSchema]))
    .describe("User message content parts (text or file)"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Assistant Message Schema
 */
const AssistantMessageSchema = z.object({
  role: z.literal("assistant"),
  content: z
    .array(
      z.union([
        TextPartSchema,
        FilePartSchema,
        ReasoningPartSchema,
        ToolCallPartSchema,
        ToolResultPartSchema,
      ]),
    )
    .describe(
      "Assistant message content parts (text, file, reasoning, tool-call, or tool-result)",
    ),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Tool Message Schema
 */
const ToolMessageSchema = z.object({
  role: z.literal("tool"),
  content: z
    .array(ToolResultPartSchema)
    .describe("Tool message content (tool results)"),
  providerOptions: ProviderOptionsSchema,
});

/**
 * Language Model Message Schema
 * A message in a language model prompt
 */
export const LanguageModelMessageSchema = z.union([
  SystemMessageSchema,
  UserMessageSchema,
  AssistantMessageSchema,
  ToolMessageSchema,
]);

/**
 * Language Model Prompt Schema
 * A prompt is a list of messages based on LanguageModelV2Prompt from @ai-sdk/provider
 */
export const LanguageModelPromptSchema = z
  .array(LanguageModelMessageSchema)
  .describe("A list of messages forming the prompt");
/**
 * Language Model Call Options Schema
 * Based on LanguageModelV2CallOptions from @ai-sdk/provider
 */
export const LanguageModelCallOptionsSchema = z.object({
  // Core parameters
  prompt: LanguageModelPromptSchema.describe(
    "A language model prompt is a standardized prompt type (array of messages with roles: system, user, assistant, tool)",
  ),

  // Generation parameters
  maxOutputTokens: z
    .number()
    .optional()
    .describe("Maximum number of tokens to generate"),
  temperature: z
    .number()
    .optional()
    .describe(
      "Temperature setting. The range depends on the provider and model",
    ),
  topP: z.number().optional().describe("Nucleus sampling parameter"),
  topK: z
    .number()
    .optional()
    .describe(
      "Only sample from the top K options for each subsequent token. Used to remove long tail low probability responses",
    ),
  presencePenalty: z
    .number()
    .optional()
    .describe(
      "Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt",
    ),
  frequencyPenalty: z
    .number()
    .optional()
    .describe(
      "Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases",
    ),
  seed: z
    .number()
    .optional()
    .describe(
      "The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results",
    ),

  // Stop sequences
  stopSequences: z
    .array(z.string())
    .optional()
    .describe(
      "Stop sequences. If set, the model will stop generating text when one of the stop sequences is generated",
    ),

  // Response format
  responseFormat: z
    .union([
      z.object({ type: z.literal("text") }),
      z.object({
        type: z.literal("json"),
        schema: z
          .any()
          .optional()
          .describe("JSON schema that the generated output should conform to"),
        name: z
          .string()
          .optional()
          .describe("Name of output that should be generated"),
        description: z
          .string()
          .optional()
          .describe("Description of the output that should be generated"),
      }),
    ])
    .optional()
    .describe(
      "Response format. The output can either be text or JSON. Default is text",
    ),

  // Tools
  tools: z
    .array(z.any())
    .optional()
    .describe("The tools that are available for the model"),
  toolChoice: z
    .any()
    .optional()
    .describe("Specifies how the tool should be selected. Defaults to 'auto'"),

  // Stream options
  includeRawChunks: z
    .boolean()
    .optional()
    .describe(
      "Include raw chunks in the stream. Only applicable for streaming calls",
    ),

  // Abort signal
  abortSignal: z
    .any()
    .optional()
    .describe("Abort signal for cancelling the operation"),

  // Additional options
  // headers: z
  //   .record(z.string(), z.union([z.string(), z.undefined()]))
  //   .optional()
  //   .describe("Additional HTTP headers to be sent with the request"),
  providerOptions: z
    .any()
    .optional()
    .describe("Additional provider-specific options"),
});

/**
 * Language Model Generate Output Schema
 * Based on the return type of LanguageModelV2.doGenerate from @ai-sdk/provider
 */
export const LanguageModelGenerateOutputSchema = z.object({
  // Ordered content that the model has generated
  content: z
    .array(
      z.union([
        TextOutputPartSchema,
        FileOutputPartSchema,
        ReasoningOutputPartSchema,
        ToolCallOutputPartSchema,
        ToolResultOutputPartSchema,
        SourcePartSchema,
      ]),
    )
    .describe(
      "Ordered content that the model has generated (text, tool-calls, reasoning, files, sources)",
    ),

  // Finish reason (required)
  finishReason: z
    .enum([
      "stop",
      "length",
      "content-filter",
      "tool-calls",
      "error",
      "other",
      "unknown",
    ])
    .describe("Reason why generation stopped"),

  // Usage information (required)
  usage: z
    .looseObject({
      inputTokens: z.number().optional(),
      outputTokens: z.number().optional(),
      totalTokens: z.number().optional(),
      reasoningTokens: z.number().optional(),
    })
    .describe("Usage information for the language model call"),

  // Provider metadata
  providerMetadata: z
    .any()
    .optional()
    .describe("Additional provider-specific metadata"),

  // Request information for telemetry and debugging
  request: z
    .object({
      body: z
        .any()
        .optional()
        .describe("Request HTTP body sent to the provider API"),
    })
    .optional()
    .describe("Optional request information for telemetry and debugging"),

  // Response information for telemetry and debugging
  response: z
    .object({
      id: z.string().optional().describe("ID for the generated response"),
      timestamp: z.iso
        .datetime()
        .optional()
        .describe("Timestamp for the start of the generated response"),
      modelId: z
        .string()
        .optional()
        .describe("The ID of the response model that was used"),
      headers: z
        .record(z.string(), z.string())
        .optional()
        .describe("Response headers"),
      body: z.any().optional().describe("Response HTTP body"),
    })
    .optional()
    .describe("Optional response information for telemetry and debugging"),

  // Warnings for the call (required)
  warnings: z
    .array(z.any())
    .describe("Warnings for the call, e.g. unsupported settings"),
});

/**
 * Language Model Stream Output Schema
 * Based on the return type of LanguageModelV2.doStream from @ai-sdk/provider
 */
export const LanguageModelStreamOutputSchema = z.object({
  // Stream of language model output parts
  stream: z.any().describe("ReadableStream of LanguageModelV2StreamPart"),

  // Request information for telemetry and debugging
  request: z
    .object({
      body: z
        .any()
        .optional()
        .describe("Request HTTP body sent to the provider API"),
    })
    .optional()
    .describe("Optional request information for telemetry and debugging"),

  // Response information
  response: z
    .object({
      headers: z
        .record(z.string(), z.string())
        .optional()
        .describe("Response headers"),
    })
    .optional()
    .describe("Optional response data"),
});

export const LanguageModelMetadataSchema = z.object({
  supportedUrls: z
    .record(z.string(), z.array(z.string()))
    .describe("Supported URL patterns by media type for the provider"),
});

/**
 * Simple Model schema for LLM operations
 */
export const ModelSchema = z.object({
  modelId: z.string().describe("The ID of the model"),
  // Model-specific fields
  logo: z.string().nullable(),
  description: z.string().nullable(),
  capabilities: z.array(z.string()),
  limits: z
    .object({
      contextWindow: z.number(),
      maxOutputTokens: z.number(),
    })
    .nullable(),
  costs: z
    .object({
      input: z.number(),
      output: z.number(),
    })
    .nullable(),
  // Provider information
  provider: z
    .enum([
      "openai",
      "anthropic",
      "google",
      "x-ai",
      "deepseek",
      "openai-compatible",
      "openrouter",
    ])
    .nullable(),
});

export const LanguageModelInputSchema = z.object({
  modelId: z.string().describe("The ID of the model"),
  callOptions: LanguageModelCallOptionsSchema,
});

export const ModelCollectionEntitySchema = BaseCollectionEntitySchema.extend({
  // Model-specific fields
  logo: z.string().nullable(),
  description: z.string().nullable(),
  capabilities: z.array(z.string()),
  limits: z
    .object({
      contextWindow: z.number(),
      maxOutputTokens: z.number(),
    })
    .nullable(),
  costs: z
    .object({
      input: z.number(),
      output: z.number(),
    })
    .nullable(),
  // Provider information
  provider: z
    .enum([
      "openai",
      "anthropic",
      "google",
      "xai",
      "deepseek",
      "openai-compatible",
      "openrouter",
    ])
    .nullable(),
});

/**
 * LLM Collection Binding (internal)
 *
 * Collection bindings for language models (read-only).
 * Provides LIST and GET operations for AI models.
 */
const LLM_COLLECTION_BINDING = createCollectionBindings(
  "llm",
  ModelCollectionEntitySchema,
  { readOnly: true },
);

/**
 * Language Model Binding
 *
 * Defines the interface for AI model providers.
 * Any MCP that implements this binding can provide AI models.
 *
 * Required tools:
 * - LLM_METADATA: Get metadata for a specific model
 * - LLM_DO_STREAM: Stream a language model response
 * - LLM_DO_GENERATE: Generate a language model response
 * - COLLECTION_LLM_LIST: List available AI models with their capabilities
 * - COLLECTION_LLM_GET: Get a single model by ID
 *
 * @deprecated Use native AI SDK provider adapters instead. The llm-binding
 * abstraction wraps MCP connections as AI SDK v2 language models, but the
 * AI SDK now expects v3. The decopilot stream path already uses native
 * providers. This binding will be removed in a future release.
 */
export const LANGUAGE_MODEL_BINDING = [
  {
    name: "LLM_METADATA" as const,
    inputSchema: z.object({
      modelId: z.string().describe("The ID of the model"),
    }),
    outputSchema: LanguageModelMetadataSchema,
  },
  {
    name: "LLM_DO_STREAM" as const,
    inputSchema: LanguageModelInputSchema,
  },
  {
    name: "LLM_DO_GENERATE" as const,
    inputSchema: LanguageModelInputSchema,
    outputSchema: LanguageModelGenerateOutputSchema,
  },
  ...LLM_COLLECTION_BINDING,
] satisfies ToolBinder[];

/**
 * @deprecated Use native AI SDK provider adapters instead. See {@link LANGUAGE_MODEL_BINDING}.
 */
export const LanguageModelBinding = bindingClient(LANGUAGE_MODEL_BINDING);
