import type {
  ActionConfirmation,
  CalendlyBookingSlot,
  CalendlyScheduledEvent,
  WidgetTheme,
} from "./public-contracts.js";

import type { AccessibilityCapabilities } from "./accessibility/types.js";
import { createBookingOfferCard } from "./booking-view.js";
import type {
  ConversationMessage,
  ConversationState,
  ExternalAction,
} from "./conversation.js";
import { createMessageMarkdown } from "./message-markdown.js";
import { createPlaybookCard } from "./playbook-view.js";
import type { SubmitPlaybookFieldsRequest } from "./playbook-contracts.js";
import { surfaceStyles } from "./styles.js";
import { applyWidgetTheme } from "./theme.js";

export type SurfaceKind = "embed" | "inline";

export interface SurfaceViewHandlers {
  onAbort(): void;
  onConfirmAction(): void;
  onCancelAction(): void;
  onCompleteExternalAction(event: CalendlyScheduledEvent): void;
  onSelectBookingSlot(slot: CalendlyBookingSlot): void;
  onSubmitPlaybookFields(
    step: SubmitPlaybookFieldsRequest["step"],
    values: SubmitPlaybookFieldsRequest["values"],
  ): void;
  onSubmitBookingDetails(details: {
    name: string;
    email: string;
    timezone: string;
  }): void;
  onFeedback(resolved: boolean): void;
  onResetConversation(): void;
  onSend(message: string): void;
}

/**
 * Owns the stable Shadow DOM and translates controller snapshots into visible
 * UI. It intentionally contains no API key, fetch logic, or tenant decisions.
 */
export class SurfaceView {
  private readonly root: ShadowRoot;
  private readonly kind: SurfaceKind;
  private readonly surface: HTMLElement;
  private readonly conversation: HTMLElement;
  private readonly messages: HTMLElement;
  private readonly composer: HTMLFormElement;
  private readonly textarea: HTMLTextAreaElement;
  private readonly sendButton: HTMLButtonElement;
  private readonly sendIcon: HTMLElement;
  private readonly speechButton: HTMLButtonElement;
  private readonly status: HTMLElement;
  private readonly error: HTMLElement;
  private readonly dialog: HTMLDialogElement | undefined;
  private readonly dialogComposerSlot: HTMLElement | undefined;
  private readonly resumeButton: HTMLButtonElement | undefined;
  private capabilities: AccessibilityCapabilities = {};
  private handlers: SurfaceViewHandlers | undefined;
  private externalAction: ExternalAction | null = null;
  private hasBlockingInteraction = false;
  private isHistoryLoading = false;
  private isStreaming = false;
  private speechStop: (() => void) | undefined;

  constructor(root: ShadowRoot, kind: SurfaceKind) {
    this.root = root;
    this.kind = kind;
    const style = document.createElement("style");
    style.textContent = surfaceStyles;
    this.surface = createSurface(kind);

    this.conversation = createElement("div", "conversation");
    const conversationInner = createElement("div", "conversation-inner");
    this.messages = createElement("div", "messages");
    this.messages.setAttribute("role", "log");
    this.messages.setAttribute("aria-live", "polite");
    this.messages.setAttribute("aria-relevant", "additions text");
    conversationInner.append(this.messages);
    this.conversation.append(conversationInner);

    const composerElements = createComposer(kind);
    this.composer = composerElements.composer;
    this.textarea = composerElements.textarea;
    this.sendButton = composerElements.sendButton;
    this.sendIcon = composerElements.sendIcon;
    this.speechButton = composerElements.speechButton;

    this.status = createElement("div", "status");
    this.status.setAttribute("role", "status");
    this.status.setAttribute("aria-live", "polite");
    this.error = createElement("div", "error");
    this.error.setAttribute("role", "alert");
    this.error.hidden = true;

    if (kind === "embed") {
      const dialogElements = createEmbedDialog(
        this.conversation,
        this.status,
        this.error,
      );
      this.dialog = dialogElements.dialog;
      this.dialogComposerSlot = dialogElements.composerSlot;
      this.resumeButton = createResumeButton();
      this.composer.append(this.resumeButton);
      this.surface.append(this.composer);
      this.root.append(style, this.surface, this.dialog);
      this.bindDialogEvents(
        dialogElements.closeButton,
        dialogElements.resetButton,
      );
    } else {
      this.dialog = undefined;
      this.dialogComposerSlot = undefined;
      this.resumeButton = undefined;
      this.surface.append(
        this.conversation,
        this.composer,
        this.status,
        this.error,
      );
      this.root.append(style, this.surface);
    }

    this.bindComposerEvents();
    window.addEventListener("message", this.handleCalendlyMessage);
  }

