import { useCallback, useEffect, useState } from "react";
import { Alert, Dimensions, Pressable, Text, View } from "react-native";
import { useIsFocused } from "@react-navigation/native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { StatusBar } from "expo-status-bar";
import * as Haptics from "expo-haptics";
import { ChevronDown } from "lucide-react-native";
import {
  Gesture,
  GestureDetector,
} from "react-native-gesture-handler";
import Animated, {
  Extrapolation,
  interpolate,
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
import { isParticleDeleted, type Particle } from "@/api/types";
import {
  parseParticlePath,
  particlePath,
  toFirestoreDocPath,
  type ParticlePath,
} from "@/lib/particle-path";
import {
  softDeleteParticle,
  toggleParticleReaction,
  updateStreamStatus,
} from "@/lib/firestore-particles";
import { toast } from "sonner-native";
import { toUserMessage } from "@/lib/errors";
import { useNetwork } from "@/hooks/use-networks";
import { useStreamPlayback } from "@/hooks/use-stream-playback";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
import {
  selectIsComposing,
  selectIsPaused,
  usePlaybackPauseStore,
} from "@/stores/playback-pause-store";
import { useAuthStore } from "@/stores/auth-store";
import { ComposeDock } from "@/features/compose/ComposeDock";
import { ComposingIndicator } from "@/components/ComposingIndicator";
import { PlaybackPageIndicator } from "./PlaybackPageIndicator";
import { ReactionSheet } from "./ReactionSheet";
import { StreamMetadataHeader } from "./StreamMetadataHeader";
import { StreamSafeAreaProvider } from "./stream-safe-area";
import {
  StreamPresenceProvider,
  useStreamComposing,
} from "./stream-presence-context";
import { TextParticleView } from "./TextParticleView";
import { MediaParticleView } from "./MediaParticleView";
import { DeletedParticleView } from "./DeletedParticleView";
import { FallbackParticleView } from "./FallbackParticleView";
import { useExitCountdown } from "./use-exit-countdown";
import { StreamTopActions } from "./StreamTopActions";
import { StreamActionsSheet, type StreamActionId } from "./StreamActionsSheet";
import { StreamMembersSheet } from "./StreamMembersSheet";
import { RenameStreamSheet } from "./RenameStreamSheet";
import { EditParticleSheet } from "./EditParticleSheet";
import { ReactionStack } from "./ReactionStack";

const SCREEN_HEIGHT = Dimensions.get("window").height;
// Tap-zone split: left 28% goes back, right 72% goes forward — matching the
// asymmetric "Snapchat thumb-zone" so right-handed taps default to forward.
const PREV_ZONE_RATIO = 0.28;
// Swipe-down dismiss commit thresholds — either move 1/4 of the screen, or
// flick downward fast enough.
const DISMISS_DISTANCE = SCREEN_HEIGHT * 0.25;
const DISMISS_VELOCITY = 900;
// Swipe-up reactions commit thresholds — flick up ~80px or with enough velocity.
const REACTIONS_DISTANCE = 80;
const REACTIONS_VELOCITY = 600;
// Approx height of the ComposeDock from the screen bottom (record button stack
// + pb-10). Status pills sit just above this so they aren't hidden behind it.
const COMPOSE_DOCK_HEIGHT = 50;

interface StreamViewProps {
  streamParticle: Particle & { type: "stream" };
  path: ParticlePath;
  onExit: () => void;
}

export function StreamView(props: StreamViewProps) {
  const { networkId } = parseParticlePath(props.path);
  // The presence provider wraps the inner view so any descendant can broadcast
  // composing state without re-deriving the channel id.
  return (
    <StreamPresenceProvider
      networkId={networkId}
      streamId={props.streamParticle.id}
    >
      <StreamViewInner {...props} />
    </StreamPresenceProvider>
  );
}

function StreamViewInner({ streamParticle, path, onExit }: StreamViewProps) {
  const { networkId } = parseParticlePath(path);
  const network = useNetwork(networkId);
  const insets = useSafeAreaInsets();
  const { composingUsers } = useStreamComposing();

  const { children, currentParticle, currentIndex, status, next, prev } =
    useStreamPlayback(streamParticle, path);

  const paused = usePlaybackPauseStore(selectIsPaused);
  const composing = usePlaybackPauseStore(selectIsComposing);
  const [progress, setProgress] = useState(0);
  const userId = useAuthStore((s) => s.user?.id) ?? "";

  // Local hold state drives the "touch-hold" pause suspender. We wrap the JS
  // setter inside a runOnJS callback dispatched from the worklet thread.
  const [holdActive, setHoldActive] = useState(false);
  useSuspendPlayback(holdActive, "touch-hold");

  // Reaction sheet — opens via swipe-up on the canvas.
  const [reactionsOpen, setReactionsOpen] = useState(false);

  // Top-right cluster sheet state. `videoFit` lets the user toggle expo-video's
  // contentFit for the active media particle when desktop captures of unusual
  // aspect ratios get cropped uncomfortably under the default `cover` mode.
  const [actionsOpen, setActionsOpen] = useState(false);
  const [membersOpen, setMembersOpen] = useState(false);
  const [renameOpen, setRenameOpen] = useState(false);
  const [editOpen, setEditOpen] = useState(false);
  const [videoFit, setVideoFit] = useState<"cover" | "contain">("cover");

  const isCreator = !!userId && userId === streamParticle.created_by_human_id;
  const canDeleteCurrentParticle =
    !!currentParticle &&
    !!userId &&
    currentParticle.created_by_human_id === userId &&
    currentParticle.type !== "stream" &&
    currentParticle.type !== "folder" &&
    !isParticleDeleted(currentParticle);
  const canEditCurrentParticle =
    !!currentParticle &&
    !!userId &&
    currentParticle.created_by_human_id === userId &&
    currentParticle.type === "text" &&
    !isParticleDeleted(currentParticle);
  const editableTextParticle =
    canEditCurrentParticle && currentParticle && currentParticle.type === "text"
      ? currentParticle
      : null;

  const showFitToggle =
    !!currentParticle &&
    !isParticleDeleted(currentParticle) &&
    currentParticle.type === "media" &&
    !currentParticle.properties.mime_type.startsWith("audio/");

  const handleStreamAction = useCallback(
    async (action: StreamActionId) => {
      const streamDocPath = toFirestoreDocPath(
        particlePath(networkId, [streamParticle.id]),
      );
      switch (action) {
        case "toggle-status": {
          try {
            await updateStreamStatus(
              streamDocPath,
              streamParticle.status === "open" ? "closed" : "open",
            );
          } catch (err) {
            toast.error(toUserMessage(err));
          }
          return;
        }
        case "rename":
          setRenameOpen(true);
          return;
        case "members":
          setMembersOpen(true);
          return;
        case "edit-particle":
          if (!canEditCurrentParticle) return;
          setEditOpen(true);
          return;
        case "delete-particle": {
          if (!currentParticle || !userId) return;
          if (!canDeleteCurrentParticle) return;
          Alert.alert(
            "Delete this particle?",
            "This cannot be undone. Other viewers will see a \"deleted\" message in its place.",
            [
              { text: "Cancel", style: "cancel" },
              {
                text: "Delete",
                style: "destructive",
                onPress: async () => {
                  try {
                    const docPath = toFirestoreDocPath(
                      particlePath(networkId, [
                        streamParticle.id,
                        currentParticle.id,
                      ]),
                    );
                    await softDeleteParticle(docPath, userId);
                  } catch (err) {
                    toast.error(toUserMessage(err));
                  }
                },
              },
            ],
          );
          return;
        }
      }
    },
    [
      networkId,
      streamParticle.id,
      streamParticle.status,
      currentParticle,
      userId,
      canDeleteCurrentParticle,
      canEditCurrentParticle,
    ],
  );

  const reactionsOnCurrent =
    currentParticle && !isParticleDeleted(currentParticle)
      ? currentParticle.type === "media" || currentParticle.type === "text"
        ? currentParticle.reactions
        : undefined
      : undefined;

  const handleToggleReaction = useCallback(
    (key: string) => {
      if (!userId || !currentParticle) return;
      if (isParticleDeleted(currentParticle)) return;
      const docPath = toFirestoreDocPath(
        particlePath(networkId, [streamParticle.id, currentParticle.id]),
      );
      void toggleParticleReaction(
        docPath,
        key,
        userId,
        reactionsOnCurrent,
      );
    },
    [userId, currentParticle, networkId, streamParticle.id, reactionsOnCurrent],
  );

  const openReactions = useCallback(() => setReactionsOpen(true), []);

  // Reset progress whenever the active particle changes.
  useEffect(() => {
    setProgress(0);
  }, [currentParticle?.id]);

  const handleTap = useCallback(
    (xRatio: number) => {
      if (xRatio < PREV_ZONE_RATIO) {
        if (currentIndex <= 0) {
          // Soft "thud" — nothing to go back to.
          void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
          return;
        }
        prev();
      } else {
        next();
      }
    },
    [currentIndex, next, prev],
  );

  // --- Swipe-down dismiss ---
  const translateY = useSharedValue(0);
  const screenWidth = Dimensions.get("window").width;

  const exit = useCallback(() => {
    onExit();
  }, [onExit]);

  const panDown = Gesture.Pan()
    .activeOffsetY(15)
    .failOffsetX([-30, 30])
    .failOffsetY(-20)
    .onUpdate((e) => {
      "worklet";
      translateY.value = Math.max(0, e.translationY);
    })
    .onEnd((e) => {
      "worklet";
      if (
        e.translationY > DISMISS_DISTANCE ||
        e.velocityY > DISMISS_VELOCITY
      ) {
        translateY.value = withTiming(SCREEN_HEIGHT, { duration: 220 });
        runOnJS(exit)();
      } else {
        translateY.value = withSpring(0, {
          damping: 22,
          stiffness: 220,
          mass: 0.6,
        });
      }
    });

  // Swipe-up opens the reaction sheet. Mirror the down pan's discipline —
  // fail on horizontal motion so it doesn't fight the tap-zones.
  const panUp = Gesture.Pan()
    .activeOffsetY(-15)
    .failOffsetX([-30, 30])
    .failOffsetY(20)
    .onEnd((e) => {
      "worklet";
      if (
        e.translationY < -REACTIONS_DISTANCE ||
        e.velocityY < -REACTIONS_VELOCITY
      ) {
        runOnJS(openReactions)();
      }
    });

  // --- Tap (advance / regress) ---
  const tap = Gesture.Tap()
    .maxDuration(180)
    .maxDistance(15)
    .onEnd((e, success) => {
      "worklet";
      if (!success) return;
      const ratio = e.x / screenWidth;
      runOnJS(handleTap)(ratio);
    });

  // --- Long-press (hold-to-pause) ---
  const longPress = Gesture.LongPress()
    .minDuration(180)
    .maxDistance(15)
    .onStart(() => {
      "worklet";
      runOnJS(setHoldActive)(true);
    })
    .onTouchesUp(() => {
      "worklet";
      runOnJS(setHoldActive)(false);
    })
    .onFinalize(() => {
      "worklet";
      runOnJS(setHoldActive)(false);
    });

  // Pan-down (dismiss), pan-up (reactions), and tap+longPress race against
  // each other. The first to clear its activeOffsetY wins; the others fail.
  const composed = Gesture.Race(
    panDown,
    panUp,
    Gesture.Simultaneous(tap, longPress),
  );

  const containerStyle = useAnimatedStyle(() => {
    const opacity = interpolate(
      translateY.value,
      [0, SCREEN_HEIGHT * 0.5],
      [1, 0.4],
      Extrapolation.CLAMP,
    );
    const scale = interpolate(
      translateY.value,
      [0, SCREEN_HEIGHT],
      [1, 0.85],
      Extrapolation.CLAMP,
    );
    return {
      transform: [{ translateY: translateY.value }, { scale }],
      opacity,
    };
  });

  const backdropStyle = useAnimatedStyle(() => {
    const opacity = interpolate(
      translateY.value,
      [0, SCREEN_HEIGHT * 0.5],
      [1, 0.6],
      Extrapolation.CLAMP,
    );
    return { opacity };
  });

  // --- End-of-stream countdown ---
  // Pause the countdown when this screen isn't focused (e.g. a Huddle screen
  // is mounted on top). Otherwise the timer keeps ticking under the huddle
  // and goBack() pops the huddle out from under the user.
  const isFocused = useIsFocused();
  const exitRemainingMs = useExitCountdown(status, paused || !isFocused, exit);

  // Chrome reservations: top = safe-area + segmented bar (3) + gap (12) +
  // metadata row (~38) + breathing room (12). Bottom = safe-area + compose
  // dock + breathing room. Pause / countdown pills moved to the top so the
  // bottom only reserves space for the compose dock now.
  const chromeTop = insets.top + 65;
  const chromeBottom = insets.bottom + COMPOSE_DOCK_HEIGHT + 14;

  // --- Render the active particle ---
  const renderParticle = (particle: Particle) => {
    if (isParticleDeleted(particle)) {
      return (
        <DeletedParticleView
          key={particle.id}
          particle={particle}
          networkId={networkId}
          paused={paused}
          onEnded={next}
        />
      );
    }
    switch (particle.type) {
      case "text":
        return (
          <TextParticleView
            key={particle.id}
            particle={particle}
            paused={paused}
            onEnded={next}
            onProgress={setProgress}
          />
        );
      case "media":
        return (
          <MediaParticleView
            key={particle.id}
            particle={particle}
            paused={paused}
            onEnded={next}
            onProgress={setProgress}
            contentFit={videoFit}
          />
        );
      default:
        return (
          <FallbackParticleView
            key={particle.id}
            particle={particle}
            networkId={networkId}
            paused={paused}
            onEnded={next}
          />
        );
    }
  };

  // --- Content guards ---
  if (children.length === 0) {
    return (
      <View className="flex-1 bg-black items-center justify-center px-8">
        <StatusBar style="light" hidden />
        <Text className="text-white/70 text-base">
          No particles in this stream yet.
        </Text>
        <Pressable onPress={exit} className="mt-6 px-4 py-2">
          <Text className="text-white/60">Close</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <Animated.View style={[{ flex: 1 }, backdropStyle]} className="bg-black">
      <StatusBar style="light" hidden />
      <Animated.View style={[{ flex: 1 }, containerStyle]} className="bg-black">
        <GestureDetector gesture={composed}>
          <View className="flex-1">
            {/* Particle canvas — fills the whole screen, gesture-aware.
                StreamSafeAreaProvider tells particle views how much space the
                chrome occupies so scrollable content doesn't slip under. */}
            <StreamSafeAreaProvider top={chromeTop} bottom={chromeBottom}>
              <View className="flex-1">
                {/* While composing we fully unmount the particle so the
                    underlying expo-video player releases the AVAudioSession.
                    Otherwise it contends with expo-camera and crashes the
                    app when video recording starts. */}
                {currentParticle && !composing
                  ? renderParticle(currentParticle)
                  : null}
              </View>
            </StreamSafeAreaProvider>

            {/* Top chrome: segmented bar + metadata. Painted over the canvas
                so the canvas can be edge-to-edge but content gets a safe-area
                gradient to read against. A real linear gradient (vs a flat
                bg-black/40 block) avoids the hard "bar" edge under the chrome. */}
            <View
              pointerEvents="none"
              className="absolute inset-x-0 top-0"
              style={{ height: insets.top + 120 }}
            >
              <Svg width="100%" height="100%">
                <Defs>
                  <LinearGradient
                    id="streamTopFade"
                    x1="0"
                    y1="0"
                    x2="0"
                    y2="1"
                  >
                    <Stop offset="0" stopColor="#000000" stopOpacity="0.55" />
                    <Stop offset="1" stopColor="#000000" stopOpacity="0" />
                  </LinearGradient>
                </Defs>
                <Rect width="100%" height="100%" fill="url(#streamTopFade)" />
              </Svg>
            </View>
            <View
              pointerEvents="none"
              className="absolute inset-x-0"
              style={{ top: insets.top + 8 }}
            >
              <View className="px-3">
                <PlaybackPageIndicator
                  total={children.length}
                  current={currentIndex}
                  progress={progress}
                  paused={paused}
                />
              </View>
            </View>

            {/* Top status pills: paused + exit countdown. Anchored just below
                the metadata row (avatar + name ≈ 40px tall, starts at
                insets.top + 32) so they share the top chrome real estate
                instead of competing with captions at the bottom. */}
            <View
              pointerEvents="none"
              className="absolute inset-x-0 items-center"
              style={{ top: insets.top + 88 }}
            >
              {paused ? (
                <View className="bg-white/15 rounded-full px-3 py-1">
                  <Text className="text-white/90 text-xs font-medium">
                    Paused
                  </Text>
                </View>
              ) : null}
              {exitRemainingMs !== null ? (
                <View className="bg-white/15 rounded-full px-3 py-1 mt-2">
                  <Text className="text-white/90 text-xs font-medium">
                    Closing in {Math.ceil(exitRemainingMs / 1000)}s
                  </Text>
                </View>
              ) : null}
            </View>
          </View>
        </GestureDetector>

        {/* Top metadata + actions row — lifted OUTSIDE the GestureDetector so
            taps on the action cluster aren't claimed by the stream's tap
            gesture (which advances/regresses the playhead). The chain uses
            `box-none` so empty space still falls through to gestures below. */}
        <View
          pointerEvents="box-none"
          className="absolute inset-x-0"
          style={{ top: insets.top + 8 + 24 }}
        >
          <View className="px-4" pointerEvents="box-none">
            <View
              className="flex-row items-start gap-2"
              pointerEvents="box-none"
            >
              <Pressable
                onPress={exit}
                accessibilityLabel="Close stream"
                hitSlop={8}
                className="bg-white/10 active:bg-white/20 h-8 w-8 items-center justify-center rounded-full"
              >
                <ChevronDown color="white" size={18} strokeWidth={2} />
              </Pressable>
              <View className="flex-1" pointerEvents="none">
                <StreamMetadataHeader
                  particle={currentParticle}
                  network={network ?? null}
                />
              </View>
              <StreamTopActions
                networkId={networkId}
                streamParticle={streamParticle}
                humans={network?.humans ?? []}
                videoFit={videoFit}
                onToggleVideoFit={() =>
                  setVideoFit((v) => (v === "cover" ? "contain" : "cover"))
                }
                onOpenMembers={() => setMembersOpen(true)}
                onOpenActions={() => setActionsOpen(true)}
                showFitToggle={showFitToggle}
              />
            </View>
            {composingUsers.length > 0 ? (
              <View className="mt-2" pointerEvents="none">
                <ComposingIndicator
                  users={composingUsers}
                  networkHumans={network?.humans}
                />
              </View>
            ) : null}
          </View>
        </View>

        {/* Right-edge reaction stack — mirrors desktop's ReactionBar. Vertically
            centered on the canvas; outside the GestureDetector so each pill
            tap toggles cleanly without competing with the stream advance/back
            taps. Hidden during composing so the camera preview is unobstructed. */}
        {currentParticle &&
        !composing &&
        !isParticleDeleted(currentParticle) &&
        (currentParticle.type === "media" ||
          currentParticle.type === "text") ? (
          <View
            pointerEvents="box-none"
            className="absolute right-3"
            style={{
              top: insets.top + 100,
              bottom: insets.bottom + COMPOSE_DOCK_HEIGHT + 40,
              justifyContent: "center",
            }}
          >
            <ReactionStack
              reactions={reactionsOnCurrent}
              currentHumanId={userId}
              humans={network?.humans}
              onToggle={handleToggleReaction}
              onOpenSheet={openReactions}
            />
          </View>
        ) : null}

        {/* Safe-area sentinel for top notch — kept outside GestureDetector so
            iOS's status-bar tap doesn't fight our gestures. */}
        <SafeAreaView edges={["top"]} pointerEvents="none" />

        {/* Compose dock + recording overlays. Sits above the GestureDetector
            so its hold-FAB pan gesture isn't competed-with by the StreamView
            tap zones. */}
        <ComposeDock networkId={networkId} targetPath={path} />

        {/* Reaction sheet — slides up over everything, suspends playback
            internally while open. */}
        <ReactionSheet
          open={reactionsOpen}
          onClose={() => setReactionsOpen(false)}
          reactions={reactionsOnCurrent}
          currentHumanId={userId}
          humans={network?.humans}
          onToggle={handleToggleReaction}
        />

        <StreamActionsSheet
          open={actionsOpen}
          onClose={() => setActionsOpen(false)}
          onSelect={(action) => void handleStreamAction(action)}
          streamStatus={streamParticle.status ?? "open"}
          isCreator={isCreator}
          canEditParticle={canEditCurrentParticle}
          canDeleteParticle={canDeleteCurrentParticle}
        />

        <StreamMembersSheet
          open={membersOpen}
          onClose={() => setMembersOpen(false)}
          networkId={networkId}
          streamParticle={streamParticle}
          isCreator={isCreator}
        />

        <RenameStreamSheet
          open={renameOpen}
          onClose={() => setRenameOpen(false)}
          networkId={networkId}
          streamId={streamParticle.id}
          currentName={streamParticle.properties.name}
        />

        {editableTextParticle ? (
          <EditParticleSheet
            open={editOpen}
            onClose={() => setEditOpen(false)}
            networkId={networkId}
            streamId={streamParticle.id}
            particleId={editableTextParticle.id}
            currentContent={editableTextParticle.properties.content}
          />
        ) : null}
      </Animated.View>
    </Animated.View>
  );
}
