import { useCallback, useEffect, useState } from "preact/hooks";
import { getCardLiveContent } from "../../cardRuntime/store";
import type { CardItem, CardState, JsonValue, SessionSummary, WorkbenchItem } from "../../types";
import { BACKEND_URL, fetchPersistedCardsFromBackend, fetchWorkbenchFromBackend } from "./backend";
import type { RawPersistedCard, RawWorkbenchItem } from "./types";

const CARD_LIVE_CONTENT_EVENT = "nanobot:card-live-content-change";
const INBOX_REFRESH_EVENT = "nanobot:inbox-refresh";

let cardIdCounter = 0;

const STATE_RANK: Record<CardState, number> = {
  active: 0,
  stale: 1,
  resolved: 2,
  superseded: 3,
  archived: 4,
};

function readCardScore(card: Pick<CardItem, "priority" | "serverId">): number {
  if (!card.serverId) return card.priority;
  const liveContent = getCardLiveContent(card.serverId);
  if (!liveContent || typeof liveContent !== "object" || Array.isArray(liveContent)) {
    return card.priority;
  }
  const score = (liveContent as Record<string, unknown>).score;
  return typeof score === "number" && Number.isFinite(score) ? score : card.priority;
}

function compareCards(a: CardItem, b: CardItem): number {
  const stateDiff = STATE_RANK[a.state] - STATE_RANK[b.state];
  if (stateDiff !== 0) return stateDiff;
  const scoreDiff = readCardScore(b) - readCardScore(a);
  if (scoreDiff !== 0) return scoreDiff;
  if (a.priority !== b.priority) return b.priority - a.priority;
  const createdDiff = b.createdAt.localeCompare(a.createdAt);
  if (createdDiff !== 0) return createdDiff;
  return a.id - b.id;
}

function readTaskGroupKey(
  card: Pick<CardItem, "serverId" | "slot" | "templateKey" | "templateState">,
): string {
  const raw = card.templateState;
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
    const taskKey = raw.task_key;
    if (typeof taskKey === "string" && taskKey.trim()) return taskKey.trim();
  }
  return "";
}

function isTaskParentCard(card: Pick<CardItem, "templateKey" | "templateState">): boolean {
  return card.templateKey === "todo-item-live" && !!readTaskGroupKey(card);
}

function isLinkedHelperCard(card: Pick<CardItem, "serverId" | "slot" | "templateState">): boolean {
  const taskGroupKey = readTaskGroupKey(card);
  if (!taskGroupKey) return false;
  const raw = card.templateState;
  const helperKind =
    raw && typeof raw === "object" && !Array.isArray(raw) ? raw.helper_kind : undefined;
  return (
    (typeof helperKind === "string" && !!helperKind.trim()) ||
    (card.serverId ?? "").startsWith("task-helper-") ||
    (card.slot ?? "").startsWith("taskhelper:")
  );
}

interface TaskLinkedCardIndex {
  parentGroups: Set<string>;
  helpersByGroup: Map<string, CardItem[]>;
}

function indexTaskLinkedCards(base: CardItem[]): TaskLinkedCardIndex {
  const parentGroups = new Set<string>();
  const helpersByGroup = new Map<string, CardItem[]>();

  for (const card of base) {
    const groupKey = readTaskGroupKey(card);
    if (!groupKey) continue;
    if (isTaskParentCard(card)) {
      parentGroups.add(groupKey);
      continue;
    }
    if (!isLinkedHelperCard(card)) continue;
    const current = helpersByGroup.get(groupKey);
    if (current) current.push(card);
    else helpersByGroup.set(groupKey, [card]);
  }

  return { parentGroups, helpersByGroup };
}

function shouldDeferLinkedHelper(card: CardItem, index: TaskLinkedCardIndex): boolean {
  const groupKey = readTaskGroupKey(card);
  return !!groupKey && isLinkedHelperCard(card) && index.parentGroups.has(groupKey);
}

