"use client";

/**
 * One event's place in its booking journey: cart, hold, purchase.
 *
 * Lives in its own module because two tables show it — the shared event records
 * table and the member page's own — and a second copy would drift from the first
 * the moment the producer changes a field.
 */

import { Box, Group, Stack, Text, Tooltip } from "@mantine/core";
import { Fragment } from "react";
import {
  moduleLabel,
  eventTypeLabel,
  type EventRecord,
} from "@/lib/features/event-analytics/types";

/**
 * Which records are already represented inside a later step's journey.
 *
 * `addToCartId` and `bookingHoldId` hold the Firestore document ids of the
 * earlier steps, so one booking attempt spans three documents and the table
 * lists all three. Drawing the strip on each would show the same journey three
 * times, with only the final row showing it in full.
 *
 * A record is superseded when some other loaded record points back at it. The
 * purchase is never superseded — nothing comes after it — so the journey is
 * shown once, on the most complete step that is in view.
 *
 * Scoped to the records actually loaded. A cart whose purchase sits on the next
 * page still draws its own strip, which is right: within what the reader can
 * see, that is all the journey there is.
 */
export function buildJourneyIndex(
  records: EventRecord[],
): Map<string, EventRecord> {
  const supersededBy = new Map<string, EventRecord>();
  for (const record of records) {
    for (const earlier of [record.addToCartId, record.bookingHoldId]) {
      if (!earlier) continue;
      const existing = supersededBy.get(earlier);
      /*
       * Prefer the latest step when several point at the same ancestor — a cart
       * is referenced by both its hold and its purchase, and the purchase is the
       * one carrying the complete journey.
       */
      if (!existing || rank(record.eventType) > rank(existing.eventType)) {
        supersededBy.set(earlier, record);
      }
    }
  }
  return supersededBy;
}

/** Chronological order of a single journey's events. */
const STEP_ORDER = ["add_to_cart", "booking_hold_initiated", "purchase"];
const rank = (eventType: string) => {
  const i = STEP_ORDER.indexOf(eventType);
  return i === -1 ? STEP_ORDER.length : i;
};

const MONTHS = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
];

/** "17:33:53" — the row shows times, with the date carried once beside them. */
function formatTime(iso: string | null): string | null {
  if (!iso) return null;
  const m = /[T ](\d{2}):(\d{2}):(\d{2})/.exec(iso);
  return m ? `${m[1]}:${m[2]}:${m[3]}` : iso;
}

/** "05 Oct" — the day part alone, for the row's single date label. */
function formatDay(iso: string | null): string | null {
  if (!iso) return null;
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
  return m ? `${m[3]} ${MONTHS[Number(m[2]) - 1] ?? m[2]}` : iso;
}

/** "05 Oct 17:33" — the row's compact form. Same no-parsing rule as below. */
function formatStampShort(iso: string | null): string | null {
  if (!iso) return null;
  const m = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/.exec(iso);
  if (!m) return iso;
  const [, , mo, d, hh, mm] = m;
  return `${d} ${MONTHS[Number(mo) - 1] ?? mo} ${hh}:${mm}`;
}

/**
 * "05 Oct 2026 · 17:33:53", read straight off the string.
 *
 * The producer writes a naive ISO timestamp with no zone. Parsing it into a Date
 * would have the browser read it as local time and render it back shifted, so the
 * parts are taken from the string as written — which is what someone comparing
 * this against Firestore expects to see.
 */
function formatStamp(iso: string | null): string | null {
  if (!iso) return null;
  const m = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})/.exec(iso);
  if (!m) return iso;
  const [, y, mo, d, hh, mm, ss] = m;
  const month = MONTHS[Number(mo) - 1] ?? mo;
  return `${d} ${month} ${y} · ${hh}:${mm}:${ss}`;
}

/** Hours between two ISO strings, or null when either is missing/unparseable. */
function hoursBetween(from: string | null, to: string | null): number | null {
  if (!from || !to) return null;
  const a = Date.parse(from);
  const b = Date.parse(to);
  if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
  const hours = (b - a) / 3_600_000;
  return hours >= 0 ? hours : null;
}

/** "1h 43m", "12m", "3d 4h" — the gap between two steps, read at a glance. */
function formatGap(hours: number): string {
  /*
   * Seconds matter at this end of the scale. A cart held and purchased 21
   * seconds apart rendered as "0m", which read as "no time recorded" rather
   * than "immediately" — and immediately is the interesting part.
   */
  const seconds = hours * 3600;
  if (seconds < 60) return `${Math.max(1, Math.round(seconds))}s`;
  if (hours < 1) return `${Math.round(hours * 60)}m`;
  if (hours < 24) {
    const h = Math.floor(hours);
    const m = Math.round((hours - h) * 60);
    return m ? `${h}h ${m}m` : `${h}h`;
  }
  const d = Math.floor(hours / 24);
  const h = Math.round(hours % 24);
  return h ? `${d}d ${h}h` : `${d}d`;
}