  setHandlers(handlers: SurfaceViewHandlers): void {
    this.handlers = handlers;
  }

  setCapabilities(capabilities: AccessibilityCapabilities): void {
    this.capabilities = capabilities;
    this.speechButton.hidden = capabilities.speechToText === undefined;
  }

  setTheme(theme: WidgetTheme): void {
    applyWidgetTheme(this.root.host as HTMLElement, theme);
    this.textarea.placeholder = theme.placeholder;
  }

  /** Applies one immutable controller snapshot to the existing controls. */
  update(state: ConversationState): void {
    const wasHistoryLoading = this.isHistoryLoading;
    this.isHistoryLoading = state.isHistoryLoading;
    const isPlaybookBlocking =
      state.playbookRequest !== null &&
      state.playbookRequest.kind !== "handoff_result";
    this.hasBlockingInteraction =
      state.pendingAction !== null ||
      state.externalAction !== null ||
      isPlaybookBlocking;
    this.isStreaming = state.isStreaming;
    this.externalAction = state.externalAction;
    this.renderMessages(
      state.messages,
      state.isStreaming,
      state.feedback,
      state.pendingAction,
      state.externalAction,
      state.bookingOffer,
      state.selectedBookingSlot,
      state.playbookRequest,
      state.isActionSubmitting,
    );
    const hasConversation = state.messages.length > 0 || state.isHistoryLoading;
    this.conversation.classList.toggle(
      "conversation--open",
      this.kind === "embed" || hasConversation,
    );
    this.textarea.disabled =
      state.isStreaming ||
      state.pendingAction !== null ||
      state.externalAction !== null ||
      isPlaybookBlocking ||
      state.isActionSubmitting;
    this.sendButton.type = state.isStreaming ? "button" : "submit";
    this.sendButton.setAttribute(
      "aria-label",
      state.isStreaming ? "Interrompi risposta" : "Invia messaggio",
    );
    this.sendIcon.textContent = state.isStreaming ? "■" : "↑";
    this.status.textContent = state.isHistoryLoading
      ? "Recupero la conversazione…"
      : state.isStreaming
        ? "Akintu sta rispondendo… Premi Esc per interrompere."
        : "";
    this.error.hidden = state.error === null;
    this.error.textContent = state.error ?? "";
    this.updateResumeButton();
    if (
      wasHistoryLoading &&
      !state.isHistoryLoading &&
      this.hasBlockingInteraction
    ) {
      this.openOverlay();
    }
  }

  destroy(): void {
    this.speechStop?.();
    this.capabilities.textToSpeech?.cancel();
    window.removeEventListener("message", this.handleCalendlyMessage);
    this.closeOverlay(false);
  }

  private bindComposerEvents(): void {
    this.composer.addEventListener("submit", (event) =>
      this.handleSubmit(event),
    );
    this.sendButton.addEventListener("click", (event) => {
      if (!this.isStreaming) return;
      event.preventDefault();
      this.handlers?.onAbort();
    });
    this.textarea.addEventListener("keydown", (event) =>
      this.handleKeyDown(event),
    );
    this.speechButton.addEventListener("click", () => this.handleSpeech());
    this.resumeButton?.addEventListener("click", () => this.openOverlay());
  }