function appendGroupedTaskHelpers(
  grouped: CardItem[],
  emitted: Set<number>,
  card: CardItem,
  index: TaskLinkedCardIndex,
): void {
  const groupKey = readTaskGroupKey(card);
  if (!groupKey || !isTaskParentCard(card)) return;
  const helpers = index.helpersByGroup.get(groupKey) ?? [];
  for (const helper of helpers) {
    if (emitted.has(helper.id)) continue;
    grouped.push(helper);
    emitted.add(helper.id);
  }
}

function sortCards(items: CardItem[]): CardItem[] {
  const base = [...items].sort(compareCards);
  const linkedIndex = indexTaskLinkedCards(base);

  const emitted = new Set<number>();
  const grouped: CardItem[] = [];
  for (const card of base) {
    if (emitted.has(card.id)) continue;
    if (shouldDeferLinkedHelper(card, linkedIndex)) continue;
    grouped.push(card);
    emitted.add(card.id);
    appendGroupedTaskHelpers(grouped, emitted, card, linkedIndex);
  }
  return grouped;
}

function toCardItem(msg: Extract<RawPersistedCard, { type?: "card" }>): Omit<CardItem, "id"> {
  return {
    serverId: msg.id,
    kind: msg.kind,
    content: msg.content,
    title: msg.title,
    question: msg.question || undefined,
    choices: msg.choices.length > 0 ? msg.choices : undefined,
    responseValue: msg.response_value || undefined,
    slot: msg.slot || undefined,
    lane: msg.lane,
    priority: msg.priority,
    state: msg.state,
    templateKey: msg.template_key || undefined,
    templateState:
      msg.template_state && typeof msg.template_state === "object" ? msg.template_state : undefined,
    contextSummary: msg.context_summary || undefined,
    createdAt: msg.created_at || new Date().toISOString(),
    updatedAt: msg.updated_at || new Date().toISOString(),
  };
}

function mergeCardItem(prev: CardItem[], item: Omit<CardItem, "id">): CardItem[] {
  const existingIndex = item.serverId
    ? prev.findIndex((card) => card.serverId === item.serverId)
    : -1;
  if (existingIndex >= 0) {
    const next = [...prev];
    next[existingIndex] = { ...next[existingIndex], ...item };
    return sortCards(next);
  }
  return sortCards([...prev, { ...item, id: cardIdCounter++ }]);
}

function normalizePersistedCard(raw: RawPersistedCard): Omit<CardItem, "id"> {
  return toCardItem({
    type: "card",
    ...(raw as Omit<Extract<RawPersistedCard, { type?: "card" }>, "type">),
  });
}

function areEquivalentCardItems(
  existing: CardItem,
  next: Omit<CardItem, "id"> & { id: number },
): boolean {
  return (
    existing.serverId === next.serverId &&
    existing.kind === next.kind &&
    existing.content === next.content &&
    existing.title === next.title &&
    existing.question === next.question &&
    existing.responseValue === next.responseValue &&
    existing.slot === next.slot &&
    existing.lane === next.lane &&
    existing.priority === next.priority &&
    existing.state === next.state &&
    existing.templateKey === next.templateKey &&
    existing.contextSummary === next.contextSummary &&
    existing.createdAt === next.createdAt &&
    areStringArraysEqual(existing.choices, next.choices) &&
    areJsonRecordsEqual(existing.templateState, next.templateState)
  );
}

function areStringArraysEqual(left: string[] | undefined, right: string[] | undefined): boolean {
  if (left === right) return true;
  if (!left || !right) return !left && !right;
  if (left.length !== right.length) return false;
  return left.every((value, index) => value === right[index]);
}

function areJsonRecordsEqual(
  left: Record<string, JsonValue> | undefined,
  right: Record<string, JsonValue> | undefined,
): boolean {
  if (left === right) return true;
  if (!left || !right) return !left && !right;
  try {
    return JSON.stringify(left) === JSON.stringify(right);
  } catch {
    return false;
  }
}

