/*
 * Copyright 2026 Element Creations Ltd.
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
 * Please see LICENSE files in the repository root for full details.
 */

import type { ReactNode } from "react";
import type { ViewModel } from "../../../core/viewmodel/ViewModel";

// ─── Timeline item: one renderable row ─────────────────────────────

/** Discriminated union of every row kind the timeline can render. */
export type TimelineItemKind = "event" | "date-separator" | "read-marker" | "loading" | "gap";

export interface EventTimelineItem {
    key: string;
    kind: "event";
    /** Whether this event continues unbroken from the previous sender (suppresses avatar/name). */
    continuation: boolean;
    /**
     * Whether this event closes its continuation group (next event has a different
     * sender / a gap / a separator, or it is the last). Rounds the group's closing
     * corner — border-radius only, so it is recomputed every build, never cached.
     */
    lastInSection: boolean;
}

export interface DateSeparatorTimelineItem {
    key: string;
    kind: "date-separator";
    label: string;
}

export interface ReadMarkerTimelineItem {
    key: string;
    kind: "read-marker";
}

export interface LoadingTimelineItem {
    key: string;
    kind: "loading";
}

export interface GapTimelineItem {
    key: string;
    kind: "gap";
}

export type TimelineItem =
    | EventTimelineItem
    | DateSeparatorTimelineItem
    | ReadMarkerTimelineItem
    | LoadingTimelineItem
    | GapTimelineItem;

/**
 * Stable keys for the edge loading spinners. The ViewModel assigns one of these as the
 * `key` of a `kind:"loading"` item; the View matches them to keep scroll anchoring off a
 * spinner (whose key vanishes when the loaded batch replaces it). Shared here so the two
 * sides can't drift.
 */
export const BACKWARD_LOADING_KEY = "backward-loading";
export const FORWARD_LOADING_KEY = "forward-loading";

// ─── Navigation anchor ─────────────────────────────────────────────

/** Where in the viewport to place the target when scrolling to an anchor. */
export type AnchorAlign = "start" | "center" | "end";

export interface NavigationAnchor {
    /** The `TimelineItem.key` to scroll to. */
    targetKey: string;
    /** Where in the viewport to place the target. */
    align: AnchorAlign;
}

/**
 * Imperative scroll-to-anchor handle the View hands to ViewModel actions, to scroll
 * without waiting for a data update. The VM invokes it only when the target is already
 * in the loaded window; otherwise it sets `pendingAnchor` and lets a load() drive the
 * scroll. Must be called synchronously inside the action — the View's closure captures
 * the current items snapshot to resolve the target.
 */
export type ImmediateScroll = (anchor: NavigationAnchor) => void;

// ─── Timeline view model contract ──────────────────────────────────

export interface TimelineViewSnapshot {
    /** The ordered list of items to render. */
    items: TimelineItem[];

    /**
     * True when the timeline window has reached the live end — i.e. there are
     * no more forward events to paginate to. Used to gate follow-on-append so
     * that the view only auto-scrolls to the bottom when we are actually
     * viewing the live end of the room.
     */
    atLiveEnd: boolean;

    /**
     * Placement target for the current load. The View scrolls it into place on mount
     * and re-asserts it on later loads without remounting. While set, follow-on-append
     * is disabled (this also keeps a cold-loading list pinned to the anchor rather than
     * snapping to the bottom). Cleared when the View reports {@link TimelineViewActions.onAnchorReached}.
     */
    pendingAnchor: NavigationAnchor | null;

    /**
     * The event ID that should be visually highlighted (e.g. permalink target).
     * Unlike `pendingAnchor`, this is not cleared after scrolling — it persists
     * so the event tile stays highlighted.
     */
    highlightedEventId: string | null;

    /** True when the list is scrolled to the bottom (within a 4px threshold). */
    isAtBottom: boolean;

    /**
     * Whether a read-marker is visible above (`"above"`) or below (`"below"`) the
     * current viewport, or not reachable/applicable (`false`).
     * - `"above"` — marker is above the viewport (or above the loaded window).
     * - `"below"` — marker is below the viewport but within the loaded window.
     * Controls visibility and direction of the "Jump to unread" / "Mark as read" bar.
     */
    canJumpToReadMarker: "above" | "below" | false;

    /**
     * Number of new messages that have arrived since the user last scrolled
     * to the live bottom. Reset to zero when the user reaches the live bottom.
     * Used as the badge count on the "Jump to bottom" button.
     */
    numUnreadMessages: number;

    /**
     * True when at least one of the new-since-leaving-bottom messages is a
     * highlight (mention / keyword). Drives the highlight style on the
     * "Jump to bottom" button.
     */
    hasHighlights: boolean;
}

export interface TimelineViewActions {
    /** Called when the view reaches the start; VM decides whether to paginate. */
    onStartReached(): void;

    /** Called when the view reaches the end; VM decides whether to paginate. */
    onEndReached(): void;

    /**
     * Report that the anchor placement has settled (the target has stabilised in
     * the visible range). The VM clears `pendingAnchor`, re-enabling
     * follow-on-append and normal scroll-position tracking.
     */
    onAnchorReached(): void;

    /**
     * Called on every visible-range change; the VM tracks the bottommost visible event
     * for scroll-position persistence. Indices are 0-based into the items array.
     */
    onVisibleRangeChanged(startIndex: number, endIndex: number): void;

    /** Called when the at-bottom state changes; VM uses this to decide whether to clear the saved scroll position on dispose. */
    onAtBottomStateChange(atBottom: boolean): void;

    /**
     * Scroll to the read-marker item (jump to unread messages).
     *
     * `scrollNow` is invoked synchronously when the marker is already in the
     * loaded window (no data update needed). Otherwise the VM triggers a load
     * at the marker and the scroll happens via `pendingAnchor` after the load.
     */
    onJumpToReadMarker(scrollNow: ImmediateScroll): void;

    /** Mark all currently-visible messages as read, clearing the read marker. */
    onMarkAllAsRead(): void;

    /**
     * Navigate to the live end of the timeline.
     *
     * `scrollNow` is invoked synchronously when the window already reaches
     * the live end (no data update needed). Otherwise the VM reloads the
     * timeline window at the live end and the scroll happens via
     * `pendingAnchor` after the load.
     */
    onJumpToLive(scrollNow: ImmediateScroll): void;
}

export type TimelineViewModel = ViewModel<TimelineViewSnapshot, TimelineViewActions>;

// ─── Shared timeline view props ────────────────────────────────────

export interface TimelineViewProps {
    vm: TimelineViewModel;

    /**
     * Render callback for each timeline item.
     * The shared container calls this for every visible item.
     */
    renderItem: (item: TimelineItem) => ReactNode;
}