/**
 * The booking attempt this event belongs to: cart, hold, purchase.
 *
 * Every event carries the timestamps of the steps *before* it —
 * `addToCartAt` and `bookingHoldAt` — so one row is enough to reconstruct the
 * journey up to that point without fetching its siblings. The event's own step
 * is marked, since the same journey appears on up to three rows and it would
 * otherwise be unclear which one is being looked at.
 *
 * A step with no timestamp is shown as not reached rather than hidden: the gap
 * is the finding. A cart that never became a hold is exactly what someone
 * scanning this table is looking for.
 */
export default function JourneyStrip({
  record,
  supersededBy,
  variant = "full",
}: {
  record: EventRecord;
  /** The later step whose journey already covers this one, when there is one. */
  supersededBy?: EventRecord;
  /**
   * `inline` is the row itself: three nodes and the gaps between them, sized to
   * sit in a table cell. `full` adds the timestamps and the source fields.
   *
   * The journey belongs in the row — it is why someone is reading the row at all
   * — so it should not be something you have to open a panel to see.
   */
  variant?: "full" | "inline";
}) {
  /*
   * Already shown further down the table, in full. Pointing at it is more use
   * than repeating a prefix of it — and repeating it invites the reader to count
   * one booking attempt as three.
   */
  if (variant === "inline") {
    const purchaseAt =
      record.eventType === "purchase" ? record.occurredAt : null;
    const steps = [
      { key: "add_to_cart", short: "Cart", at: record.addToCartAt },
      {
        key: "booking_hold_initiated",
        short: "Hold",
        at: record.bookingHoldAt,
      },
      { key: "purchase", short: "Purchase", at: purchaseAt },
    ].map((step) =>
      step.key === record.eventType && !step.at
        ? { ...step, at: record.occurredAt }
        : step,
    );

    /*
     * The date is carried once, not under every node.
     *
     * Nearly every journey completes inside a day — this one ran 21 seconds — so
     * repeating "05 Oct" three times spent the row's width on the part that does
     * not change. When a journey does straddle midnight the days differ and each
     * node gets its own, because then the date is the point.
     */
    const days = [
      ...new Set(steps.map((s) => formatDay(s.at)).filter(Boolean)),
    ];
    const singleDay = days.length === 1 ? days[0] : null;

    return (
      <Group gap="xs" wrap="nowrap" align="center">
        {singleDay && (
          <Text size="xs" c="dimmed" fw={500} style={{ whiteSpace: "nowrap" }}>
            {singleDay}
          </Text>
        )}
        <Group gap={0} wrap="nowrap" align="flex-start">
          {steps.map((step, i) => {
            const reached = Boolean(step.at);
            const isCurrent = step.key === record.eventType;
            const gap = i > 0 ? hoursBetween(steps[i - 1].at, step.at) : null;
            return (
              <Fragment key={step.key}>
                {i > 0 && (
                  <Stack gap={0} align="center" style={{ width: 46 }} mt={4}>
                    <Box
                      style={{
                        height: 2,
                        width: "100%",
                        background: reached
                          ? "var(--mantine-color-teal-4)"
                          : "var(--mantine-color-gray-3)",
                      }}
                    />
                    <Text size="xs" c="dimmed" lh={1.3}>
                      {gap !== null ? formatGap(gap) : ""}
                    </Text>
                  </Stack>
                )}
                <Tooltip
                  label={`${eventTypeLabel(step.key)} — ${formatStamp(step.at) ?? "not reached"}`}
                >
                  <Stack gap={1} align="center" style={{ minWidth: 62 }}>
                    <Box
                      style={{
                        width: 10,
                        height: 10,
                        borderRadius: "50%",
                        flexShrink: 0,
                        border: `2px solid ${
                          reached
                            ? "var(--mantine-color-teal-6)"
                            : "var(--mantine-color-gray-4)"
                        }`,
                        background: reached
                          ? isCurrent
                            ? "var(--mantine-color-teal-6)"
                            : "var(--mantine-color-teal-1)"
                          : "transparent",
                      }}
                    />
                    <Text
                      size="xs"
                      c={reached ? undefined : "dimmed"}
                      fw={isCurrent ? 600 : 400}
                      lh={1.3}
                      style={{ fontVariantNumeric: "tabular-nums" }}
                    >
                      {(singleDay
                        ? formatTime(step.at)
                        : formatStampShort(step.at)) ?? "—"}
                    </Text>
                    <Text size="xs" c="dimmed" lh={1.2}>
                      {step.short}
                    </Text>
                  </Stack>
                </Tooltip>
              </Fragment>
            );
          })}
        </Group>
      </Group>
    );
  }

  if (supersededBy) {
    return (
      <Stack gap={4} p="md">
        <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
          Journey
        </Text>
        <Text size="sm">
          Part of the {eventTypeLabel(supersededBy.eventType).toLowerCase()}{" "}
          journey
          {supersededBy.occurredAt
            ? ` on ${formatStamp(supersededBy.occurredAt)}`
            : ""}{" "}
          — shown there in full.
        </Text>
        <Text size="xs" c="dimmed" ff="monospace">
          {supersededBy.id}
        </Text>
      </Stack>
    );
  }

  const purchaseAt = record.eventType === "purchase" ? record.occurredAt : null;
  const steps = [
    { key: "add_to_cart", at: record.addToCartAt, id: record.addToCartId },
    {
      key: "booking_hold_initiated",
      at: record.bookingHoldAt,
      id: record.bookingHoldId,
    },
    { key: "purchase", at: purchaseAt, id: null as string | null },
  ];

  /*
   * An event's own step may predate the correlation ids.
   *
   * The producer only began writing `addToCartID` recently, so an older cart row
   * has no `addToCartAt` of its own. Falling back to the event's timestamp for
   * its own step keeps the strip truthful rather than showing the row itself as
   * "not reached".
   */
  const resolved = steps.map((step) =>
    step.key === record.eventType && !step.at
      ? { ...step, at: record.occurredAt }
      : step,
  );

  return (
    <Stack gap="md" p="md">
      <Group gap="xs" align="center">
        <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
          Journey
        </Text>
        {record.journeyId ? (
          <Text size="xs" c="dimmed" ff="monospace">
            {record.journeyId}
          </Text>
        ) : (
          <Text size="xs" c="dimmed">
            steps inferred from this event
          </Text>
        )}
      </Group>

      {/*
        A line with a node per step, rather than three badges in a row.
        The gap between steps is the thing being read — how long a member sat on
        a cart before holding it — and a connector carrying that number shows it
        where a list of badges only implied it.
      */}
      <Group gap={0} align="flex-start" wrap="nowrap">
        {resolved.map((step, i) => {
          const reached = Boolean(step.at);
          const isCurrent = step.key === record.eventType;
          const gap = i > 0 ? hoursBetween(resolved[i - 1].at, step.at) : null;
          return (
            <Fragment key={step.key}>
              {i > 0 && (
                <Stack
                  gap={2}
                  align="center"
                  style={{ flex: 1, minWidth: 64 }}
                  mt={7}
                >
                  <Box
                    style={{
                      height: 2,
                      width: "100%",
                      background: reached
                        ? "var(--mantine-color-teal-4)"
                        : "var(--mantine-color-gray-3)",
                    }}
                  />
                  <Text size="xs" c="dimmed" ta="center">
                    {gap !== null ? formatGap(gap) : reached ? "—" : ""}
                  </Text>
                </Stack>
              )}

              <Stack gap={4} align="center" style={{ minWidth: 132 }}>
                <Box
                  style={{
                    width: 16,
                    height: 16,
                    borderRadius: "50%",
                    border: `2px solid ${
                      reached
                        ? "var(--mantine-color-teal-6)"
                        : "var(--mantine-color-gray-4)"
                    }`,
                    background: reached
                      ? isCurrent
                        ? "var(--mantine-color-teal-6)"
                        : "var(--mantine-color-teal-1)"
                      : "transparent",
                  }}
                />
                <Text size="sm" fw={isCurrent ? 700 : 500} ta="center">
                  {eventTypeLabel(step.key)}
                </Text>
                <Text size="xs" c="dimmed" ta="center">
                  {formatStamp(step.at) ?? "not reached"}
                </Text>
              </Stack>
            </Fragment>
          );
        })}
      </Group>

      {/* Where the attempt came from: the in-app module, then the campaign. */}
      <Group gap="xl">
        <Stack gap={0}>
          <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
            Source
          </Text>
          <Text size="sm">{moduleLabel(record.source)}</Text>
        </Stack>
        <Stack gap={0}>
          <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
            Channel
          </Text>
          <Text size="sm">
            {record.utmSource
              ? `${record.utmSource}${record.utmMedium ? ` / ${record.utmMedium}` : ""}`
              : "Direct"}
          </Text>
        </Stack>
        {record.propertyName && (
          <Stack gap={0}>
            <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
              Property
            </Text>
            <Text size="sm">{record.propertyName}</Text>
          </Stack>
        )}
      </Group>
    </Stack>
  );
}