function reconcilePersistedCards(prev: CardItem[], rawCards: RawPersistedCard[]): CardItem[] {
  const byServerId = new Map(
    prev.filter((card) => card.serverId).map((card) => [card.serverId as string, card]),
  );
  const next = rawCards.map((raw) => {
    const card = normalizePersistedCard(raw);
    const existing = card.serverId ? byServerId.get(card.serverId) : undefined;
    return {
      ...card,
      id: existing?.id ?? cardIdCounter++,
    };
  });
  const reused = next.map((card) => {
    const existing = card.serverId ? byServerId.get(card.serverId) : undefined;
    if (!existing) return card;
    return areEquivalentCardItems(existing, card) ? existing : card;
  });
  const sorted = sortCards(reused);
  if (sorted.length === prev.length && sorted.every((card, index) => card === prev[index])) {
    return prev;
  }
  return sorted;
}

function toWorkbenchItem(msg: Extract<RawWorkbenchItem, { type?: "workbench" }>): WorkbenchItem {
  return {
    id: msg.id,
    chatId: msg.chat_id,
    kind: msg.kind,
    title: msg.title,
    content: msg.content,
    question: msg.question || undefined,
    choices: msg.choices.length > 0 ? msg.choices : undefined,
    responseValue: msg.response_value || undefined,
    slot: msg.slot || undefined,
    templateKey: msg.template_key || undefined,
    templateState:
      msg.template_state && typeof msg.template_state === "object" ? msg.template_state : undefined,
    contextSummary: msg.context_summary || undefined,
    promotable: msg.promotable !== false,
    sourceCardId: msg.source_card_id || undefined,
    createdAt: msg.created_at || new Date().toISOString(),
    updatedAt: msg.updated_at || new Date().toISOString(),
  };
}

function normalizePersistedWorkbench(raw: RawWorkbenchItem): WorkbenchItem {
  return toWorkbenchItem({
    type: "workbench",
    ...(raw as Omit<Extract<RawWorkbenchItem, { type?: "workbench" }>, "type">),
  });
}

function sortWorkbenchItems(items: WorkbenchItem[]): WorkbenchItem[] {
  return [...items].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}

function mergeWorkbenchItem(prev: WorkbenchItem[], item: WorkbenchItem): WorkbenchItem[] {
  const existingIndex = prev.findIndex((entry) => entry.id === item.id);
  if (existingIndex >= 0) {
    const next = [...prev];
    next[existingIndex] = { ...next[existingIndex], ...item };
    return sortWorkbenchItems(next);
  }
  return sortWorkbenchItems([item, ...prev]);
}

function reconcilePersistedWorkbench(
  prev: WorkbenchItem[],
  rawItems: RawWorkbenchItem[],
): WorkbenchItem[] {
  const byId = new Map(prev.map((item) => [item.id, item]));
  const next = rawItems.map((raw) => {
    const item = normalizePersistedWorkbench(raw);
    const existing = byId.get(item.id);
    return existing ? { ...existing, ...item } : item;
  });
  return sortWorkbenchItems(next);
}

interface SessionSurfaceEventsOptions {
  activeSessionId: string;
  loadPersistedCards: () => Promise<void>;
  loadWorkbench: () => Promise<void>;
  refreshSessions: () => Promise<SessionSummary[] | null>;
  setSessions: (sessions: SessionSummary[]) => void;
}

function parseUiEventPayload(raw: string): Record<string, unknown> | null {
  try {
    const payload = JSON.parse(raw) as Record<string, unknown>;
    return payload && typeof payload === "object" ? payload : null;
  } catch {
    return null;
  }
}

