import {
  UnsupportedFunctionalityError,
  type Experimental_BatchV4 as BatchV4,
  type Experimental_BatchV4ItemResult as BatchV4ItemResult,
  type LanguageModelV4GenerateResult,
  type LanguageModelV4ToolCall,
  type ImageModelV4Result,
  type ProviderV4,
} from '@ai-sdk/provider';
import { gateway } from '@ai-sdk/gateway';
import {
  detectMediaType,
  type ToolSet,
  withUserAgentSuffix,
} from '@ai-sdk/provider-utils';
import { InvalidArgumentError } from '../error/invalid-argument-error';
import { convertLanguageModelContent } from '../generate-text/convert-language-model-content';
import {
  getImageProviderMetadata,
  normalizePrompt as normalizeImagePrompt,
} from '../generate-image/generate-image';
import { DefaultGeneratedFile } from '../generate-text/generated-file';
import { parseToolCall } from '../generate-text/parse-tool-call';
import { prepareToolChoice } from '../prompt/prepare-tool-choice';
import { prepareTools } from '../prompt/prepare-tools';
import { logWarnings } from '../logger/log-warnings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { prepareLanguageModelCallOptions } from '../prompt/prepare-language-model-call-options';
import { getTotalTimeoutMs } from '../prompt/request-options';
import { standardizePrompt } from '../prompt/standardize-prompt';
import { wrapGatewayError } from '../prompt/wrap-gateway-error';
import { asLanguageModelUsage } from '../types/usage';
import { asAsyncIterableStream } from '../util/async-iterable-stream';
import { mergeAbortSignals } from '../util/merge-abort-signals';
import { isDeepEqualData } from '../util/is-deep-equal-data';
import { prepareRetries } from '../util/prepare-retries';
import { VERSION } from '../version';
import { asProviderV4 } from '../model/as-provider-v4';
import type {
  BatchItemResult,
  BatchProvider,
  BatchReference,
  BatchStatus,
  CancelBatchOptions,
  CancelBatchResult,
  GetBatchResultsOptions,
  GetBatchStatusOptions,
  ListBatchesOptions,
  ListBatchesResult,
  StartBatchOptions,
  StartBatchResult,
  TextBatchGenerationResult,
  ImageBatchGenerationResult,
} from './batch-types';

/**
 * Requests cancellation of a batch.
 */
export async function cancelBatch({
  provider,
  batch,
  providerOptions,
  abortSignal,
  headers,
  timeout,
}: CancelBatchOptions): Promise<CancelBatchResult> {
  const batchApi = resolveBatchApi(provider);
  validateBatchReference({ batchApi, batch });

  if (batchApi.doCancelBatch == null) {
    throw new UnsupportedFunctionalityError({
      functionality: 'batch cancellation',
      message: 'The provider does not support batch cancellation.',
    });
  }

  const operationAbortSignal = mergeAbortSignals(
    abortSignal,
    getTotalTimeoutMs(timeout),
  );

  try {
    return await batchApi.doCancelBatch({
      batchId: batch.id,
      providerOptions,
      abortSignal: operationAbortSignal,
      headers: withUserAgentSuffix(headers ?? {}, `ai/${VERSION}`),
    });
  } catch (error) {
    throw wrapGatewayError(error);
  }
}

/**
 * Lists a page of batches.
 */
export async function listBatches({
  provider,
  providerOptions,
  limit,
  cursor,
  maxRetries,
  abortSignal,
  headers,
  timeout,
}: ListBatchesOptions = {}): Promise<ListBatchesResult> {
  const batchApi = resolveBatchApi(provider);

  const doListBatches = batchApi.doListBatches?.bind(batchApi);
  if (doListBatches == null) {
    throw new UnsupportedFunctionalityError({
      functionality: 'batch listing',
      message: 'The provider does not support listing batches.',
    });
  }

  const operationAbortSignal = mergeAbortSignals(
    abortSignal,
    getTotalTimeoutMs(timeout),
  );
  const { retry } = prepareRetries({
    maxRetries,
    abortSignal: operationAbortSignal,
  });

  try {
    const { batches, nextCursor, providerMetadata } = await retry(() =>
      doListBatches({
        providerOptions,
        abortSignal: operationAbortSignal,
        headers: withUserAgentSuffix(headers ?? {}, `ai/${VERSION}`),
        ...(limit != null && { limit }),
        ...(cursor != null && { cursor }),
      }),
    );

    return {
      batches: batches.map(({ batchId, ...status }) => ({
        version: 2,
        id: batchId,
        provider: batchApi.provider,
        ...status,
      })),
      ...(nextCursor != null && { nextCursor }),
      ...(providerMetadata != null && { providerMetadata }),
    };
  } catch (error) {
    throw wrapGatewayError(error);
  }
}

