// One live Zag toast child service, rendered by <Toaster> for each entry in the
// group machine's `toasts` context.
//
// Serialization contract: this component is only ever instantiated on the
// client (the toaster region server-renders empty, because toasts are created
// by user interaction). Its inputs are two closures created in the parent's
// template — `options` and `parent` — plus a number. Template-created closures
// are serializable currency in Marko; the unserializable values they close over
// (the running group service, the Zag option object) never appear as tag input
// themselves.
import * as toastMachine from "@zag-js/toast";
import { connect, type ZagModule } from "marko-zag";
import Icon from "../icon/icon.marko";
import { cn } from "#lib/utils.ts";
import { toastItem as styles } from "./classes.ts";

export interface Input {
  /** Getter for this toast's options as held by the group machine. */
  options: () => toastMachine.Props;
  /** Getter for the running group service that owns this toast. */
  parent: () => toastMachine.GroupService;
  /** This toast's position in the group, front-most first. */
  index: number;
  /**
   * The group service getter.
   *
   * Half of this machine's rendered geometry comes from the PARENT's context —
   * `computed("height")`, `heightIndex`, `heightBefore` and `zIndex` all read
   * `prop("parent").context`. A group update therefore has to invalidate this
   * child's connected api, and reading the getter inside `props=` is what
   * makes the parent's notify invalidate this child too — its identity
   * changes on every group update.
   */
  group: () => unknown;
  /**
   * Extra class(es) applied to this toast's root, merged after the built-in
   * ones. Threaded down from `<Toaster>`'s `toastClass` input — the zag-native
   * equivalent of sonner's `toastOptions.classNames.toast`.
   */
  toastClass?: string;
}

// `@zag-js/toast` has no runtime `props` export (unlike every other zag
// module) — its Props/GroupProps are TS-only types, so the module doesn't
// structurally satisfy `ZagModule` even though `<zag-machine>`'s `props=`
// verbatim mode (no `from=`) never actually reads `.props`. Widen the
// getter's return type to bridge that gap; `props` is never read here.
<const/toastModule=() => toastMachine as unknown as typeof toastMachine & ZagModule/>

// Built from closures (`input.options()`, `input.parent()`), not `input`
// itself, so `props=` is used verbatim (no `from=`). Reading `input.group()`
// here — even though its value is discarded — is what threads the parent's
// notify into this child's own reactive dependency list.
<zag-machine/service=toastModule props=() => {
  input.group();
  return {
    ...input.options(),
    index: input.index,
    parent: input.parent(),
  };
}/>
<const/api=() => connect(toastModule(), service())/>

// The machine measures this toast in a microtask fired by its entry action —
// which in Marko lands before the browser has laid the new element out, so the
// measurement reads 0 and the whole stack collapses onto one offset. Re-send
// the machine's own MEASURE event once layout has actually happened.
<lifecycle
  onMount() {
    const frame = requestAnimationFrame(() => {
      requestAnimationFrame(() => service().send({ type: "MEASURE" }));
    });
    return { cleanup: () => cancelAnimationFrame(frame) };
  }
  onDestroy() {
    this.cleanup?.();
  }
/>

// Zag drives placement/stacking through CSS custom properties on this
// element (--x/--y/--scale/--z-index/--height/--opacity). They must be
// consumed by the `translate`/`scale` PROPERTIES, not by Tailwind's
// `transform` utilities, which would overwrite them.
//
// `--scale` is only set for background toasts in overlap mode, so it needs
// an identity fallback. `--height` is deliberately NOT consumed: Zag
// measures each toast with `offsetHeight`, and binding the CSS `height` to
// the measurement it feeds produces a 0px fixed point on first paint.
// `min-h` keeps the stack offsets stable while the content sizes itself.
<div
  ...api().getRootProps()
  data-slot="toast"
  class=cn(input.toastClass, styles.root)
>
  // Ghost elements keep hover/pointer state stable across the gap between
  // toasts and during exit transitions. The "before" ghost is scaled to twice
  // the root height, so every real part must stack above it to stay clickable.
  //
  // These must be <div>, not <span>: Zag's getGhostBefore/AfterStyle set
  // block-box geometry (position/inset/height/width), and `normalize.element`
  // types the props as div attributes.
  <div ...api().getGhostBeforeProps()/>

  // Deliberately NOT converted to <Icon>: this is lucide's `circle-check-big`
  // (a broken-arc circle with an oversized check breaking out of it), and the
  // abstract icon-name set <Icon> resolves against is the 191-name mapping
  // vendored verbatim from shadcn's public/r/icons/index.json (see
  // tooling/build-icons.ts) — that set has no `circle-check-big` entry. Its
  // nearest neighbours (CircleCheckIcon / CheckCircle2Icon) are the DIFFERENT
  // `circle-check` glyph (closed circle, small inset check), so swapping would
  // silently change the rendered mark. Left inline until the mapping gains the
  // name.
  <if=(api().type === "success")>
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="toast-icon" class=styles.iconSuccess>
      <path d="M21.801 10A10 10 0 1 1 17 3.335"/>
      <path d="m9 11 3 3L22 4"/>
    </svg>
  </if>
  <else if=(api().type === "error")>
    <Icon
      name="CircleAlertIcon"
      data-slot="toast-icon"
      class=styles.iconError
    />
  </else>
  <else if=(api().type === "warning")>
    <Icon
      name="TriangleAlertIcon"
      data-slot="toast-icon"
      class=styles.iconWarning
    />
  </else>
  <else if=(api().type === "info")>
    <Icon
      name="InfoIcon"
      data-slot="toast-icon"
      class=styles.iconInfo
    />
  </else>
  <else if=(api().type === "loading")>
    <Icon
      name="Loader2"
      data-slot="toast-icon"
      class=styles.iconLoading
    />
  </else>

  <div class=styles.body>
    <if=api().title>
      <div ...api().getTitleProps() data-slot="toast-title" class=styles.title>
        ${api().title}
      </div>
    </if>
    <if=api().description>
      <div ...api().getDescriptionProps() data-slot="toast-description" class=styles.description>
        ${api().description}
      </div>
    </if>
  </div>

  <if=input.options().action>
    <button type="button"
      ...api().getActionTriggerProps()
      data-slot="toast-action"
      class=styles.action
    >
      ${input.options().action?.label}
    </button>
  </if>

  <if=api().closable>
    <button type="button"
      ...api().getCloseTriggerProps()
      data-slot="toast-close"
      aria-label="Dismiss notification"
      class=styles.close
    >
      <Icon name="X" class=styles.closeIcon/>
    </button>
  </if>

  <div ...api().getGhostAfterProps()/>
</div>
