import {
  asSchema,
  isExecutableTool,
  safeValidateTypes,
  type Context,
  type InferToolSetContext,
  type ModelMessage,
  type ToolSet,
} from '@ai-sdk/provider-utils';
import { InvalidToolApprovalSignatureError } from '../error/invalid-tool-approval-signature-error';
import { InvalidToolInputError } from '../error/invalid-tool-input-error';
import { getOwn } from '../util/get-own';
import { isDeepEqualData } from '../util/is-deep-equal-data';
import type { CollectedToolApprovals } from './collect-tool-approvals';
import { refineParsedToolCallInput } from './parse-tool-call';
import { resolveToolApproval } from './resolve-tool-approval';
import { verifyToolApprovalSignature } from './tool-approval-signature';
import type { ToolApprovalConfiguration } from './tool-approval-configuration';
import type { ToolInputRefinement } from './tool-input-refinement';

/**
 * Re-validates approved tool approvals reconstructed from client-supplied
 * message history before they are executed. Checks HMAC signature (when
 * configured), input schema, and approval policy.
 */
export async function validateApprovedToolApprovals<
  TOOLS extends ToolSet,
  RUNTIME_CONTEXT extends Context | unknown | never,
>({
  approvedToolApprovals,
  tools,
  toolApproval,
  messages,
  toolsContext,
  runtimeContext,
  toolApprovalSecret,
  refineToolInput,
}: {
  approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
  tools: TOOLS | undefined;
  toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined;
  messages: ModelMessage[];
  toolsContext: InferToolSetContext<TOOLS>;
  runtimeContext: RUNTIME_CONTEXT;
  toolApprovalSecret?: string | Uint8Array;
  refineToolInput?: ToolInputRefinement<TOOLS>;
}): Promise<{
  approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
  deniedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
  invalidToolApprovals: Array<
    CollectedToolApprovals<TOOLS> & { error: InvalidToolInputError }
  >;
}> {
  const approved: Array<CollectedToolApprovals<TOOLS>> = [];
  const denied: Array<CollectedToolApprovals<TOOLS>> = [];
  const invalid: Array<
    CollectedToolApprovals<TOOLS> & { error: InvalidToolInputError }
  > = [];

  for (const collectedApproval of approvedToolApprovals) {
    // Look up the tool by own property only: `toolName` comes from
    // client-supplied history, so a name matching an inherited object property
    // (e.g. `constructor`, `toString`) must resolve to "no such tool" rather
    // than a prototype value that would silently skip input validation below.
    const tool = getOwn(tools, collectedApproval.toolCall.toolName);

    // History can omit or alter these fields. Rebuild them from the current
    // server-side definition before input refinement and approval policy checks,
    // just as parsing a new model tool call does.
    const {
      toolMetadata: _toolMetadata,
      dynamic: _dynamic,
      ...toolCallFromHistory
    } = collectedApproval.toolCall;
    const approval: CollectedToolApprovals<TOOLS> = {
      ...collectedApproval,
      toolCall: {
        ...toolCallFromHistory,
        ...(tool?.metadata != null ? { toolMetadata: tool.metadata } : {}),
        ...(tool?.type === 'dynamic' ? { dynamic: true } : {}),
        title: tool?.title,
      } as typeof collectedApproval.toolCall,
    };
    const { approvalRequest, toolCall } = approval;

    if (toolApprovalSecret != null) {
      if (approvalRequest.signature == null) {
        throw new InvalidToolApprovalSignatureError({
          approvalId: approvalRequest.approvalId,
          toolCallId: toolCall.toolCallId,
          reason: 'missing signature',
        });
      }

      const valid = await verifyToolApprovalSignature({
        secret: toolApprovalSecret,
        signature: approvalRequest.signature,
        approvalId: approvalRequest.approvalId,
        toolCallId: toolCall.toolCallId,
        toolName: toolCall.toolName,
        input: toolCall.input,
      });

      if (!valid) {
        throw new InvalidToolApprovalSignatureError({
          approvalId: approvalRequest.approvalId,
          toolCallId: toolCall.toolCallId,
          reason: 'invalid signature',
        });
      }
    }

    if (isExecutableTool(tool) && tool.inputSchema != null) {
      let validationError: unknown;
      try {
        // Preserve the approved input before validators or refiners can mutate
        // it, and give them an independent copy of the schema input.
        const approvedInput = structuredClone(toolCall.input);
        const hasInputSchemaInput = Object.prototype.hasOwnProperty.call(
          approvalRequest,
          'inputSchemaInput',
        );
        const validation = await safeValidateTypes({
          value: structuredClone(
            hasInputSchemaInput
              ? approvalRequest.inputSchemaInput
              : toolCall.input,
          ),
          schema: asSchema(tool.inputSchema),
        });

        if (!validation.success) {
          validationError = validation.error;
        } else {
          const revalidatedToolCall = await refineParsedToolCallInput({
            toolCall: {
              ...toolCall,
              input: validation.value,
            },
            refineToolInput,
          });

          // Normalize the revalidated input to the snapshot's realm.
          const revalidatedInput = structuredClone(revalidatedToolCall.input);

          // Revalidation must never change the operation that was approved,
          // including when older or projected history omits the schema input.
          if (!isDeepEqualData(revalidatedInput, approvedInput)) {
            validationError = new Error(
              'Approved tool input does not match the validated schema output.',
            );
          }
        }
      } catch (error) {
        validationError = error;
      }

      if (validationError != null) {
        invalid.push({
          ...approval,
          error: new InvalidToolInputError({
            toolName: toolCall.toolName,
            toolInput: JSON.stringify(toolCall.input),
            cause: validationError,
          }),
        });
        continue;
      }
    }

    const approvalStatus = await resolveToolApproval({
      tools,
      toolApproval,
      toolCall,
      messages,
      toolsContext,
      runtimeContext,
    });

    if (approvalStatus.type === 'denied') {
      denied.push({
        ...approval,
        approvalResponse: {
          ...approval.approvalResponse,
          approved: false,
          reason: approvalStatus.reason ?? approval.approvalResponse.reason,
        },
      });
    } else {
      approved.push(approval);
    }
  }

  return {
    approvedToolApprovals: approved,
    deniedToolApprovals: denied,
    invalidToolApprovals: invalid,
  };
}