/**
 * Starts a batch.
 */
export async function startBatch<
  TOOLS extends ToolSet,
  PROVIDER extends BatchProvider,
>({
  provider,
  requests,
  providerOptions,
  webhookUrl,
  abortSignal,
  headers,
  timeout,
}: StartBatchOptions<TOOLS, PROVIDER>): Promise<StartBatchResult> {
  validateRequests(requests);

  const batchApi = resolveBatchApi(provider);
  const operationAbortSignal = mergeAbortSignals(
    abortSignal,
    getTotalTimeoutMs(timeout),
  );
  const supportedUrls = await batchApi.supportedUrls;
  operationAbortSignal?.throwIfAborted();
  const normalizedRequests = [];
  const toolsByName = new Map<string, unknown>();

  for (const request of requests) {
    const requestType = request.type;
    switch (requestType) {
      case 'text': {
        const standardizedPrompt = await standardizePrompt(request);
        const preparedTools = await prepareTools({
          tools: request.tools,
          toolOrder: request.toolOrder,
          toolsContext: request.toolsContext,
        });
        validateCompatibleTools({
          requestId: request.id,
          tools: preparedTools,
          toolsByName,
        });

        normalizedRequests.push({
          id: request.id,
          type: request.type,
          modelId: request.model,
          options: {
            ...prepareLanguageModelCallOptions(request),
            prompt: await convertToLanguageModelPrompt({
              prompt: standardizedPrompt,
              supportedUrls,
              download: undefined,
              abortSignal: operationAbortSignal,
              provider: batchApi.provider.split('.')[0],
            }),
            tools: preparedTools,
            toolChoice: prepareToolChoice({ toolChoice: request.toolChoice }),
            providerOptions: request.providerOptions,
          },
        });
        break;
      }
      case 'image': {
        const { prompt, files, mask } = normalizeImagePrompt(request.prompt);
        normalizedRequests.push({
          id: request.id,
          type: request.type,
          modelId: request.model,
          options: {
            prompt,
            n: request.n ?? 1,
            size: request.size,
            aspectRatio: request.aspectRatio,
            seed: request.seed,
            files,
            mask,
            providerOptions: request.providerOptions ?? {},
          },
        });
        break;
      }
      default: {
        const _exhaustiveCheck: never = requestType;
        throw new InvalidArgumentError({
          parameter: 'requests',
          value: _exhaustiveCheck,
          message: `Unsupported batch request type "${_exhaustiveCheck}".`,
        });
      }
    }
    operationAbortSignal?.throwIfAborted();
  }

  const headersWithUserAgent = withUserAgentSuffix(
    headers ?? {},
    `ai/${VERSION}`,
  );
  try {
    const result = await batchApi.doStartBatch({
      requests: normalizedRequests,
      providerOptions,
      abortSignal: operationAbortSignal,
      headers: headersWithUserAgent,
      ...(webhookUrl != null && { webhookUrl }),
    });
    const { batchId, warnings, ...status } = result;

    const modelByRequestId = new Map(
      normalizedRequests.map(request => [request.id, request.modelId]),
    );
    for (const { requestId, warning } of warnings) {
      logWarnings({
        warnings: [warning],
        provider: batchApi.provider,
        model: requestId == null ? undefined : modelByRequestId.get(requestId),
      });
    }

    return {
      version: 2,
      id: batchId,
      provider: batchApi.provider,
      ...status,
      warnings,
    };
  } catch (error) {
    throw wrapGatewayError(error);
  }
}

function validateCompatibleTools({
  requestId,
  tools,
  toolsByName,
}: {
  requestId: string;
  tools: ReadonlyArray<{ name: string }> | undefined;
  toolsByName: Map<string, unknown>;
}) {
  for (const tool of tools ?? []) {
    const previousTool = toolsByName.get(tool.name);

    if (previousTool != null && !isDeepEqualData(previousTool, tool)) {
      throw new InvalidArgumentError({
        parameter: 'requests',
        value: requestId,
        message: `tool "${tool.name}" must have the same definition in every batch request`,
      });
    }

    toolsByName.set(tool.name, tool);
  }
}

