import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { AgentIndicator } from "./components/AgentIndicator";
import { CardFeed } from "./components/CardFeed";
import { ControlBar, VoiceStatus } from "./components/Controls";
import { LogPanel } from "./components/LogPanel";
import { useAudioMeter } from "./hooks/useAudioMeter";
import { usePTT } from "./hooks/usePTT";
import { useWebRTC } from "./hooks/useWebRTC";
import type { CardItem, CardMessageMetadata, CardSelectionRange, JsonValue } from "./types";

const SWIPE_THRESHOLD_PX = 64;
const SWIPE_DIRECTION_RATIO = 1.15;
const CARD_SELECTION_EVENT = "nanobot:card-selection-change";

interface AppRtcActions {
  connect(): Promise<void>;
  sendJson(
    msg:
      | { type: "command"; command: string }
      | { type: "card-response"; card_id: string; value: string }
      | { type: "voice-ptt"; pressed: boolean; metadata?: CardMessageMetadata },
  ): void;
  setTextOnly(enabled: boolean): void;
  sendTextMessage(text: string, metadata?: CardMessageMetadata): Promise<void>;
  connected: boolean;
  connecting: boolean;
}

function toNullableNumber(value: JsonValue | undefined): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function readCardSelection(cardId: string | null | undefined): CardSelectionRange | null {
  const raw = window.__nanobotGetCardSelection?.(cardId);
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
  const record = raw as Record<string, JsonValue>;
  if (record.kind !== "git_diff_range") return null;
  if (typeof record.file_label !== "string" || typeof record.range_label !== "string") return null;

  const filePath = typeof record.file_path === "string" ? record.file_path : record.file_label;
  const label =
    typeof record.label === "string"
      ? record.label
      : `${record.file_label} · ${record.range_label}`;

  return {
    kind: "git_diff_range",
    file_path: filePath,
    file_label: record.file_label,
    range_label: record.range_label,
    label,
    old_start: toNullableNumber(record.old_start),
    old_end: toNullableNumber(record.old_end),
    new_start: toNullableNumber(record.new_start),
    new_end: toNullableNumber(record.new_end),
  };
}

function buildCardMetadata(card: CardItem): CardMessageMetadata {
  const metadata: CardMessageMetadata = {
    card_id: card.serverId,
    card_slot: card.slot,
    card_title: card.title,
    card_lane: card.lane,
    card_template_key: card.templateKey,
    card_context_summary: card.contextSummary,
    card_response_value: card.responseValue,
  };
  const selection = card.serverId ? readCardSelection(card.serverId) : null;
  if (selection) {
    metadata.card_selection = selection as unknown as JsonValue;
    metadata.card_selection_label = selection.label;
  }
  const liveContent = card.serverId
    ? window.__nanobotGetCardLiveContent?.(card.serverId)
    : undefined;
  if (liveContent !== undefined) metadata.card_live_content = liveContent as JsonValue;
  return metadata;
}

function AgentCardContext({
  card,
  selection,
  onClear,
}: {
  card: CardItem;
  selection: CardSelectionRange | null;
  onClear(): void;
}) {
  const label = selection ? "Using diff context" : "Using card";
  const title = selection?.file_label || card.title;
  const meta = selection?.range_label || "";

  return (
    <div id="agent-card-context" data-no-swipe="1">
      <div class="agent-card-context-label">{label}</div>
      <div class="agent-card-context-row">
        <div class="agent-card-context-main">
          <div class="agent-card-context-title">{title}</div>
          {meta && <div class="agent-card-context-meta">{meta}</div>}
        </div>
        <button
          class="agent-card-context-clear"
          type="button"
          aria-label="Clear selected card context"
          onClick={(e) => {
            e.preventDefault();
            e.stopPropagation();
            onClear();
          }}
        >
          Clear
        </button>
      </div>
    </div>
  );
}

