import * as tourMachine from "@zag-js/tour";
import type { MachineInput } from "marko-zag";
import Icon from "../icon/icon.marko";
import { cn } from "#lib/utils.ts";
import { stripOwnProps } from "#lib/native-attrs.ts";
import { tour as styles } from "./classes.ts";

// A step whose target is a CSS SELECTOR instead of a live element getter.
// Zag's own StepDetails.target is `() => HTMLElement | null`, and functions
// cannot cross Marko tag input (input is serialized for resume — passing a
// getter throws `Unable to serialize "input" (reading steps[n].target)`).
// The selector is resolved into Zag's getter below, inside the component.
export interface TourStep
  extends Omit<tourMachine.StepDetails, "target" | "effect"> {
  /** CSS selector of the element this step highlights. Omit for a modal step. */
  target?: string | undefined;
}

// MachineInput = native <div> attributes ∩ the machine's full Props, with
// `id` made optional (generated below, overridable). `stepIdChange` is
// additive sugar enabling Marko's two-way bind shorthand
// (<Tour stepId:=currentStepId/>); Zag's own `onStepChange` also works and
// receives the details object. Zag's `steps` is replaced by `items`: its
// `target` getters are functions, and tag input is serialized for resume, so
// they cannot cross the boundary.
export type Input = Omit<
  MachineInput<"div", tourMachine.Props>,
  "steps"
> & {
  /** Steps with selector targets — the serializable way to describe a tour. */
  items?: TourStep[];
  /** Sugar for onStepChange — receives the plain step id. */
  stepIdChange?: (stepId: string | null) => void;
  /** Sugar for onStatusChange — receives the plain status. */
  statusChange?: (status: tourMachine.StepStatus) => void;
  /** Render-prop for the tour trigger (usually a "Start tour" button). */
  trigger?: Marko.AttrTag<{ content: Marko.Body<[Record<string, unknown>]> }>;
};

static function resolveSteps(
  items: TourStep[] | undefined,
): tourMachine.StepDetails[] {
  if (!items) return [];
  return items.map(({ target, ...step }) => ({
    ...step,
    target: target
      ? () =>
          typeof document === "undefined"
            ? null
            : document.querySelector<HTMLElement>(target)
      : undefined,
  }));
}

// `steps` is layered on in `props=` on purpose: the resolved steps carry
// target getter functions, and anything written as a tag attribute becomes
// serialized input. A template closure is the serializable currency here —
// the getters are created when the closure runs, never stored in input.
<zag/api=() => tourMachine from=input props=(picked) => ({
  ...picked,
  steps: resolveSteps(input.items),
})
  onStepChange(details: tourMachine.StepChangeDetails) {
    input.onStepChange?.(details);
    input.stepIdChange?.(details.stepId);
  }
/>

// Whatever is neither machine-owned (splitProps) nor component-owned
// (class, sugar callbacks, trigger) passes through to the content element.
<const/nativeAttrs=() =>
  stripOwnProps(
    tourMachine.splitProps(input)[1],
    "class",
    "items",
    "stepIdChange",
    "statusChange",
    "trigger",
  )
>

<if=input.trigger>
  <${input.trigger.content}({
    "data-slot": "tour-trigger",
    onClick: () => api().start(),
  })/>
</if>

// The tour machine carries its own position/size CSS vars inside each part's
// props style object (backdrop clip-path, spotlight rect, positioner
// transform) — so NO style attribute of our own goes on those parts.
<zag-portal>
  <if=api().open>
    <div
      ...api().getBackdropProps()
      data-slot="tour-backdrop"
      class=styles.backdrop
    />
    <div
      ...api().getSpotlightProps()
      data-slot="tour-spotlight"
      class=styles.spotlight
    />
    <div
      ...api().getPositionerProps()
      data-slot="tour-positioner"
      class=styles.positioner
    >
      <div
        ...nativeAttrs()
        ...api().getContentProps()
        data-slot="tour-content"
        class=cn(styles.content, styles.contentAnim, input.class)
      >
        <if=api().step?.arrow>
          // Zag sizes/paints the arrow from `--arrow-size` /
          // `--arrow-background`, which it expects the consumer to define, and
          // the tour machine (unlike the popover/tooltip machines) runs no
          // popper arrow middleware — nothing writes the arrow's offset. So
          // the side placement is done here in CSS, keyed off the content's
          // own `data-side`, with arbitrary-property classes rather than a
          // style attribute of our own (the arrow's style object is Zag's).
          <div
            ...api().getArrowProps()
            data-slot="tour-arrow"
            class=styles.arrow
          >
            <div ...api().getArrowTipProps() data-slot="tour-arrow-tip"/>
          </div>
        </if>

        <div class=styles.headerRow>
          <p
            ...api().getTitleProps()
            data-slot="tour-title"
            class=styles.title
          >
            ${api().step?.title}
          </p>
          <button type="button"
            ...api().getCloseTriggerProps()
            data-slot="tour-close"
            class=styles.close
          >
            <Icon name="X" class=styles.closeIcon/>
            <span class=styles.srOnly>Close tour</span>
          </button>
        </div>

        <p
          ...api().getDescriptionProps()
          data-slot="tour-description"
          class=styles.description
        >
          ${api().step?.description}
        </p>

        <div class=styles.footerRow>
          <div data-slot="tour-progress" class=styles.progress>
            <for|index| from=0 to=api().totalSteps - 1>
              <const/isCurrent=index === api().stepIndex/>
              <span
                data-slot="tour-progress-dot"
                data-current=(isCurrent ? "" : undefined)
                class=cn(
                  styles.progressDot,
                  isCurrent ? styles.progressDotCurrent : styles.progressDotInactive,
                )
              />
            </for>
            <span
              ...api().getProgressTextProps()
              data-slot="tour-progress-text"
              class=styles.progressText
            >
              ${api().stepIndex + 1} / ${api().totalSteps}
            </span>
          </div>

          <div data-slot="tour-actions" class=styles.actions>
            <for|action| of=api().step?.actions ?? []>
              <button type="button"
                ...api().getActionTriggerProps({ action })
                data-slot="tour-action-trigger"
                class=cn(
                  styles.actionTrigger,
                  styles.actionTriggerNext,
                  styles.actionTriggerPrev,
                  styles.actionTriggerSkip,
                  // `dismiss` is how a tour finishes, so it reads as primary —
                  // the muted ✕ is a separate part (closeTrigger).
                  styles.actionTriggerClose,
                  styles.actionTriggerCustom,
                )
              >
                ${action.label}
              </button>
            </for>
          </div>
        </div>
      </div>
    </div>
  </if>
</zag-portal>

// Parents can receive the api getter: <Tour/tourApi/> then
// `tourApi().start()` — it stays live across machine updates.
<return=api>
