import {
  parseCalendlyEmbeddedBooking,
  parseChatHistory,
  parseWidgetConfig,
} from "./contracts.js";
import { mapHttpError, WidgetError } from "./errors.js";
import { parseSubmitPlaybookFieldsResponse } from "./playbook-contract-parser.js";
import type {
  ActionConfirmation,
  CalendlyBookingOffer,
  ChatHistoryResponse,
  ChatRequest,
  CalendlyScheduledEvent,
  ConversationFeedbackRequest,
  PrepareCalendlyBookingRequest,
  IntegrationActionResponse,
  WidgetConfigResponse,
} from "./public-contracts.js";
import type {
  StructuredUiRequest,
  SubmitPlaybookFieldsRequest,
  SubmitPlaybookFieldsResponse,
} from "./playbook-contracts.js";
import { parseSseStream } from "./sse.js";

export const DEFAULT_API_BASE_URL = "https://api.akintu.io";

export interface ChatStreamHandlers {
  onActionConfirmation?(action: ActionConfirmation): void;
  onBookingSlots?(offer: CalendlyBookingOffer): void;
  onStructuredUi?(request: StructuredUiRequest): void;
  onSources(chunkIds: string[]): void;
  onToken(content: string): void;
}

export interface WidgetApiClientOptions {
  apiKey: string;
  apiBaseUrl?: string | undefined;
  fetchImplementation?: typeof fetch | undefined;
}

export interface WidgetApiClient {
  getConfig(signal?: AbortSignal): Promise<WidgetConfigResponse>;
  getHistory(
    sessionId: string,
    signal?: AbortSignal,
  ): Promise<ChatHistoryResponse>;
  streamChat(
    input: ChatRequest,
    handlers: ChatStreamHandlers,
    signal?: AbortSignal,
  ): Promise<string>;
  confirmAction?(
    actionId: string,
    confirmationToken: string,
  ): Promise<IntegrationActionResponse>;
  completeAction?(
    actionId: string,
    event: CalendlyScheduledEvent,
  ): Promise<IntegrationActionResponse>;
  cancelAction?(actionId: string): Promise<void>;
  prepareCalendlyBooking?(
    input: PrepareCalendlyBookingRequest,
  ): Promise<IntegrationActionResponse>;
  submitPlaybookFields?(
    runId: string,
    input: SubmitPlaybookFieldsRequest,
  ): Promise<SubmitPlaybookFieldsResponse>;
  submitFeedback(input: ConversationFeedbackRequest): Promise<void>;
}

/**
 * Creates the only network boundary used by Embed and Inline. The raw key is
 * captured in this closure and is never copied into component attributes or
 * persistent storage.
 */
