/**
 * CommandLinkManager — mounts as a child of <Article> and intercepts
 * clicks on command-like links (>roll, >spark, >link, >stat) to send
 * them as journal stream messages instead of navigating.
 *
 * Uses capture-phase event delegation so it fires before the Article
 * component's own link interception (which would otherwise navigate).
 */

import { Component, onCleanup } from "solid-js";
import { useArticleDom } from "./Article";
import { useJournalStream, sendMessage } from "./stores/journalStream";
import { useJournalCompletions } from "./journal/completions";
import { parseInput } from "./journal/command-parser";
import { resolveRollPayload } from "./journal/types/roll";
import { resolveSparkPayload } from "./journal/types/spark";
import {
  resolveStatRoll,
  resolveTemplateSet,
  canModifyStat,
  fullKey,
  findStatDef,
} from "./journal/stat-helpers";

/** Commands that get intercepted as stream messages. */
const COMMAND_PREFIXES = [">roll", ">spark", ">link", ">stat"];

/**
 * Extract the command text from an href.
 * Percent-encoded spaces (%20) are decoded automatically by decodeURIComponent.
 */
function hrefToCommand(href: string): string | null {
  // Exclude external URLs and pure anchors
  if (/^(https?:|\/\/)/i.test(href)) return null;
  if (href === "#" || href.startsWith("#!")) return null;

  // Decode percent-encoding (%20 → space, etc.)
  let cleaned = decodeURIComponent(href);

  // Strip trailing .md extension (markdown links may include it)
  if (cleaned.endsWith(".md")) {
    cleaned = cleaned.slice(0, -3);
  }

  // Only consider links starting with a command prefix
  if (!COMMAND_PREFIXES.some((p) => cleaned.startsWith(p))) return null;

  return cleaned.trim();
}

// We need unwrap helper for sendMessage results
function unwrap<R>(r: { success: true; msg: R } | { success: false; error: string }) {
  return r.success
    ? ({ ok: true, err: undefined } as const)
    : ({ ok: false, err: r.error } as const);
}

export const CommandLinkManager: Component = () => {
  const contentDom = useArticleDom();
  const stream = useJournalStream();
  const comp = useJournalCompletions();

  const isObserver = () => stream.myRole === "observer";
  const isPlayer = () => stream.myRole === "player";

  // ---- Click interceptor (capture phase) ----

  const onClick = (e: MouseEvent) => {
    const anchor = (e.target as HTMLElement).closest("a[href]");
    if (!anchor) return;

    const href = anchor.getAttribute("href");
    if (!href) return;

    const command = hrefToCommand(href);
    if (!command) return;

    // This is a command link — intercept it
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();

    dispatchCommand(command);
  };

  const dom = contentDom();
  if (dom) {
    // Use capture phase to beat the Article component's bubble-phase handler
    dom.addEventListener("click", onClick, true);
  }

  onCleanup(() => {
    const d = contentDom();
    if (d) d.removeEventListener("click", onClick, true);
  });

  // ---- Command dispatch ----

  function dispatchCommand(raw: string) {
    // Convert > prefix to / so parseInput understands it
    const command = raw.startsWith(">") ? "/" + raw.slice(1) : raw;

    // Observers: everything goes as chat
    if (isObserver()) {
      sendMessage("chat", { text: command });
      return;
    }

    const parsed = parseInput(command);
    if (parsed.error) return;

    // Players: chat + stat commands only
    if (isPlayer()) {
      if (parsed.type === "chat") {
        sendMessage("chat", { text: raw });
        return;
      }
      if (parsed.type === "stat") {
        dispatchStat(parsed.payload as Record<string, unknown>);
        return;
      }
      return;
    }

    // GM: all commands
    if (parsed.type === "roll") {
      const p = resolveRollPayload(
        parsed.payload as { notation: string; label?: string },
      );
      sendMessage("roll", p);
    } else if (parsed.type === "spark") {
      void (async () => {
        try {
          const key = (parsed.payload as { key: string }).key;
          const match = comp.data.sparkTables.find((s) => s.slug === key);
          const csvPath = match?.csvPath ?? "";
          const remix = match?.remix ?? false;
          const p = await resolveSparkPayload({ key, csvPath, remix });
          sendMessage("spark", p);
        } catch {
          // silently ignore spark table errors from links
        }
      })();
    } else if (parsed.type === "stat") {
      dispatchStat(parsed.payload as Record<string, unknown>);
    } else {
      sendMessage(parsed.type, parsed.payload);
    }
  }

  /** Resolve bare key -> full key (copied from JournalInput for standalone use). */
  function resolveKey(
    inputKey: string,
    statDefs: typeof comp.data.stats,
    playerName: string,
  ): string {
    if (statDefs.some((d) => fullKey(d, playerName) === inputKey)) return inputKey;
    const def = statDefs.find((d) => d.key === inputKey);
    if (def) return fullKey(def, playerName);
    return inputKey;
  }

  function dispatchStat(payload: Record<string, unknown>) {
    const p = payload as { action?: string; key?: string; value?: string };

    if (p.action === "roll" && p.key) {
      const resolved = resolveStatRoll(
        p.key,
        comp.data.stats,
        stream.stats,
        stream.myName,
        comp.data.statTemplates,
      );
      if (resolved.error) return;

      const result = sendMessage("stat", {
        action: "set",
        key: resolved.fullKey,
        value: resolved.value,
      });
      if (!unwrap(result).ok) return;

      if (resolved.modifiers) {
        for (const [mk, mv] of Object.entries(resolved.modifiers)) {
          sendMessage("stat", { action: "set", key: mk, value: mv });
        }
      }
      return;
    }

    if (!p.action || !p.key) return;

    const fk = resolveKey(p.key, comp.data.stats, stream.myName);

    if (!canModifyStat(stream.myRole, stream.myName, fk, comp.data.stats)) {
      return;
    }

    sendMessage("stat", { action: p.action, key: fk, value: p.value });

    if (p.action === "set" && p.value) {
      const def = findStatDef(fk, comp.data.stats, stream.myName);
      if (def) {
        const modifiers = resolveTemplateSet(
          def,
          p.value,
          comp.data.stats,
          stream.stats,
          stream.myName,
          comp.data.statTemplates,
        );
        if (modifiers) {
          for (const [mk, mv] of Object.entries(modifiers)) {
            sendMessage("stat", { action: "set", key: mk, value: mv });
          }
        }
      }
    }
  }

  return null; // renders nothing — purely event-based
};

export default CommandLinkManager;