  private bindDialogEvents(
    closeButton: HTMLButtonElement,
    resetButton: HTMLButtonElement,
  ): void {
    closeButton.addEventListener("click", () => this.closeOverlay(true));
    resetButton.addEventListener("click", () => {
      if (
        window.confirm(
          "Vuoi iniziare una nuova conversazione? La cronologia attuale non sarà più mostrata in questa chat.",
        )
      ) {
        this.handlers?.onResetConversation();
      }
    });
    this.dialog?.addEventListener("cancel", (event) => {
      event.preventDefault();
      if (this.isStreaming) this.handlers?.onAbort();
      this.closeOverlay(true);
    });
    this.dialog?.addEventListener("close", () =>
      this.restoreCompactComposer(false),
    );
  }

  private handleSubmit(event: SubmitEvent): void {
    event.preventDefault();
    if (this.isStreaming) return;
    const message = this.textarea.value.trim();
    if (message.length === 0) return;
    if (this.kind === "embed") this.openOverlay();
    this.textarea.value = "";
    this.handlers?.onSend(message);
  }

  private handleKeyDown(event: KeyboardEvent): void {
    if (event.key === "Escape") {
      this.speechStop?.();
      if (this.isStreaming) this.handlers?.onAbort();
      return;
    }
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      this.textarea.form?.requestSubmit();
    }
  }

  private openOverlay(): void {
    if (this.dialog === undefined || this.dialogComposerSlot === undefined) {
      return;
    }
    if (this.resumeButton !== undefined) this.resumeButton.hidden = true;
    this.dialogComposerSlot.append(this.composer);
    if (this.dialog.open) return;
    if (typeof this.dialog.showModal === "function") {
      this.dialog.showModal();
    } else {
      this.dialog.setAttribute("open", "");
    }
    this.textarea.focus();
  }

  private closeOverlay(shouldRestoreFocus: boolean): void {
    if (this.dialog === undefined) return;
    if (this.dialog.open && typeof this.dialog.close === "function") {
      this.dialog.close();
    } else {
      this.dialog.removeAttribute("open");
    }
    this.restoreCompactComposer(shouldRestoreFocus);
  }

  private restoreCompactComposer(shouldRestoreFocus: boolean): void {
    if (this.kind !== "embed") return;
    if (this.composer.parentElement !== this.surface) {
      this.surface.append(this.composer);
    }
    this.updateResumeButton();
    if (shouldRestoreFocus && !this.textarea.disabled) this.textarea.focus();
  }

  private updateResumeButton(): void {
    if (this.resumeButton === undefined) return;
    const shouldShow =
      this.hasBlockingInteraction && this.dialog?.open !== true;
    this.resumeButton.hidden = !shouldShow;
    this.resumeButton.textContent =
      this.externalAction === null
        ? "Riprendi richiesta"
        : "Riprendi prenotazione";
  }

  private handleSpeech(): void {
    const speechToText = this.capabilities.speechToText;
    if (speechToText === undefined || this.speechStop !== undefined) return;
    this.speechButton.classList.add("speech-button--active");
    this.speechButton.textContent = "Ascolto…";
    const finish = (): void => {
      this.speechStop = undefined;
      this.speechButton.classList.remove("speech-button--active");
      this.speechButton.textContent = "Voce";
    };
    const recognition = speechToText.start({
      onTranscript: (transcript) => {
        this.textarea.value = transcript;
        this.textarea.focus();
      },
      onError: () => {
        this.status.textContent =
          "Il riconoscimento vocale non è disponibile. Puoi continuare scrivendo.";
        finish();
      },
      onEnd: finish,
    });
    this.speechStop = recognition === undefined ? undefined : recognition.stop;
    if (recognition === undefined) finish();
  }

  private readonly handleCalendlyMessage = (
    event: MessageEvent<unknown>,
  ): void => {
    const action = this.externalAction;
    if (action === null) return;
    if (event.origin !== new URL(action.schedulingUrl).origin) return;
    const scheduledEvent = parseCalendlyScheduledEvent(event.data);
    if (scheduledEvent === undefined) return;
    this.handlers?.onCompleteExternalAction(scheduledEvent);
  };

  private renderMessages(
    messages: ConversationMessage[],
    isStreaming: boolean,
    feedback: ConversationState["feedback"],
    pendingAction: ActionConfirmation | null,
    externalAction: ExternalAction | null,
    bookingOffer: ConversationState["bookingOffer"],
    selectedBookingSlot: ConversationState["selectedBookingSlot"],
    playbookRequest: ConversationState["playbookRequest"],
    isActionSubmitting: boolean,
  ): void {
    this.messages.replaceChildren();
    messages.forEach((message, index) => {
      const wrapper = createElement(
        "article",
        `message message--${message.role}`,
      );
      wrapper.setAttribute(
        "aria-label",
        message.role === "user" ? "Tu" : "Akintu",
      );
      const text = document.createElement("div");
      text.className =
        message.role === "assistant"
          ? "message-content message-content--formatted"
          : "message-content";
      const visibleContent =
        message.content ||
        (isStreaming && index === messages.length - 1
          ? "Sto preparando la risposta…"
          : "");
      if (message.role === "assistant") {
        text.append(createMessageMarkdown(visibleContent));
      } else {
        text.textContent = visibleContent;
      }
      wrapper.append(text);
      if (
        message.role === "assistant" &&
        message.content.length > 0 &&
        this.capabilities.textToSpeech !== undefined
      ) {
        const actions = createElement("div", "message-actions");
        const listenButton = document.createElement("button");
        listenButton.type = "button";
        listenButton.className = "listen-button";
        listenButton.textContent = "Ascolta la risposta";
        listenButton.addEventListener("click", () =>
          this.capabilities.textToSpeech?.speak(message.content),
        );
        actions.append(listenButton);
        wrapper.append(actions);
      }
      if (
        message.role === "assistant" &&
        message.content.length > 0 &&
        index === messages.length - 1 &&
        !isStreaming &&
        externalAction === null
      ) {
        wrapper.append(this.createFeedbackActions(feedback));
      }
      this.messages.append(wrapper);
    });
    if (pendingAction !== null) {
      this.messages.append(
        this.createActionConfirmation(pendingAction, isActionSubmitting),
      );
    }
    if (bookingOffer !== null && externalAction === null) {
      const booking = createBookingOfferCard({
        offer: bookingOffer,
        selectedSlot: selectedBookingSlot,
        isSubmitting: isActionSubmitting,
        onSelectSlot: (slot) => this.handlers?.onSelectBookingSlot(slot),
        onSubmitDetails: (details) =>
          this.handlers?.onSubmitBookingDetails(details),
      });
      this.messages.append(booking);
    }
    if (playbookRequest !== null) {
      this.messages.append(
        createPlaybookCard({
          request: playbookRequest,
          isSubmitting: isActionSubmitting,
          onSubmit: (step, values) =>
            this.handlers?.onSubmitPlaybookFields(step, values),
        }),
      );
    }
    if (externalAction !== null) {
      const booking = this.createCalendlyBooking(externalAction);
      this.messages.append(booking);
      this.messages.scrollTop = booking.offsetTop;
      return;
    }
    this.messages.scrollTop = this.messages.scrollHeight;
  }

  private createCalendlyBooking(action: ExternalAction): HTMLElement {
    const card = createElement("section", "calendly-booking");
    card.setAttribute("aria-label", "Prenotazione Calendly");
    const title = createElement("strong", "calendly-booking-title");
    title.textContent = `Conferma ${action.eventTypeName}`;
    const explanation = createElement("p", "calendly-booking-description");
    explanation.textContent =
      "I dati sono già precompilati. Conferma su Calendly: Akintu verificherà automaticamente l’esito.";
    const frame = document.createElement("iframe");
    frame.className = "calendly-booking-frame";
    frame.title = `Calendly · ${action.eventTypeName}`;
    frame.loading = "eager";
    frame.referrerPolicy = "strict-origin-when-cross-origin";
    const url = new URL(action.schedulingUrl);
    url.searchParams.set("embed_type", "Inline");
    url.searchParams.set("embed_domain", window.location.hostname);
    url.searchParams.set("name", action.inviteeName);
    url.searchParams.set("email", action.inviteeEmail);
    url.searchParams.set("utm_source", "akintu");
    url.searchParams.set("utm_medium", "chat");
    url.searchParams.set("utm_campaign", "booking");
    url.searchParams.set("utm_content", action.actionId);
    frame.src = url.toString();
    const fallback = document.createElement("a");
    fallback.className = "calendly-booking-fallback";
    fallback.href = url.toString();
    fallback.target = "_blank";
    fallback.rel = "noopener noreferrer";
    fallback.textContent = "Apri Calendly in una nuova scheda";
    card.append(title, explanation, frame, fallback);
    return card;
  }

  private createActionConfirmation(
    action: ActionConfirmation,
    isSubmitting: boolean,
  ): HTMLElement {
    const card = createElement("section", "action-confirmation");
    card.setAttribute("aria-label", "Conferma azione");
    const title = createElement("strong", "action-confirmation-title");
    title.textContent = "Conferma richiesta";
    const summary = createElement("p", "action-confirmation-summary");
    summary.textContent = action.summary;
    const actions = createElement("div", "action-confirmation-actions");
    const confirm = document.createElement("button");
    confirm.type = "button";
    confirm.className = "action-confirm-button";
    confirm.disabled = isSubmitting;
    confirm.textContent = isSubmitting ? "Esecuzione…" : "Conferma";
    confirm.addEventListener("click", () => this.handlers?.onConfirmAction());
    const cancel = document.createElement("button");
    cancel.type = "button";
    cancel.className = "action-cancel-button";
    cancel.disabled = isSubmitting;
    cancel.textContent = "Annulla";
    cancel.addEventListener("click", () => this.handlers?.onCancelAction());
    actions.append(confirm, cancel);
    card.append(title, summary, actions);
    return card;
  }

  private createFeedbackActions(
    feedback: ConversationState["feedback"],
  ): HTMLElement {
    const actions = createElement("div", "feedback-actions");
    if (feedback !== null) {
      actions.textContent = "Grazie per il feedback.";
      return actions;
    }
    const label = createElement("span", "feedback-label");
    label.textContent = "Hai risolto?";
    const yes = document.createElement("button");
    yes.type = "button";
    yes.className = "feedback-button";
    yes.textContent = "Sì";
    yes.addEventListener("click", () => this.handlers?.onFeedback(true));
    const no = document.createElement("button");
    no.type = "button";
    no.className = "feedback-button";
    no.textContent = "No";
    no.addEventListener("click", () => this.handlers?.onFeedback(false));
    actions.append(label, yes, no);
    return actions;
  }
}