/**
 * Retrieves the latest normalized status for a batch.
 */
export async function getBatchStatus({
  provider,
  batch,
  providerOptions,
  maxRetries,
  abortSignal,
  headers,
  timeout,
}: GetBatchStatusOptions): Promise<BatchStatus> {
  const batchApi = resolveBatchApi(provider);
  validateBatchReference({ batchApi, batch });

  const operationAbortSignal = mergeAbortSignals(
    abortSignal,
    getTotalTimeoutMs(timeout),
  );
  const { retry } = prepareRetries({
    maxRetries,
    abortSignal: operationAbortSignal,
  });

  try {
    const status = await retry(() =>
      batchApi.doGetBatchStatus({
        batchId: batch.id,
        providerOptions,
        abortSignal: operationAbortSignal,
        headers: withUserAgentSuffix(headers ?? {}, `ai/${VERSION}`),
      }),
    );

    return status;
  } catch (error) {
    throw wrapGatewayError(error);
  }
}

/**
 * Streams complete terminal results for the requests in a batch.
 */
export function getBatchResults<TOOLS extends ToolSet>({
  provider,
  batch,
  tools,
  providerOptions,
  maxRetries,
  abortSignal,
  headers,
  timeout,
}: GetBatchResultsOptions<TOOLS>) {
  const batchApi = resolveBatchApi(provider);
  validateBatchReference({ batchApi, batch });

  const streamAbortController = new AbortController();
  const operationAbortSignal = mergeAbortSignals(
    abortSignal,
    getTotalTimeoutMs(timeout),
    streamAbortController.signal,
  );
  const { retry } = prepareRetries({
    maxRetries,
    abortSignal: operationAbortSignal,
  });
  const transformer: Transformer<BatchV4ItemResult, BatchItemResult<TOOLS>> & {
    cancel?: (reason?: unknown) => void;
  } = {
    async transform(item, controller) {
      controller.enqueue(
        await convertBatchItemResult({
          item,
          tools,
          abortSignal: operationAbortSignal,
        }),
      );
    },

    cancel(reason) {
      streamAbortController.abort(
        reason ?? new Error('Batch results stream was cancelled.'),
      );
    },
  };
  const transform = new TransformStream<
    BatchV4ItemResult,
    BatchItemResult<TOOLS>
  >(transformer);

  void (async () => {
    try {
      const stream = await retry(() =>
        batchApi.doGetBatchResults({
          batchId: batch.id,
          providerOptions,
          abortSignal: operationAbortSignal,
          headers: withUserAgentSuffix(headers ?? {}, `ai/${VERSION}`),
        }),
      );

      await stream.pipeTo(transform.writable, {
        signal: operationAbortSignal,
      });
    } catch (error) {
      await transform.writable.abort(wrapGatewayError(error)).catch(() => {});
    }
  })();

  return asAsyncIterableStream(transform.readable);
}

function resolveBatchApi(provider?: BatchProvider): BatchV4 {
  provider ??= asProviderV4(globalThis.AI_SDK_DEFAULT_PROVIDER ?? gateway);

  if (isBatchApi(provider)) {
    return provider;
  }

  if (!hasBatchFactory(provider)) {
    throw new UnsupportedFunctionalityError({
      functionality: 'batch processing',
      message:
        'The provider does not support batch processing. Make sure it exposes an experimental_batch() method.',
    });
  }

  return provider.experimental_batch();
}

function hasBatchFactory(
  provider: ProviderV4,
): provider is ProviderV4 & { experimental_batch(): BatchV4 } {
  return (
    typeof (provider as { experimental_batch?: unknown }).experimental_batch ===
    'function'
  );
}

function isBatchApi(provider: BatchProvider): provider is BatchV4 {
  const candidate = provider as Partial<BatchV4>;
  return (
    typeof candidate.doStartBatch === 'function' &&
    typeof candidate.doGetBatchStatus === 'function' &&
    typeof candidate.doGetBatchResults === 'function'
  );
}