function useSwipeHandlers(
  composing: boolean,
  view: "agent" | "feed",
  setView: (view: "agent" | "feed") => void,
  isInteractiveTarget: (target: EventTarget | null) => boolean,
) {
  const swipeStartRef = useRef<{ x: number; y: number } | null>(null);

  const onSwipeStart = useCallback(
    (e: Event) => {
      const pe = e as PointerEvent;
      if (composing) return;
      if (pe.pointerType === "mouse" && pe.button !== 0) return;
      if (isInteractiveTarget(pe.target)) return;
      swipeStartRef.current = { x: pe.clientX, y: pe.clientY };
    },
    [composing, isInteractiveTarget],
  );

  const onSwipeEnd = useCallback(
    (e: Event) => {
      const pe = e as PointerEvent;
      const start = swipeStartRef.current;
      swipeStartRef.current = null;
      if (!start || composing) return;
      const dx = pe.clientX - start.x;
      const dy = pe.clientY - start.y;
      if (Math.abs(dx) < SWIPE_THRESHOLD_PX) return;
      if (Math.abs(dx) < Math.abs(dy) * SWIPE_DIRECTION_RATIO) return;
      if (view === "agent" && dx < 0) setView("feed");
      if (view === "feed" && dx > 0) setView("agent");
    },
    [composing, view, setView],
  );

  return { onSwipeStart, onSwipeEnd };
}

function useCardActions(
  setView: (view: "agent" | "feed") => void,
  setSelectedCardId: (cardId: string | null) => void,
) {
  const handleAskCard = useCallback(
    (card: CardItem) => {
      if (!card.serverId) return;
      setSelectedCardId(card.serverId);
      setView("agent");
    },
    [setSelectedCardId, setView],
  );

  return { handleAskCard };
}

function useControlActions(rtc: AppRtcActions) {
  const handleReset = useCallback(async () => {
    const confirmed = window.confirm("Clear the current conversation context and start fresh?");
    if (!confirmed) return;
    await rtc.connect();
    rtc.sendJson({ type: "command", command: "reset" });
  }, [rtc]);

  const handleToggleTextOnly = useCallback(
    async (enabled: boolean) => {
      rtc.setTextOnly(enabled);
      if (enabled && !rtc.connected && !rtc.connecting) await rtc.connect();
    },
    [rtc],
  );

  return { handleReset, handleToggleTextOnly };
}