function handleSessionSurfaceEvent(
  payload: Record<string, unknown>,
  options: Pick<
    SessionSurfaceEventsOptions,
    "loadPersistedCards" | "loadWorkbench" | "refreshSessions" | "setSessions"
  >,
): void {
  if (typeof payload.type !== "string") return;
  const { loadPersistedCards, loadWorkbench, refreshSessions, setSessions } = options;

  switch (payload.type) {
    case "cards.changed":
      loadPersistedCards().catch(() => {});
      return;
    case "workbench.changed":
      loadWorkbench().catch(() => {});
      return;
    case "sessions.changed":
      if (Array.isArray(payload.sessions)) {
        setSessions(payload.sessions as SessionSummary[]);
        return;
      }
      refreshSessions().catch(() => {});
      return;
    case "inbox.changed":
      window.dispatchEvent(
        new CustomEvent(INBOX_REFRESH_EVENT, {
          detail: { items: Array.isArray(payload.items) ? payload.items : undefined },
        }),
      );
      return;
    default:
      return;
  }
}

export function useSessionSurfaceEvents({
  activeSessionId,
  loadPersistedCards,
  loadWorkbench,
  refreshSessions,
  setSessions,
}: SessionSurfaceEventsOptions) {
  useEffect(() => {
    loadPersistedCards().catch(() => {});
    loadWorkbench().catch(() => {});
    refreshSessions().catch(() => {});

    let fallbackPollId: number | null = null;
    let source: EventSource | null = null;

    const connectEvents = () => {
      const params = new URLSearchParams({ chat_id: activeSessionId });
      const url = BACKEND_URL ? `${BACKEND_URL}/events?${params}` : `/events?${params}`;
      source = new EventSource(url);
      source.onopen = () => {
        loadPersistedCards().catch(() => {});
        loadWorkbench().catch(() => {});
        refreshSessions().catch(() => {});
      };
      source.onmessage = (event) => {
        const payload = parseUiEventPayload(event.data);
        if (!payload) return;
        handleSessionSurfaceEvent(payload, {
          loadPersistedCards,
          loadWorkbench,
          refreshSessions,
          setSessions,
        });
      };
    };

    if (typeof EventSource === "function") {
      connectEvents();
    } else {
      fallbackPollId = window.setInterval(() => {
        loadPersistedCards().catch(() => {});
        loadWorkbench().catch(() => {});
        refreshSessions().catch(() => {});
      }, 5000);
    }

    const onVisible = () => {
      if (document.visibilityState === "visible") {
        loadPersistedCards().catch(() => {});
        loadWorkbench().catch(() => {});
        refreshSessions().catch(() => {});
      }
    };
    const onCardsRefresh = () => {
      loadPersistedCards().catch(() => {});
    };
    const onWorkbenchRefresh = () => {
      loadWorkbench().catch(() => {});
    };
    window.addEventListener("focus", onVisible);
    window.addEventListener("nanobot:cards-refresh", onCardsRefresh);
    window.addEventListener("nanobot:workbench-refresh", onWorkbenchRefresh);
    document.addEventListener("visibilitychange", onVisible);
    return () => {
      if (fallbackPollId !== null) window.clearInterval(fallbackPollId);
      source?.close();
      window.removeEventListener("focus", onVisible);
      window.removeEventListener("nanobot:cards-refresh", onCardsRefresh);
      window.removeEventListener("nanobot:workbench-refresh", onWorkbenchRefresh);
      document.removeEventListener("visibilitychange", onVisible);
    };
  }, [activeSessionId, loadPersistedCards, loadWorkbench, refreshSessions, setSessions]);
}

