import { useCallback, 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,
    goToParticle,
  } = useStreamPlayback(streamParticle, path);

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

  // Reset progress whenever the active particle changes.
  const [prevParticleId, setPrevParticleId] = useState(currentParticle?.id);
  if (currentParticle?.id !== prevParticleId) {
    setPrevParticleId(currentParticle?.id);
    setProgress(0);
  }

  // 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');

  // Suspend playback whenever another screen (Huddle, NewStream, modals
  // routed as screens) is on top. Native stack keeps StreamView mounted, so
  // without this the stream would keep advancing — and the exit countdown
  // would fire — behind the huddle.
  const isFocused = useIsFocused();
  useSuspendPlayback(!isFocused, 'screen-unfocused');

  // 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), []);

  // When the local user sends a particle while sitting on the last one, follow
  // it forward so they stay caught up and the stream isn't left unseen. If they
  // were browsing earlier particles, leave their position untouched. `children`
  // is still the pre-send snapshot here (the live query hasn't delivered the new
  // particle yet), so the last slot is children.length - 1; goToParticle resolves
  // once it arrives. Mirrors desktop's StreamView.handleParticleCreated.
  const handleParticleCreated = useCallback(
    (particleId: string) => {
      if (currentIndex === -1) return;
      if (currentIndex === children.length - 1) {
        goToParticle(particleId);
      }
    },
    [children.length, currentIndex, goToParticle],
  );

  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 ---
  // `paused` already reflects unfocused state via the "screen-unfocused"
  // suspender registered above, so the countdown freezes under the huddle.
  const exitRemainingMs = useExitCountdown(status, paused, 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}
          onParticleCreated={handleParticleCreated}
        />

        {/* 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>
  );
}
