/**
 * MessageController — message loading, pagination, and pending-delete logic.
 * Extracted from MainPage to reduce god-object coupling and enable unit testing.
 */

import type { ApiClient } from "@lib/api";
import { createLogger } from "@lib/logger";
import {
  setMessages,
  prependMessages,
  isChannelLoaded,
  getChannelMessages,
  setChannelLoading,
  setChannelLoadError,
} from "@stores/messages.store";

const log = createLogger("message-ctrl");
const PAGE_SIZE = 50;

// ---------------------------------------------------------------------------
// Pending Delete Manager
// ---------------------------------------------------------------------------

export interface PendingDeleteManager {
  /**
   * Attempt to delete a message. Returns "confirmed" on the second click
   * within the timeout window, "pending" on the first click.
   */
  tryDelete(msgId: number): "confirmed" | "pending";
  /** Clear all pending timeouts. */
  cleanup(): void;
}

export function createPendingDeleteManager(): PendingDeleteManager {
  const pending = new Map<number, number>();

  function tryDelete(msgId: number): "confirmed" | "pending" {
    if (pending.has(msgId)) {
      window.clearTimeout(pending.get(msgId));
      pending.delete(msgId);
      return "confirmed";
    }
    const tid = window.setTimeout(() => pending.delete(msgId), 5000);
    pending.set(msgId, tid);
    return "pending";
  }

  function cleanup(): void {
    for (const tid of pending.values()) {
      window.clearTimeout(tid);
    }
    pending.clear();
  }

  return { tryDelete, cleanup };
}

// ---------------------------------------------------------------------------
// Message Controller
// ---------------------------------------------------------------------------

export interface MessageControllerOptions {
  readonly api: ApiClient;
  readonly showError: (msg: string) => void;
}

export interface MessageController {
  loadMessages(channelId: number, signal: AbortSignal): Promise<void>;
  loadOlderMessages(channelId: number, signal: AbortSignal): Promise<void>;
}

export function createMessageController(opts: MessageControllerOptions): MessageController {
  const { api, showError } = opts;

  async function loadMessages(channelId: number, signal: AbortSignal): Promise<void> {
    if (isChannelLoaded(channelId)) {
      log.debug("Messages already loaded", { channelId });
      return;
    }
    // Runs synchronously before the first await, so the message region shows
    // its in-region loading placeholder from the very first render.
    setChannelLoading(channelId);
    try {
      const resp = await api.getMessages(channelId, { limit: PAGE_SIZE }, signal);
      // Re-check "loaded" after the await: a same-channel jump can install an
      // around-window (setAroundMessages) while this mount-time tail fetch is
      // still in flight — nothing aborts this fetch's signal in that case.
      // Both landing marks the channel loaded, so a tail response that lost
      // the race is discarded instead of clobbering the jump's window.
      if (!signal.aborted && !isChannelLoaded(channelId)) {
        log.info("Messages loaded", {
          channelId,
          count: resp.messages.length,
          hasMore: resp.has_more,
        });
        setMessages(channelId, resp.messages, resp.has_more);
      }
    } catch (err) {
      // Same re-check as the success path above: a same-channel jump can
      // install an around-window (setAroundMessages) while this mount-time
      // tail fetch is still in flight, and nothing aborts this fetch's
      // signal in that case. If that window already landed and marked the
      // channel loaded, this fetch lost the race — it must be discarded
      // silently instead of flagging a correctly-loaded, fully-rendered
      // channel as load-errored and toasting on top of a jump that worked.
      if (!signal.aborted && !isChannelLoaded(channelId)) {
        log.error("Failed to load messages", {
          channelId,
          error: String(err),
        });
        // Inline section error + Retry in the message region (UX spec §2) —
        // a toast would vanish and leave the region silently empty.
        setChannelLoadError(channelId);
        // The inline region only renders when the channel has no rows; live
        // broadcasts or an optimistic send may already have populated it, in
        // which case the failure must still be surfaced (no silent drop).
        if (getChannelMessages(channelId).length > 0) {
          showError("Failed to load message history");
        }
      }
    }
  }

  async function loadOlderMessages(channelId: number, signal: AbortSignal): Promise<void> {
    const messages = getChannelMessages(channelId);
    if (messages.length === 0) return;
    const oldest = messages[0]!;
    try {
      const resp = await api.getMessages(
        channelId,
        { before: oldest.id, limit: PAGE_SIZE },
        signal,
      );
      if (!signal.aborted) {
        // The window can be replaced wholesale while this fetch is in flight
        // (e.g. a same-channel jump swaps in an around-window via
        // setAroundMessages) — nothing aborts this fetch's controller in that
        // case. Splicing this now-stale page onto a window it was never
        // fetched for would duplicate/misorder rows, so bail if the row this
        // page continues from is no longer the window's oldest.
        const current = getChannelMessages(channelId);
        if (current.length > 0 && current[0]!.id === oldest.id) {
          prependMessages(channelId, resp.messages, resp.has_more);
        }
      }
    } catch (err) {
      if (!signal.aborted) {
        log.error("Failed to load older messages", {
          channelId,
          error: String(err),
        });
        showError("Failed to load older messages");
      }
    }
  }

  return { loadMessages, loadOlderMessages };
}