export function useCardsState(activeSessionIdRef: { current: string }) {
  const [cards, setCards] = useState<CardItem[]>([]);

  const upsertCard = useCallback((item: Omit<CardItem, "id">) => {
    setCards((prev) => mergeCardItem(prev, item));
  }, []);

  const dismissCard = useCallback((id: number) => {
    setCards((prev) => {
      const card = prev.find((entry) => entry.id === id);
      if (card?.serverId) {
        const url = BACKEND_URL
          ? `${BACKEND_URL}/cards/${card.serverId}`
          : `/cards/${card.serverId}`;
        fetch(url, { method: "DELETE" }).catch(() => {});
      }
      return prev.filter((entry) => entry.id !== id);
    });
  }, []);

  const loadPersistedCards = useCallback(async () => {
    try {
      const rawCards = await fetchPersistedCardsFromBackend(activeSessionIdRef.current);
      if (!rawCards) return;
      setCards((prev) => reconcilePersistedCards(prev, rawCards));
    } catch (err) {
      console.warn("[cards] failed to load persisted cards", err);
    }
  }, [activeSessionIdRef]);

  const clearCards = useCallback(() => {
    setCards([]);
  }, []);

  useEffect(() => {
    const onCardLiveContentChange = (event: Event) => {
      const customEvent = event as CustomEvent<{ cardId?: unknown }>;
      const cardId =
        customEvent.detail && typeof customEvent.detail.cardId === "string"
          ? customEvent.detail.cardId
          : "";
      setCards((prev) => {
        if (prev.length === 0) return prev;
        if (cardId && !prev.some((card) => card.serverId === cardId)) return prev;
        return sortCards([...prev]);
      });
    };

    window.addEventListener(CARD_LIVE_CONTENT_EVENT, onCardLiveContentChange);
    return () => {
      window.removeEventListener(CARD_LIVE_CONTENT_EVENT, onCardLiveContentChange);
    };
  }, []);

  return { cards, upsertCard, dismissCard, loadPersistedCards, clearCards };
}

export function useWorkbenchState(activeSessionIdRef: { current: string }) {
  const [workbenchItems, setWorkbenchItems] = useState<WorkbenchItem[]>([]);

  const upsertWorkbench = useCallback((item: WorkbenchItem) => {
    setWorkbenchItems((prev) => mergeWorkbenchItem(prev, item));
  }, []);

  const dismissWorkbenchItem = useCallback(
    async (id: string) => {
      const key = id.trim();
      if (!key) return;
      const chatId = activeSessionIdRef.current;
      const params = new URLSearchParams({ chat_id: chatId });
      const url = BACKEND_URL
        ? `${BACKEND_URL}/workbench/${encodeURIComponent(key)}?${params}`
        : `/workbench/${encodeURIComponent(key)}?${params}`;
      await fetch(url, { method: "DELETE" }).catch(() => {});
      setWorkbenchItems((prev) => prev.filter((item) => item.id !== key));
    },
    [activeSessionIdRef],
  );

  const promoteWorkbenchItem = useCallback(
    async (id: string) => {
      const key = id.trim();
      if (!key) return;
      const chatId = activeSessionIdRef.current;
      const url = BACKEND_URL
        ? `${BACKEND_URL}/workbench/${encodeURIComponent(key)}/promote`
        : `/workbench/${encodeURIComponent(key)}/promote`;
      const resp = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ chat_id: chatId }),
      });
      if (!resp.ok) throw new Error(`promote failed (${resp.status})`);
      setWorkbenchItems((prev) => prev.filter((item) => item.id !== key));
      window.dispatchEvent(new Event("nanobot:cards-refresh"));
    },
    [activeSessionIdRef],
  );

  const loadWorkbench = useCallback(async () => {
    try {
      const rawItems = await fetchWorkbenchFromBackend(activeSessionIdRef.current);
      if (!rawItems) return;
      setWorkbenchItems((prev) => reconcilePersistedWorkbench(prev, rawItems));
    } catch (err) {
      console.warn("[workbench] failed to load workbench items", err);
    }
  }, [activeSessionIdRef]);

  const clearWorkbench = useCallback(() => {
    setWorkbenchItems([]);
  }, []);

  return {
    workbenchItems,
    upsertWorkbench,
    dismissWorkbenchItem,
    promoteWorkbenchItem,
    loadWorkbench,
    clearWorkbench,
  };
}