export function App() {
  const rtc = useWebRTC();
  const remoteAudioLevel = useAudioMeter(rtc.remoteStream);
  const audioLevel = rtc.textOnly ? 0 : remoteAudioLevel;

  const [view, setView] = useState<"agent" | "feed">("agent");
  const [composing, setComposing] = useState(false);
  const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
  const [selectionVersion, setSelectionVersion] = useState(0);
  const autoOpenedFeedRef = useRef(false);

  const selectedCard = useMemo(
    () =>
      selectedCardId ? (rtc.cards.find((card) => card.serverId === selectedCardId) ?? null) : null,
    [rtc.cards, selectedCardId],
  );
  const selectedCardSelection = useMemo(
    () => (selectedCardId ? readCardSelection(selectedCardId) : null),
    [selectedCardId, selectionVersion],
  );
  const selectedCardMetadata = useCallback(
    () => (selectedCard ? buildCardMetadata(selectedCard) : undefined),
    [selectedCard, selectionVersion],
  );

  const { agentStateOverride, handlePointerDown, handlePointerMove, handlePointerUp } = usePTT({
    connected: rtc.connected && !rtc.textOnly,
    onSendPtt: (pressed) =>
      rtc.sendJson({ type: "voice-ptt", pressed, metadata: selectedCardMetadata() }),
    onBootstrap: rtc.connect,
  });
  const effectiveAgentState = agentStateOverride ?? rtc.agentState;

  const isInteractiveTarget = useCallback((target: EventTarget | null): boolean => {
    if (!(target instanceof Element)) return false;
    return Boolean(target.closest("button,textarea,input,a,[data-no-swipe='1']"));
  }, []);
  const { onSwipeStart, onSwipeEnd } = useSwipeHandlers(
    composing,
    view,
    setView,
    isInteractiveTarget,
  );

  useEffect(() => {
    document.addEventListener("pointerdown", handlePointerDown, { passive: false });
    document.addEventListener("pointermove", handlePointerMove, { passive: true });
    document.addEventListener("pointerup", handlePointerUp, { passive: false });
    document.addEventListener("pointercancel", handlePointerUp, { passive: false });
    return () => {
      document.removeEventListener("pointerdown", handlePointerDown);
      document.removeEventListener("pointermove", handlePointerMove);
      document.removeEventListener("pointerup", handlePointerUp);
      document.removeEventListener("pointercancel", handlePointerUp);
    };
  }, [handlePointerDown, handlePointerMove, handlePointerUp]);

  useEffect(() => {
    if (autoOpenedFeedRef.current || rtc.cards.length === 0) return;
    autoOpenedFeedRef.current = true;
    setView("feed");
  }, [rtc.cards.length]);

  useEffect(() => {
    if (!selectedCardId) return;
    if (rtc.cards.some((card) => card.serverId === selectedCardId)) return;
    setSelectedCardId(null);
  }, [rtc.cards, selectedCardId]);

  useEffect(() => {
    const handleSelectionChange = (event: Event) => {
      const detail = (event as CustomEvent<{ cardId?: string; selection?: JsonValue | null }>)
        .detail;
      setSelectionVersion((current) => current + 1);
      const cardId = typeof detail?.cardId === "string" ? detail.cardId : "";
      if (cardId && detail?.selection) setSelectedCardId(cardId);
    };

    window.addEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
    return () => {
      window.removeEventListener(CARD_SELECTION_EVENT, handleSelectionChange as EventListener);
    };
  }, []);

  const { handleToggleTextOnly } = useControlActions(rtc);
  const { handleAskCard } = useCardActions(setView, setSelectedCardId);

  const clearSelectedCardContext = useCallback(() => {
    if (selectedCardId) window.__nanobotClearCardSelection?.(selectedCardId);
    setSelectedCardId(null);
  }, [selectedCardId]);

  const handleCardChoice = useCallback(
    (cardId: string, value: string) => {
      rtc.sendJson({ type: "card-response", card_id: cardId, value });
    },
    [rtc],
  );
  const handleSendMessage = useCallback(
    async (text: string) => {
      await rtc.sendTextMessage(text, selectedCardMetadata());
    },
    [rtc, selectedCardMetadata],
  );
  const handleResetWithSelection = useCallback(async () => {
    const confirmed = window.confirm("Clear the current conversation context and start fresh?");
    if (!confirmed) return;
    if (selectedCardId) window.__nanobotClearCardSelection?.(selectedCardId);
    setSelectedCardId(null);
    await rtc.connect();
    rtc.sendJson({ type: "command", command: "reset" });
  }, [rtc, selectedCardId]);

  return (
    <>
      <div id="swipe-shell" onPointerDown={onSwipeStart} onPointerUp={onSwipeEnd}>
        <div id="swipe-track" class={view === "feed" ? "feed-active" : ""}>
          <section class="workspace-panel workspace-agent">
            {view === "agent" && (
              <ControlBar
                onReset={handleResetWithSelection}
                textOnly={rtc.textOnly}
                onToggleTextOnly={handleToggleTextOnly}
              />
            )}
            {view === "agent" && selectedCard && (
              <AgentCardContext
                card={selectedCard}
                selection={selectedCardSelection}
                onClear={clearSelectedCardContext}
              />
            )}
            {view === "agent" && (
              <LogPanel
                lines={rtc.logLines}
                disabled={!rtc.connected}
                onSendMessage={handleSendMessage}
                onExpandChange={setComposing}
              />
            )}
            <AgentIndicator
              state={effectiveAgentState}
              connected={rtc.connected}
              connecting={rtc.connecting}
              audioLevel={audioLevel}
              viewActive
              onPointerDown={() => {}}
              onPointerUp={() => {}}
            />
          </section>
          <section class="workspace-panel workspace-feed">
            <CardFeed
              cards={rtc.cards}
              viewActive
              onDismiss={rtc.dismissCard}
              onChoice={handleCardChoice}
              onAskCard={handleAskCard}
            />
          </section>
        </div>
      </div>
      <VoiceStatus text={rtc.voiceStatus} visible={rtc.statusVisible} />
    </>
  );
}