function validateRequests(requests: ReadonlyArray<{ readonly id: string }>) {
  if (requests.length === 0) {
    throw new InvalidArgumentError({
      parameter: 'requests',
      value: requests,
      message: 'requests must not be empty',
    });
  }

  const ids = new Set<string>();

  for (const request of requests) {
    if (request.id.trim().length === 0) {
      throw new InvalidArgumentError({
        parameter: 'requests',
        value: requests,
        message: 'request IDs must not be empty',
      });
    }

    if (ids.has(request.id)) {
      throw new InvalidArgumentError({
        parameter: 'requests',
        value: requests,
        message: `request IDs must be unique; duplicate ID "${request.id}"`,
      });
    }

    ids.add(request.id);
  }
}

function validateBatchReference({
  batchApi,
  batch,
}: {
  batchApi: BatchV4;
  batch: BatchReference;
}) {
  if (batch.provider !== batchApi.provider) {
    throw new InvalidArgumentError({
      parameter: 'provider',
      value: batchApi,
      message:
        `provider ${batchApi.provider} is not compatible with ` +
        `batch provider ${batch.provider}`,
    });
  }
}

async function convertBatchItemResult<TOOLS extends ToolSet>({
  item,
  tools,
  abortSignal,
}: {
  item: BatchV4ItemResult;
  tools: TOOLS | undefined;
  abortSignal: AbortSignal | undefined;
}): Promise<BatchItemResult<TOOLS>> {
  switch (item.type) {
    case 'text':
      switch (item.status) {
        case 'succeeded':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            ...(await convertGenerateResult({
              result: item.result,
              tools,
              abortSignal,
            })),
          };
        case 'failed':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            error: item.error,
            providerMetadata: item.providerMetadata,
          };
        case 'cancelled':
        case 'expired':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            error: item.error,
            providerMetadata: item.providerMetadata,
          };
      }
    case 'image':
      switch (item.status) {
        case 'succeeded':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            ...convertImageResult(item.result),
          };
        case 'failed':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            error: item.error,
            providerMetadata: item.providerMetadata,
          };
        case 'cancelled':
        case 'expired':
          return {
            type: item.type,
            id: item.id,
            status: item.status,
            error: item.error,
            providerMetadata: item.providerMetadata,
          };
      }
  }
}

function convertImageResult(
  result: ImageModelV4Result,
): ImageBatchGenerationResult {
  return {
    images: result.images.map(
      (image, index) =>
        new DefaultGeneratedFile({
          data: image,
          mediaType:
            detectMediaType({ data: image, topLevelType: 'image' }) ??
            'image/png',
          providerMetadata: getImageProviderMetadata(
            result.providerMetadata,
            index,
          ),
        }),
    ),
    warnings: result.warnings,
    response: {
      timestamp: result.response.timestamp,
      modelId: result.response.modelId,
      headers: result.response.headers,
    },
    providerMetadata: result.providerMetadata,
    usage: result.usage,
  };
}

async function convertGenerateResult<TOOLS extends ToolSet>({
  result,
  tools,
  abortSignal,
}: {
  result: LanguageModelV4GenerateResult;
  tools: TOOLS | undefined;
  abortSignal: AbortSignal | undefined;
}): Promise<TextBatchGenerationResult<TOOLS>> {
  const toolCalls = await Promise.all(
    result.content
      .filter(
        (part): part is LanguageModelV4ToolCall => part.type === 'tool-call',
      )
      .map(toolCall =>
        parseToolCall<TOOLS>({
          toolCall,
          tools,
          repairToolCall: undefined,
          refineToolInput: undefined,
          instructions: undefined,
          messages: [],
        }),
      ),
  );
  const content = await convertLanguageModelContent<TOOLS>({
    content: result.content,
    toolCalls,
    toolOutputs: [],
    toolApprovalRequests: [],
    toolApprovalResponses: [],
    tools,
    abortSignal,
  });

  return {
    content,
    text: result.content
      .filter(
        (part): part is Extract<typeof part, { type: 'text' }> =>
          part.type === 'text',
      )
      .map(part => part.text)
      .join(''),
    finishReason: result.finishReason.unified,
    rawFinishReason: result.finishReason.raw,
    usage: asLanguageModelUsage(result.usage),
    ...(result.response != null
      ? {
          response: {
            id: result.response.id,
            timestamp: result.response.timestamp?.toISOString(),
            modelId: result.response.modelId,
          },
        }
      : {}),
    providerMetadata: result.providerMetadata,
  };
}
