import { safeParseJSON } from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { MCP_EVENT_CALLBACK_KEY } from './mcp-events';
import { verifyMCPEventSignature } from './mcp-event-signature';
import {
  MCPEventSchema,
  MCPEventControlSchema,
  type MCPEvent,
  type MCPEventControl,
  type MCPEventDefinition,
  type MCPEventStore,
  type MCPEventSubscriptionInfo,
} from './mcp-event-types';

const MAX_BODY_BYTES = 256 * 1024;
const VerificationSchema = z.object({
  type: z.literal('verification'),
  challenge: z.string().min(1),
});

export type MCPEventWebhookOptions = {
  store: MCPEventStore;
  /**
   * Optionally validate event data using an application schema or the server's
   * payloadSchema. Throw to reject the payload with 400 before onEvent runs.
   * The event envelope and signature are always checked.
   */
  validatePayload?: (args: {
    definition: MCPEventDefinition;
    data: MCPEvent['data'];
  }) => void | PromiseLike<void>;
  /**
   * Resolve only after durable acceptance or successful processing. Deliveries
   * may be duplicated or reordered; deduplicate by subscription ID + eventId.
   * Throw to return 503 so the server can retry. The helper does not run agents.
   */
  onEvent: (args: {
    subscription: MCPEventSubscriptionInfo;
    event: MCPEvent;
  }) => Promise<void>;
  /**
   * Handles a signed replay gap: some events are no longer available. Reconcile
   * application state or notify the agent, then resolve after durable acceptance.
   * The helper saves the fresh cursor and truncated flag; delivery continues.
   * Throw to return 503 without changing saved state. Deduplicate by subscription
   * ID + messageId.
   */
  onGap?: (args: {
    subscription: MCPEventSubscriptionInfo;
    messageId: string;
    gap: Extract<MCPEventControl, { type: 'gap' }>;
  }) => Promise<void>;
  /**
   * Handles signed termination: the server has ended the subscription. Inspect
   * termination.error and notify the agent, then resolve after durable acceptance.
   * The helper removes the saved subscription after this callback succeeds.
   * Throw to return 503 without changing saved state. Deduplicate by subscription
   * ID + messageId.
   */
  onTerminated?: (args: {
    subscription: MCPEventSubscriptionInfo;
    messageId: string;
    termination: Extract<MCPEventControl, { type: 'terminated' }>;
  }) => Promise<void>;
  onError?: (error: unknown) => void;
};

/**
 * Create a Web Request/Response handler for MCP webhook deliveries. Mount it
 * at the callback URL before subscribing. Verification is enabled automatically
 * using the pending delivery secret saved by the client's subscribe method.
 */
export function createMCPEventWebhook({
  store,
  validatePayload,
  onEvent,
  onGap,
  onTerminated,
  onError,
}: MCPEventWebhookOptions): (request: Request) => Promise<Response> {
  return async request => {
    if (request.method !== 'POST') {
      return new Response(null, { status: 405, headers: { Allow: 'POST' } });
    }
    if (
      request.headers
        .get('content-type')
        ?.split(';')[0]
        .trim()
        .toLowerCase() !== 'application/json'
    ) {
      return new Response(null, { status: 415 });
    }
    if (Number(request.headers.get('content-length')) > MAX_BODY_BYTES) {
      return new Response(null, { status: 413 });
    }

    try {
      const key = new URL(request.url).searchParams.get(MCP_EVENT_CALLBACK_KEY);
      const id = request.headers.get('X-MCP-Subscription-Id');
      if (!key || !id) return new Response(null, { status: 401 });
      const subscription = await store.get(key);
      if (
        !subscription ||
        (subscription.id != null && subscription.id !== id)
      ) {
        return new Response(null, { status: 401 });
      }

      // Bound streamed bodies too; Content-Length is optional and untrusted.
      const reader = request.body?.getReader();
      if (!reader) return new Response(null, { status: 400 });
      const chunks: Uint8Array[] = [];
      let length = 0;
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          length += value.byteLength;
          if (length > MAX_BODY_BYTES) {
            await reader.cancel();
            return new Response(null, { status: 413 });
          }
          chunks.push(value);
        }
      } finally {
        reader.releaseLock();
      }
      const body = new Uint8Array(length);
      let offset = 0;
      for (const chunk of chunks) {
        body.set(chunk, offset);
        offset += chunk.length;
      }

      if (
        !(await verifyMCPEventSignature({
          headers: request.headers,
          body,
          secret: subscription.delivery.secret,
        }))
      ) {
        return new Response(null, { status: 401 });
      }
      const parsed = await safeParseJSON({
        text: new TextDecoder().decode(body),
      });
      if (!parsed.success) return new Response(null, { status: 400 });
      const value = parsed.value;

      let control: MCPEventControl | undefined;
      if (value != null && typeof value === 'object' && 'type' in value) {
        if (value.type === 'verification') {
          const verification = VerificationSchema.safeParse(value);
          if (!verification.success) return new Response(null, { status: 400 });
          return Response.json({ challenge: verification.data.challenge });
        }
        const parsedControl = MCPEventControlSchema.safeParse(value);
        if (!parsedControl.success) return new Response(null, { status: 400 });
        control = parsedControl.data;
      }

      // Servers can POST before a subscribe or refresh response is persisted.
      // Events and gaps must retry until the ID and renewed expiration are saved.
      // A verified termination for a known ID can end a pending renewal too:
      // access may have been revoked, so renewal can no longer succeed.
      if (
        subscription.id == null ||
        (subscription.status !== 'active' && control?.type !== 'terminated')
      )
        return new Response(null, { status: 503 });
      const subscriptionInfo: MCPEventSubscriptionInfo = {
        id: subscription.id,
        name: subscription.name,
        arguments: subscription.arguments,
        delivery: { mode: 'webhook', url: subscription.delivery.url },
        refreshBefore: subscription.refreshBefore ?? null,
      };
      if (control) {
        const messageId = request.headers.get('webhook-id')!;
        if (control.type === 'gap') {
          await onGap?.({
            subscription: subscriptionInfo,
            messageId,
            gap: control,
          });
          await store.update(subscription.key, {
            cursor: control.cursor,
            truncated: true,
          });
        } else {
          await onTerminated?.({
            subscription: subscriptionInfo,
            messageId,
            termination: control,
          });
          await store.delete(subscription.key);
        }
        return new Response(null, { status: 204 });
      }
      if (
        subscription.refreshBefore != null &&
        Date.parse(subscription.refreshBefore) <= Date.now()
      ) {
        return new Response(null, { status: 410 });
      }
      const event = MCPEventSchema.safeParse(value);
      if (
        !event.success ||
        event.data.name !== subscription.name ||
        event.data.eventId !== request.headers.get('webhook-id')
      ) {
        return new Response(null, { status: 400 });
      }
      try {
        await validatePayload?.({
          definition: subscription.definition,
          data: event.data.data,
        });
      } catch (error) {
        onError?.(error);
        return new Response(null, { status: 400 });
      }

      await onEvent({
        subscription: subscriptionInfo,
        event: event.data,
      });
      if (event.data.cursor !== undefined) {
        await store.update(subscription.key, { cursor: event.data.cursor });
      }
      return new Response(null, { status: 204 });
    } catch (error) {
      onError?.(error);
      return new Response(null, { status: 503 });
    }
  };
}