export function createWidgetApiClient(
  options: WidgetApiClientOptions,
): WidgetApiClient {
  const fetchImplementation = options.fetchImplementation ?? globalThis.fetch;
  const baseUrl = options.apiBaseUrl ?? DEFAULT_API_BASE_URL;

  return {
    async getConfig(signal) {
      const request: RequestInit = {
        headers: createWidgetHeaders(options.apiKey),
      };
      if (signal !== undefined) request.signal = signal;
      const response = await fetchImplementation(
        createApiUrl(baseUrl, "/api/widget-config"),
        request,
      );
      ensureSuccessfulResponse(response);
      return parseWidgetConfig(await readJson(response));
    },

    async getHistory(sessionId, signal) {
      const path = `/api/chat/history/${encodeURIComponent(sessionId)}`;
      const request: RequestInit = {
        headers: createWidgetHeaders(options.apiKey),
      };
      if (signal !== undefined) request.signal = signal;
      const response = await fetchImplementation(
        createApiUrl(baseUrl, path),
        request,
      );
      ensureSuccessfulResponse(response);
      return parseChatHistory(await readJson(response));
    },

    async streamChat(input, handlers, signal) {
      const request: RequestInit = {
        method: "POST",
        headers: {
          ...createWidgetHeaders(options.apiKey),
          "Content-Type": "application/json",
        },
        body: JSON.stringify(input),
      };
      if (signal !== undefined) request.signal = signal;
      const response = await fetchImplementation(
        createApiUrl(baseUrl, "/api/chat"),
        request,
      );
      ensureSuccessfulResponse(response);
      if (response.body === null) throw new WidgetError("stream_interrupted");

      // A closed connection is successful only after `done`. This distinguishes
      // a complete answer from a network interruption after the last token.
      let messageId: string | undefined;
      for await (const event of parseSseStream(response.body)) {
        switch (event.type) {
          case "token":
            handlers.onToken(event.content);
            break;
          case "sources":
            handlers.onSources(event.chunkIds);
            break;
          case "action_confirmation":
            handlers.onActionConfirmation?.(event);
            break;
          case "booking_slots":
            handlers.onBookingSlots?.(event.offer);
            break;
          case "structured_ui":
            handlers.onStructuredUi?.(event.request);
            break;
          case "done":
            messageId = event.messageId;
            break;
          case "error":
            throw new WidgetError("stream_error");
        }
      }
      if (messageId === undefined) throw new WidgetError("stream_interrupted");
      return messageId;
    },

    async confirmAction(actionId, confirmationToken) {
      const response = await fetchImplementation(
        createApiUrl(
          baseUrl,
          `/api/chat/actions/${encodeURIComponent(actionId)}/confirm`,
        ),
        {
          method: "POST",
          headers: {
            ...createWidgetHeaders(options.apiKey),
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ confirmationToken }),
        },
      );
      ensureSuccessfulResponse(response);
      return parseIntegrationActionResponse(await readJson(response));
    },

    async completeAction(actionId, event) {
      const response = await fetchImplementation(
        createApiUrl(
          baseUrl,
          `/api/chat/actions/${encodeURIComponent(actionId)}/complete`,
        ),
        {
          method: "POST",
          headers: {
            ...createWidgetHeaders(options.apiKey),
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ externalResult: event }),
        },
      );
      ensureSuccessfulResponse(response);
      return parseIntegrationActionResponse(await readJson(response));
    },

    async cancelAction(actionId) {
      const response = await fetchImplementation(
        createApiUrl(
          baseUrl,
          `/api/chat/actions/${encodeURIComponent(actionId)}/cancel`,
        ),
        { method: "POST", headers: createWidgetHeaders(options.apiKey) },
      );
      ensureSuccessfulResponse(response);
    },

    async prepareCalendlyBooking(input) {
      const response = await fetchImplementation(
        createApiUrl(baseUrl, "/api/chat/bookings"),
        {
          method: "POST",
          headers: {
            ...createWidgetHeaders(options.apiKey),
            "Content-Type": "application/json",
          },
          body: JSON.stringify(input),
        },
      );
      ensureSuccessfulResponse(response);
      return parseIntegrationActionResponse(await readJson(response));
    },

    async submitPlaybookFields(runId, input) {
      const path = `/api/chat/playbooks/${encodeURIComponent(runId)}/fields`;
      const response = await fetchImplementation(createApiUrl(baseUrl, path), {
        method: "POST",
        headers: {
          ...createWidgetHeaders(options.apiKey),
          "Content-Type": "application/json",
        },
        body: JSON.stringify(input),
      });
      ensureSuccessfulResponse(response);
      return parseSubmitPlaybookFieldsResponse(await readJson(response));
    },

    async submitFeedback(input) {
      const response = await fetchImplementation(
        createApiUrl(baseUrl, "/api/chat/feedback"),
        {
          method: "POST",
          headers: {
            ...createWidgetHeaders(options.apiKey),
            "Content-Type": "application/json",
          },
          body: JSON.stringify(input),
        },
      );
      ensureSuccessfulResponse(response);
    },
  };
}

function parseIntegrationActionResponse(
  value: unknown,
): IntegrationActionResponse {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new WidgetError("invalid_response");
  }
  const response = value as Record<string, unknown>;
  if (
    typeof response.id !== "string" ||
    typeof response.toolName !== "string" ||
    (response.status !== "succeeded" &&
      response.status !== "failed" &&
      response.status !== "awaiting_external_completion") ||
    typeof response.summary !== "string"
  ) {
    throw new WidgetError("invalid_response");
  }
  const result = response.result;
  if (response.status === "awaiting_external_completion") {
    return {
      id: response.id,
      toolName: response.toolName,
      status: response.status,
      summary: response.summary,
      result: parseCalendlyEmbeddedBooking(result),
    };
  }
  return {
    id: response.id,
    toolName: response.toolName,
    status: response.status,
    summary: response.summary,
    ...(typeof result === "object" && result !== null && !Array.isArray(result)
      ? { result: result as Record<string, unknown> }
      : {}),
  };
}

function createApiUrl(baseUrl: string, path: string): string {
  return new URL(path, `${baseUrl.replace(/\/$/, "")}/`).toString();
}

// Browsers own the Origin header. Setting it manually would trigger invalid
// CORS behavior and could not prove the actual embedding origin.
function createWidgetHeaders(apiKey: string): Record<string, string> {
  return {
    Accept: "application/json, text/event-stream",
    "X-Client-Key": apiKey,
  };
}

function ensureSuccessfulResponse(response: Response): void {
  if (!response.ok) throw mapHttpError(response.status);
}

async function readJson(response: Response): Promise<unknown> {
  try {
    return (await response.json()) as unknown;
  } catch {
    throw new WidgetError("invalid_response", { status: response.status });
  }
}
