import type {
  LanguageModelV4,
  LanguageModelV4CallOptions,
  LanguageModelV4Content,
  LanguageModelV4GenerateResult,
  LanguageModelV4Reasoning,
  LanguageModelV4Source,
  LanguageModelV4StreamPart,
  LanguageModelV4StreamResult,
  LanguageModelV4Text,
  LanguageModelV4ToolCall,
  LanguageModelV4ToolResult,
  SharedV4Warning,
} from '@ai-sdk/provider';
import {
  resolve,
  type Resolvable,
  lazySchema,
  zodSchema,
  type InferSchema,
  combineHeaders,
  createEventSourceResponseHandler,
  createJsonResponseHandler,
  createToolNameMapping,
  generateId,
  injectJsonInstructionIntoMessages,
  isCustomReasoning,
  mapReasoningToProviderEffort,
  parseProviderOptions,
  postJsonToApi,
  serializeModelOptions,
  StreamingToolCallTracker,
  WORKFLOW_SERIALIZE,
  WORKFLOW_DESERIALIZE,
  type FetchFunction,
  type ParseResult,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import {
  convertMistralUsage,
  mistralUsageSchema,
  type MistralUsage,
} from './convert-mistral-usage';
import { convertToMistralConversationInputs } from './convert-to-mistral-conversation-inputs';
import {
  mistralLanguageModelConversationOptions,
  type MistralConversationModelId,
} from './mistral-conversation-language-model-options';
import { mistralFailedResponseHandler } from './mistral-error';
import { getMistralLanguageModelEntry } from './mistral-model-catalog';
import {
  mistralProviderToolNames,
  prepareConversationTools,
} from './mistral-conversation-prepare-tools';

type MistralConversationConfig = {
  provider: string;
  baseURL: string;
  headers?: Resolvable<Record<string, string | undefined>>;
  fetch?: FetchFunction;
  generateId?: () => string;
};

export class MistralConversationLanguageModel implements LanguageModelV4 {
  readonly specificationVersion = 'v4';

  readonly modelId: MistralConversationModelId;

  private readonly config: MistralConversationConfig;
  private readonly generateId: () => string;

  static [WORKFLOW_SERIALIZE](model: MistralConversationLanguageModel) {
    return serializeModelOptions({
      modelId: model.modelId,
      config: model.config,
    });
  }

  static [WORKFLOW_DESERIALIZE](options: {
    modelId: MistralConversationModelId;
    config: MistralConversationConfig;
  }) {
    return new MistralConversationLanguageModel(
      options.modelId,
      options.config,
    );
  }

  constructor(
    modelId: MistralConversationModelId,
    config: MistralConversationConfig,
  ) {
    this.modelId = modelId;
    this.config = config;
    this.generateId = config.generateId ?? generateId;
  }

  get provider(): string {
    return this.config.provider;
  }

  readonly supportedUrls: Record<string, RegExp[]> = {
    'application/pdf': [/^https:\/\/.*$/],
  };

  private async getArgs({
    prompt,
    maxOutputTokens,
    temperature,
    topP,
    topK,
    frequencyPenalty,
    presencePenalty,
    reasoning,
    stopSequences,
    responseFormat,
    seed,
    providerOptions,
    tools,
    toolChoice,
  }: LanguageModelV4CallOptions) {
    const warnings: SharedV4Warning[] = [];

    const options =
      (await parseProviderOptions({
        provider: 'mistral',
        providerOptions,
        schema: mistralLanguageModelConversationOptions,
      })) ?? {};

    if (topK != null) {
      warnings.push({ type: 'unsupported', feature: 'topK' });
    }

    const { reasoning: reasoningCapabilities } = getMistralLanguageModelEntry(
      this.modelId,
    ).capabilities.language.endpoints.conversation;

    let resolvedReasoningEffort: string | undefined;
    if (reasoningCapabilities.support) {
      resolvedReasoningEffort = mapReasoningToProviderEffort({
        reasoning,
        providerOptionsEffort: options.reasoningEffort,
        effortMap: Object.fromEntries(
          reasoningCapabilities.efforts.map(effort => [effort, effort]),
        ),
        warnings,
      });
    } else if (isCustomReasoning(reasoning)) {
      warnings.push({
        type: 'unsupported',
        feature: 'reasoning',
        details: 'This model does not support reasoning configuration.',
      });
    }

    const structuredOutputs = options.structuredOutputs ?? true;
    const strictJsonSchema = options.strictJsonSchema ?? false;

    // For Mistral we need to instruct the model when using JSON Object mode.
    // https://docs.mistral.ai/capabilities/structured-output/structured_output_overview/
    if (
      responseFormat?.type === 'json' &&
      (!structuredOutputs || responseFormat.schema == null)
    ) {
      prompt = injectJsonInstructionIntoMessages({
        messages: prompt,
        schema: responseFormat.schema,
      });
    }

    const baseArgs = {
      // standardized settings:
      max_tokens: maxOutputTokens,
      temperature,
      top_p: topP,
      ...(frequencyPenalty != null
        ? { frequency_penalty: frequencyPenalty }
        : {}),
      ...(presencePenalty != null ? { presence_penalty: presencePenalty } : {}),
      stop: stopSequences,
      random_seed: seed,
      reasoning_effort: resolvedReasoningEffort,

      // response format:
      response_format:
        responseFormat?.type === 'json'
          ? structuredOutputs && responseFormat?.schema != null
            ? {
                type: 'json_schema',
                json_schema: {
                  schema: responseFormat.schema,
                  strict: strictJsonSchema,
                  name: responseFormat.name ?? 'response',
                  description: responseFormat.description,
                },
              }
            : { type: 'json_object' }
          : undefined,
    };

    const {
      tools: mistralTools,
      toolChoice: mistralToolChoice,
      toolWarnings,
    } = prepareConversationTools({ tools, toolChoice });

    for (const [feature, value] of Object.entries({
      safePrompt: options.safePrompt,
      documentImageLimit: options.documentImageLimit,
      documentPageLimit: options.documentPageLimit,
      promptCacheKey: options.promptCacheKey,
      parallelToolCalls: options.parallelToolCalls,
    })) {
      if (value != null) {
        warnings.push({
          type: 'unsupported',
          feature,
          details: 'Not supported by the Mistral Conversations API.',
        });
      }
    }

    return {
      args: {
        model: this.modelId,
        store: false,
        ...convertToMistralConversationInputs(
          prompt,
          createToolNameMapping({
            tools,
            providerToolNames: mistralProviderToolNames,
          }),
        ),
        tools: mistralTools,
        completion_args: { ...baseArgs, tool_choice: mistralToolChoice },
      },
      warnings: [...warnings, ...toolWarnings],
    };
  }

  async doGenerate(
    options: LanguageModelV4CallOptions,
  ): Promise<LanguageModelV4GenerateResult> {
    const { args: body, warnings } = await this.getArgs(options);

    const {
      value: response,
      rawValue: rawResponse,
      responseHeaders,
    } = await postJsonToApi({
      url: `${this.config.baseURL}/conversations`,
      headers: combineHeaders(
        await resolve(this.config.headers),
        options.headers,
      ),
      body,
      failedResponseHandler: mistralFailedResponseHandler,
      successfulResponseHandler: createJsonResponseHandler(
        mistralConversationResponseSchema,
      ),
      abortSignal: options.abortSignal,
      fetch: this.config.fetch,
    });

    const converter = createContentConverter({
      tools: options.tools,
      generateId: this.generateId,
    });
    const content: LanguageModelV4Content[] = [];
    let hasFunctionCalls = false;
    let modelId: string | undefined;

    for (const output of response.outputs) {
      switch (output.type) {
        case 'tool.execution':
          content.push(...converter.convertTool(output));
          break;
        case 'function.call':
          hasFunctionCalls = true;
          content.push({
            type: 'tool-call',
            toolCallId: output.tool_call_id,
            toolName: output.name,
            input:
              typeof output.arguments === 'string'
                ? output.arguments
                : JSON.stringify(output.arguments),
          });
          break;
        case 'message.output':
          modelId = output.model ?? modelId;
          for (const chunk of typeof output.content === 'string'
            ? [{ type: 'text' as const, text: output.content }]
            : output.content) {
            content.push(...converter.convertChunk(chunk));
          }
          break;
      }
    }

    return {
      content,
      finishReason: {
        unified: hasFunctionCalls ? 'tool-calls' : 'stop',
        raw: undefined,
      },
      usage: convertMistralUsage(response.usage),
      request: { body },
      response: {
        id: response.conversation_id,
        modelId,
        headers: responseHeaders,
        body: rawResponse,
      },
      warnings,
    };
  }

  async doStream(
    options: LanguageModelV4CallOptions,
  ): Promise<LanguageModelV4StreamResult> {
    const { args, warnings } = await this.getArgs(options);
    const body = { ...args, stream: true };

    const { value: response, responseHeaders } = await postJsonToApi({
      url: `${this.config.baseURL}/conversations`,
      headers: combineHeaders(
        await resolve(this.config.headers),
        options.headers,
      ),
      body,
      failedResponseHandler: mistralFailedResponseHandler,
      successfulResponseHandler: createEventSourceResponseHandler(
        mistralConversationChunkSchema,
      ),
      abortSignal: options.abortSignal,
      fetch: this.config.fetch,
    });

    const converter = createContentConverter({
      tools: options.tools,
      generateId: this.generateId,
    });
    const executions = new Map<
      string,
      { name: string; arguments: string; function?: string | null }
    >();
    let toolCallTracker: StreamingToolCallTracker;
    let active: { id: string; type: 'text' | 'reasoning' } | undefined;
    let usage: MistralUsage | undefined;
    let hasFunctionCalls = false;
    let hasError = false;
    let isDone = false;
    let hasModelId = false;
    const closeContent = (
      controller: TransformStreamDefaultController<LanguageModelV4StreamPart>,
    ) => {
      if (active != null) {
        controller.enqueue({
          type: active.type === 'text' ? 'text-end' : 'reasoning-end',
          id: active.id,
        });
        active = undefined;
      }
    };

    const generateId = this.generateId;

    return {
      stream: response.pipeThrough(
        new TransformStream<
          ParseResult<InferSchema<typeof mistralConversationChunkSchema>>,
          LanguageModelV4StreamPart
        >({
          start(controller) {
            toolCallTracker = new StreamingToolCallTracker(controller, {
              generateId,
            });
            controller.enqueue({ type: 'stream-start', warnings });
          },

          transform(chunk, controller) {
            if (options.includeRawChunks) {
              controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
            }

            if (!chunk.success) {
              hasError = true;
              controller.enqueue({ type: 'error', error: chunk.error });
              return;
            }
            const event = chunk.value;
            switch (event.type) {
              case 'conversation.response.started':
                controller.enqueue({
                  type: 'response-metadata',
                  id: event.conversation_id,
                  ...(event.created_at != null
                    ? { timestamp: new Date(event.created_at) }
                    : {}),
                });
                break;
              case 'conversation.response.done':
                usage = event.usage ?? undefined;
                isDone = true;
                break;
              case 'conversation.response.error':
                hasError = true;
                controller.enqueue({ type: 'error', error: event });
                break;
              case 'message.output.delta': {
                if (!hasModelId && event.model != null) {
                  hasModelId = true;
                  controller.enqueue({
                    type: 'response-metadata',
                    modelId: event.model,
                  });
                }
                for (const part of converter.convertChunk(
                  typeof event.content === 'string'
                    ? { type: 'text', text: event.content }
                    : event.content,
                )) {
                  if (part.type === 'text' || part.type === 'reasoning') {
                    const id = `${event.id}-${event.content_index ?? 0}`;
                    if (active?.id !== id || active.type !== part.type) {
                      closeContent(controller);
                      active = { id, type: part.type };
                      controller.enqueue({
                        type:
                          part.type === 'text'
                            ? 'text-start'
                            : 'reasoning-start',
                        id,
                      });
                    }
                    controller.enqueue({
                      type:
                        part.type === 'text' ? 'text-delta' : 'reasoning-delta',
                      id,
                      delta: part.text,
                    });
                  } else if (part.type === 'source') {
                    controller.enqueue(part);
                  }
                }
                break;
              }
              case 'function.call.delta':
                closeContent(controller);
                hasFunctionCalls = true;
                toolCallTracker.processDelta({
                  id: event.tool_call_id,
                  index: event.output_index,
                  function: { name: event.name, arguments: event.arguments },
                });
                break;
              case 'tool.execution.started':
              case 'tool.execution.delta': {
                closeContent(controller);
                const execution = executions.get(event.id);
                if (execution == null) {
                  executions.set(event.id, {
                    name: event.name,
                    arguments: event.arguments,
                    function: event.function,
                  });
                  controller.enqueue({
                    type: 'tool-input-start',
                    id: event.id,
                    toolName: converter.toolNameMapping.toCustomToolName(
                      event.name,
                    ),
                    providerExecuted: true,
                  });
                } else {
                  execution.arguments += event.arguments;
                }
                break;
              }
              case 'tool.execution.done': {
                const execution = executions.get(event.id);
                if (execution == null) {
                  hasError = true;
                  controller.enqueue({
                    type: 'error',
                    error: new Error(
                      `Missing tool execution start for ${event.id}`,
                    ),
                  });
                  break;
                }
                controller.enqueue({
                  type: 'tool-input-delta',
                  id: event.id,
                  delta: JSON.stringify({ arguments: execution.arguments }),
                });
                controller.enqueue({ type: 'tool-input-end', id: event.id });
                for (const part of converter.convertTool({
                  type: 'tool.execution',
                  id: event.id,
                  name: execution.name,
                  arguments: execution.arguments,
                  function: event.function ?? execution.function,
                  info: event.info,
                })) {
                  controller.enqueue(part);
                }
                executions.delete(event.id);
                break;
              }
            }
          },

          flush(controller) {
            closeContent(controller);
            toolCallTracker.flush();
            controller.enqueue({
              type: 'finish',
              finishReason: {
                unified:
                  hasError || !isDone || executions.size > 0
                    ? 'error'
                    : hasFunctionCalls
                      ? 'tool-calls'
                      : 'stop',
                raw: undefined,
              },
              usage: convertMistralUsage(usage),
            });
          },
        }),
      ),
      request: { body },
      response: { headers: responseHeaders },
    };
  }
}

function createContentConverter({
  tools,
  generateId,
}: {
  tools: LanguageModelV4CallOptions['tools'];
  generateId: () => string;
}) {
  const toolNameMapping = createToolNameMapping({
    tools,
    providerToolNames: mistralProviderToolNames,
  });
  const sourceUrls = new Set<string>();

  return {
    toolNameMapping,
    convertChunk(
      chunk: z.infer<ReturnType<typeof mistralConversationContentSchema>>,
    ): Array<
      LanguageModelV4Text | LanguageModelV4Reasoning | LanguageModelV4Source
    > {
      switch (chunk.type) {
        case 'text':
          return chunk.text.length > 0
            ? [{ type: 'text', text: chunk.text }]
            : [];
        case 'thinking': {
          const text = chunk.thinking.map(part => part.text).join('');
          return text.length > 0 ? [{ type: 'reasoning', text }] : [];
        }
        case 'tool_reference':
          if (chunk.url == null || sourceUrls.has(chunk.url)) {
            return [];
          }
          const id = generateId();
          sourceUrls.add(chunk.url);
          return [
            {
              type: 'source',
              sourceType: 'url',
              id,
              url: chunk.url,
              title: chunk.title ?? chunk.url,
            },
          ];
      }
    },
    convertTool(
      output: z.infer<
        ReturnType<typeof mistralConversationToolExecutionSchema>
      >,
    ): [LanguageModelV4ToolCall, LanguageModelV4ToolResult] {
      const toolName = toolNameMapping.toCustomToolName(output.name);
      return [
        {
          type: 'tool-call',
          toolCallId: output.id,
          toolName,
          input: JSON.stringify({ arguments: output.arguments }),
          providerExecuted: true,
          providerMetadata: {
            mistral: {
              type: 'tool.execution',
              name: output.name,
              ...(output.function != null ? { function: output.function } : {}),
            },
          },
        },
        {
          type: 'tool-result',
          toolCallId: output.id,
          toolName,
          result: { ...(output.info != null ? { info: output.info } : {}) },
        },
      ];
    },
  };
}

const mistralConversationContentSchema = () =>
  z.discriminatedUnion('type', [
    z.object({ type: z.literal('text'), text: z.string() }),
    z.object({
      type: z.literal('thinking'),
      thinking: z.array(
        z.object({ type: z.literal('text'), text: z.string() }),
      ),
    }),
    z.object({
      type: z.literal('tool_reference'),
      tool: z.string().nullish(),
      title: z.string().nullish(),
      url: z.string().nullish(),
    }),
  ]);
const mistralConversationMessageContentSchema = () =>
  z.union([z.string(), z.array(mistralConversationContentSchema())]);
const mistralConversationToolExecutionSchema = () =>
  z.object({
    type: z.literal('tool.execution'),
    id: z.string(),
    name: z.string(),
    arguments: z.string(),
    function: z.string().nullish(),
    info: z.record(z.string(), z.json()).nullish(),
  });
const mistralConversationFunctionCallSchema = () =>
  z.object({
    type: z.literal('function.call'),
    tool_call_id: z.string(),
    name: z.string(),
    arguments: z.union([z.string(), z.record(z.string(), z.json())]),
  });
const mistralConversationUsageSchema = () =>
  mistralUsageSchema.extend({
    prompt_tokens: z.number().default(0),
    completion_tokens: z.number().default(0),
    total_tokens: z.number().default(0),
  });
const mistralConversationResponseSchema = lazySchema(() =>
  zodSchema(
    z.object({
      conversation_id: z.string(),
      outputs: z.array(
        z.discriminatedUnion('type', [
          mistralConversationToolExecutionSchema(),
          mistralConversationFunctionCallSchema(),
          z.object({
            type: z.literal('message.output'),
            content: mistralConversationMessageContentSchema(),
            model: z.string().nullish(),
          }),
        ]),
      ),
      usage: mistralConversationUsageSchema().nullish(),
    }),
  ),
);
const mistralConversationChunkSchema = lazySchema(() =>
  zodSchema(
    z.discriminatedUnion('type', [
      z.object({
        type: z.literal('conversation.response.started'),
        conversation_id: z.string(),
        created_at: z.string().nullish(),
      }),
      z.object({
        type: z.literal('conversation.response.done'),
        usage: mistralConversationUsageSchema().nullish(),
      }),
      z.object({
        type: z.literal('conversation.response.error'),
        message: z.string(),
        code: z.number(),
      }),
      z.object({
        type: z.literal('message.output.delta'),
        id: z.string(),
        content_index: z.number().nullish(),
        content: z.union([z.string(), mistralConversationContentSchema()]),
        model: z.string().nullish(),
      }),
      z.object({
        type: z.literal('function.call.delta'),
        tool_call_id: z.string(),
        name: z.string(),
        arguments: z.string(),
        output_index: z.number().nullish(),
      }),
      z.object({
        type: z.enum(['tool.execution.started', 'tool.execution.delta']),
        id: z.string(),
        name: z.string(),
        arguments: z.string(),
        function: z.string().nullish(),
      }),
      z.object({
        type: z.literal('tool.execution.done'),
        id: z.string(),
        name: z.string(),
        function: z.string().nullish(),
        info: z.record(z.string(), z.json()).nullish(),
      }),
    ]),
  ),
);