function parseCalendlyScheduledEvent(
  value: unknown,
): CalendlyScheduledEvent | undefined {
  if (
    !isRecord(value) ||
    value.event !== "calendly.event_scheduled" ||
    !isRecord(value.payload)
  ) {
    return undefined;
  }
  const eventResource = value.payload.event;
  const inviteeResource = value.payload.invitee;
  if (!isRecord(eventResource) || !isRecord(inviteeResource)) return undefined;
  const eventUri = eventResource.uri;
  const inviteeUri = inviteeResource.uri;
  if (
    typeof eventUri !== "string" ||
    typeof inviteeUri !== "string" ||
    !areCalendlyResourceUrisValid(eventUri, inviteeUri)
  ) {
    return undefined;
  }
  return { eventUri, inviteeUri };
}

function areCalendlyResourceUrisValid(
  eventUri: string,
  inviteeUri: string,
): boolean {
  try {
    const eventUrl = new URL(eventUri);
    const inviteeUrl = new URL(inviteeUri);
    const eventSegments = eventUrl.pathname.split("/").filter(Boolean);
    const inviteeSegments = inviteeUrl.pathname.split("/").filter(Boolean);
    return (
      eventUrl.origin === "https://api.calendly.com" &&
      inviteeSegments[0] === "scheduled_events" &&
      inviteeUrl.origin === eventUrl.origin &&
      eventSegments.length === 2 &&
      eventSegments[0] === "scheduled_events" &&
      inviteeSegments.length === 4 &&
      inviteeSegments[1] === eventSegments[1] &&
      inviteeSegments[2] === "invitees"
    );
  } catch {
    return false;
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function createComposer(kind: SurfaceKind): {
  composer: HTMLFormElement;
  textarea: HTMLTextAreaElement;
  sendButton: HTMLButtonElement;
  sendIcon: HTMLElement;
  speechButton: HTMLButtonElement;
} {
  const composer = document.createElement("form");
  composer.className = "composer";
  const label = createElement("label", "visually-hidden");
  label.textContent = "Scrivi una domanda ad Akintu";
  const textarea = document.createElement("textarea");
  textarea.placeholder = "Ask anything";
  textarea.maxLength = 8_000;
  textarea.rows = kind === "embed" ? 2 : 1;
  label.htmlFor = "akintu-message";
  textarea.id = "akintu-message";

  const toolbar = createElement("div", "toolbar");
  const speechButton = document.createElement("button");
  speechButton.type = "button";
  speechButton.className = "speech-button";
  speechButton.textContent = "Voce";
  speechButton.setAttribute("aria-label", "Detta la domanda");
  speechButton.hidden = true;
  const sendButton = document.createElement("button");
  sendButton.type = "submit";
  sendButton.className = "send-button";
  sendButton.setAttribute("aria-label", "Invia messaggio");
  const sendIcon = createElement("span", "send-icon");
  sendIcon.setAttribute("aria-hidden", "true");
  sendIcon.textContent = "↑";
  sendButton.append(sendIcon);
  toolbar.append(speechButton, sendButton);
  composer.append(label, textarea, toolbar);
  return { composer, textarea, sendButton, sendIcon, speechButton };
}

function createResumeButton(): HTMLButtonElement {
  const button = document.createElement("button");
  button.type = "button";
  button.className = "resume-button";
  button.textContent = "Riprendi prenotazione";
  button.hidden = true;
  return button;
}

function createEmbedDialog(
  conversation: HTMLElement,
  status: HTMLElement,
  error: HTMLElement,
): {
  dialog: HTMLDialogElement;
  composerSlot: HTMLElement;
  closeButton: HTMLButtonElement;
  resetButton: HTMLButtonElement;
} {
  const dialog = document.createElement("dialog");
  dialog.className = "dialog";
  dialog.setAttribute("aria-label", "Conversazione con Akintu");
  const panel = createElement("section", "dialog-panel");
  const header = createElement("header", "dialog-header");
  const headerActions = createElement("div", "dialog-header-actions");
  const resetButton = document.createElement("button");
  resetButton.type = "button";
  resetButton.className = "reset-button";
  resetButton.textContent = "Nuova conversazione";
  const closeButton = document.createElement("button");
  closeButton.type = "button";
  closeButton.className = "close-button";
  closeButton.setAttribute("aria-label", "Chiudi conversazione");
  closeButton.textContent = "×";
  headerActions.append(resetButton, closeButton);
  header.append(headerActions);
  const composerSlot = createElement("div", "dialog-composer-slot");
  panel.append(header, conversation, composerSlot, status, error);
  dialog.append(panel);
  return { dialog, composerSlot, closeButton, resetButton };
}

function createSurface(kind: SurfaceKind): HTMLElement {
  const surface = createElement("section", `surface surface--${kind}`);
  surface.setAttribute(
    "aria-label",
    kind === "embed" ? "Collega digitale Akintu" : "Approfondisci con Akintu",
  );
  return surface;
}

function createElement<K extends keyof HTMLElementTagNameMap>(
  tagName: K,
  className: string,
): HTMLElementTagNameMap[K] {
  const element = document.createElement(tagName);
  element.className = className;
  return element;
}
