import { useCallback, useEffect, useRef, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { Mic, Type as TypeIcon, Video as VideoIcon } from "lucide-react-native";
import * as Haptics from "expo-haptics";
import { setAudioModeAsync } from "expo-audio";
import {
  useCameraPermissions,
  useMicrophonePermissions,
} from "expo-camera";
import {
  Gesture,
  GestureDetector,
} from "react-native-gesture-handler";
import Animated, {
  Easing,
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated";
import { toast } from "sonner-native";
import { cn } from "@/lib/utils";
import { useEvent } from "@/hooks/use-event";
import { logError } from "@/lib/errors";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
import { useAuthStore } from "@/stores/auth-store";
import {
  createTextParticle,
  uploadMediaParticle,
} from "@/lib/upload";
import type { ParticlePath } from "@/lib/particle-path";
import {
  useStreamComposingBroadcast,
  type ComposingMode,
} from "@/features/stream-view/stream-presence-context";
import { TextComposeModal } from "./TextComposeModal";
import { VideoRecordingOverlay } from "./VideoRecordingOverlay";
import { AudioRecordingOverlay } from "./AudioRecordingOverlay";
import { ReviewSheet } from "./ReviewSheet";

type RecordingMode = "video" | "audio";
type PendingAction = "commit" | "cancel" | null;

type ComposeUiState =
  | { kind: "idle" }
  | { kind: "warming-up"; mode: RecordingMode }
  | { kind: "recording"; mode: RecordingMode }
  | {
      kind: "review";
      mode: RecordingMode;
      uri: string;
      durationMs: number;
    }
  | { kind: "uploading" };

const CANCEL_THRESHOLD_PX = 120;
const SHORT_RELEASE_MS = 350;

interface SubmitMediaParams {
  fileUri: string;
  mimeType: string;
  durationMs: number;
  source: "camera" | "screen";
}

interface ComposeDockProps {
  networkId: string;
  /** Where new particles are created. Defaults to the current stream. */
  targetPath: ParticlePath;
  /**
   * When true, the dock won't broadcast composing state — used by the
   * new-stream screen where there's no presence channel to broadcast to.
   */
  silentPresence?: boolean;
  /**
   * Optional: replace the default media upload logic. NewStreamScreen uses
   * this to create the stream + first particle in one shot. The dock still
   * owns the review-sheet / haptic / state-machine flow; only the final
   * persistence step is swapped.
   */
  submitMedia?: (params: SubmitMediaParams) => Promise<void>;
  /** Optional: replace the default text-particle creation. */
  submitText?: (content: string) => Promise<void>;
}

/**
 * Bottom dock + recording orchestrator. The hold-to-record gesture lives on
 * the FAB and drives every state transition. `cancelProgress` is a worklet
 * shared value the overlays read for live cancel-pill animations. Playback
 * is suspended via the pause-store while the dock is anything but idle.
 */
export function ComposeDock({
  networkId,
  targetPath,
  silentPresence = false,
  submitMedia,
  submitText: submitTextOverride,
}: ComposeDockProps) {
  const userId = useAuthStore((s) => s.user?.id);

  const [mode, setMode] = useState<RecordingMode>("video");
  const [ui, setUi] = useState<ComposeUiState>({ kind: "idle" });
  const [textOpen, setTextOpen] = useState(false);
  // Both overlays read this prop to know how to finalize. It's set on release
  // and cleared as the overlay unmounts.
  const [pendingAction, setPendingAction] = useState<PendingAction>(null);

  const [camPerm, requestCamPerm] = useCameraPermissions();
  const [micPerm, requestMicPerm] = useMicrophonePermissions();

  // Suspend underlying playback whenever the dock isn't idle. Mirrors how
  // desktop's compose-overlay flips the playback-pause-store.
  const isComposing = ui.kind !== "idle" || textOpen;
  useSuspendPlayback(isComposing, "compose");

  // Broadcast composing state over Pusher so other viewers see the dot.
  // When silentPresence is set we skip — the dock renders outside a stream
  // (e.g. NewStreamScreen) where no presence channel exists yet.
  useComposingBroadcast({ ui, textOpen, silent: silentPresence });

  // Worklet-side flag the overlays read for live cancel UI.
  const cancelProgress = useSharedValue(0);
  // Press start time, kept on a ref so worklets can read it via runOnJS.
  const pressStartedAtRef = useRef<number | null>(null);

  const ensurePermissions = useCallback(
    async (forVideo: boolean): Promise<boolean> => {
      if (forVideo) {
        const cam = camPerm?.granted ? camPerm : await requestCamPerm();
        if (!cam.granted) {
          toast.error("Camera permission is required to record video.");
          return false;
        }
      }
      const mic = micPerm?.granted ? micPerm : await requestMicPerm();
      if (!mic.granted) {
        toast.error("Microphone permission is required to record.");
        return false;
      }
      return true;
    },
    [camPerm, micPerm, requestCamPerm, requestMicPerm],
  );

  // ----- Lifecycle handlers -----

  const beginRecording = useEvent(async () => {
    if (ui.kind !== "idle") return;
    pressStartedAtRef.current = Date.now();
    const ok = await ensurePermissions(mode === "video");
    if (!ok) {
      pressStartedAtRef.current = null;
      return;
    }
    // iOS requires the audio session to allow recording before either the
    // audio recorder or the camera's audio track will work.
    try {
      await setAudioModeAsync({
        allowsRecording: true,
        playsInSilentMode: true,
      });
    } catch (err) {
      logError(err, { scope: "compose.audio.setMode.record" });
    }
    void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
    setUi({ kind: "warming-up", mode });
  });

  // Restore the audio session to playback-only when we leave recording,
  // otherwise iOS keeps routing playback through the earpiece.
  const restoreAudioMode = useCallback(() => {
    setAudioModeAsync({
      allowsRecording: false,
      playsInSilentMode: true,
    }).catch((err) =>
      logError(err, { scope: "compose.audio.setMode.restore" }),
    );
  }, []);

  const handleRecordingStarted = useCallback(() => {
    setUi((prev) =>
      prev.kind === "warming-up"
        ? { kind: "recording", mode: prev.mode }
        : prev,
    );
  }, []);

  const handleRecordingComplete = useCallback(
    ({ uri, durationMs }: { uri: string; durationMs: number }) => {
      void Haptics.selectionAsync();
      setUi((prev) => {
        const m = "mode" in prev ? prev.mode : mode;
        return { kind: "review", mode: m, uri, durationMs };
      });
      setPendingAction(null);
      pressStartedAtRef.current = null;
      restoreAudioMode();
    },
    [mode, restoreAudioMode],
  );

  const handleRecordingCancel = useCallback(() => {
    setUi({ kind: "idle" });
    setPendingAction(null);
    pressStartedAtRef.current = null;
    cancelProgress.value = 0;
    restoreAudioMode();
  }, [cancelProgress, restoreAudioMode]);

  const finalizeFromGesture = useEvent(({ cancelled }: { cancelled: boolean }) => {
    const heldMs = pressStartedAtRef.current
      ? Date.now() - pressStartedAtRef.current
      : 0;

    // Recording never warmed up to "recording" — drop back to idle without
    // shipping anything (permission denied, or mount race).
    if (ui.kind === "warming-up") {
      setUi({ kind: "idle" });
      pressStartedAtRef.current = null;
      cancelProgress.value = 0;
      restoreAudioMode();
      return;
    }
    if (ui.kind !== "recording") return;

    const wantCancel = cancelled || heldMs < SHORT_RELEASE_MS;
    if (wantCancel) {
      void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    }
    setPendingAction(wantCancel ? "cancel" : "commit");
    cancelProgress.value = 0;
  });

  // ----- Gesture: hold-to-record on FAB -----

  const pan = Gesture.Pan()
    .minDistance(0)
    .activateAfterLongPress(120)
    .onStart(() => {
      "worklet";
      runOnJS(beginRecording)();
    })
    .onUpdate((e) => {
      "worklet";
      const upPx = Math.max(0, -e.translationY);
      cancelProgress.value = Math.min(upPx / CANCEL_THRESHOLD_PX, 1);
    })
    .onEnd((e) => {
      "worklet";
      const cancelled = e.translationY < -CANCEL_THRESHOLD_PX;
      runOnJS(finalizeFromGesture)({ cancelled });
    })
    .onFinalize(() => {
      "worklet";
      cancelProgress.value = 0;
    });

  // ----- Submit handlers -----

  const sendReview = useEvent(async () => {
    if (ui.kind !== "review" || !userId) return;
    const captured = ui;
    setUi({ kind: "uploading" });
    try {
      const mimeType =
        captured.mode === "audio" ? "audio/mp4" : "video/mp4";
      if (submitMedia) {
        await submitMedia({
          fileUri: captured.uri,
          mimeType,
          durationMs: captured.durationMs,
          source: "camera",
        });
      } else {
        await uploadMediaParticle({
          networkId,
          targetPath,
          fileUri: captured.uri,
          mimeType,
          durationMs: captured.durationMs,
          source: "camera",
          createdByHumanId: userId,
        });
      }
      void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
      setUi({ kind: "idle" });
    } catch (err) {
      void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
      // Restore the review sheet so the user can retry without losing their
      // take. The sheet's own toast already surfaced the error message.
      setUi(captured);
      throw err;
    }
  });

  const retake = useCallback(() => setUi({ kind: "idle" }), []);
  const cancelReview = useCallback(() => setUi({ kind: "idle" }), []);

  const submitText = useEvent(async (content: string) => {
    if (!userId) throw new Error("Not signed in.");
    if (submitTextOverride) {
      await submitTextOverride(content);
    } else {
      await createTextParticle({
        networkId,
        targetPath,
        content,
        createdByHumanId: userId,
      });
    }
    void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
  });

  // ----- FAB animations -----

  const fabScale = useSharedValue(1);
  useEffect(() => {
    const target = ui.kind === "recording" ? 1.18 : 1;
    fabScale.value = withTiming(target, {
      duration: 220,
      easing: Easing.out(Easing.cubic),
    });
  }, [ui.kind, fabScale]);

  const fabStyle = useAnimatedStyle(() => ({
    transform: [{ scale: fabScale.value }],
  }));

  const ringStyle = useAnimatedStyle(() => {
    const opacity = ui.kind === "recording" ? 1 : 0;
    const armed = cancelProgress.value > 0.5;
    return {
      opacity,
      borderColor: armed
        ? "rgba(239, 68, 68, 0.95)"
        : "rgba(255,255,255,0.95)",
    };
  });

  // ----- Render -----

  const dockHidden = ui.kind === "review" || ui.kind === "uploading";

  return (
    <>
      {!dockHidden ? (
        <View pointerEvents="box-none" className="absolute inset-x-0 bottom-0">
          <View
            pointerEvents="box-none"
            className="flex-row items-center justify-between px-8 pb-10"
          >
            <Pressable
              onPress={() =>
                setMode((m) => (m === "video" ? "audio" : "video"))
              }
              disabled={ui.kind !== "idle"}
              accessibilityLabel={`Switch to ${
                mode === "video" ? "audio" : "video"
              } mode`}
              className={cn(
                "h-11 w-11 items-center justify-center rounded-full bg-white/15",
                ui.kind !== "idle" && "opacity-40",
              )}
            >
              {mode === "video" ? (
                <VideoIcon color="white" size={20} strokeWidth={1.6} />
              ) : (
                <Mic color="white" size={20} strokeWidth={1.6} />
              )}
            </Pressable>

            <View className="items-center">
              <GestureDetector gesture={pan}>
                <Animated.View
                  style={fabStyle}
                  className="h-20 w-20 items-center justify-center rounded-full bg-white"
                >
                  <Animated.View
                    pointerEvents="none"
                    style={[
                      {
                        position: "absolute",
                        top: -6,
                        left: -6,
                        right: -6,
                        bottom: -6,
                        borderRadius: 999,
                        borderWidth: 3,
                      },
                      ringStyle,
                    ]}
                  />
                  <View
                    className={cn(
                      "h-6 w-6 rounded",
                      ui.kind === "recording" ? "bg-red-500" : "bg-black",
                    )}
                  />
                </Animated.View>
              </GestureDetector>
              <Text className="text-white/60 mt-2 text-xs">
                {ui.kind === "recording"
                  ? "Release to send · slide up to cancel"
                  : "Hold to record"}
              </Text>
            </View>

            <Pressable
              onPress={() => setTextOpen(true)}
              disabled={ui.kind !== "idle"}
              accessibilityLabel="Compose text"
              className={cn(
                "h-11 w-11 items-center justify-center rounded-full bg-white/15",
                ui.kind !== "idle" && "opacity-40",
              )}
            >
              <TypeIcon color="white" size={20} strokeWidth={1.6} />
            </Pressable>
          </View>
        </View>
      ) : null}

      {ui.kind === "warming-up" || ui.kind === "recording" ? (
        ui.mode === "video" ? (
          <VideoRecordingOverlay
            cancelProgress={cancelProgress}
            onComplete={handleRecordingComplete}
            onCancel={handleRecordingCancel}
            onRecordingStarted={handleRecordingStarted}
            pendingAction={pendingAction}
          />
        ) : (
          <AudioRecordingOverlay
            cancelProgress={cancelProgress}
            onComplete={handleRecordingComplete}
            onCancel={handleRecordingCancel}
            onRecordingStarted={handleRecordingStarted}
            pendingAction={pendingAction}
          />
        )
      ) : null}

      <ReviewSheet
        open={ui.kind === "review"}
        uri={ui.kind === "review" ? ui.uri : null}
        mode={ui.kind === "review" ? ui.mode : null}
        durationMs={ui.kind === "review" ? ui.durationMs : 0}
        onSend={sendReview}
        onRetake={retake}
        onCancel={cancelReview}
      />

      <TextComposeModal
        open={textOpen}
        onClose={() => setTextOpen(false)}
        onSubmit={submitText}
      />
    </>
  );
}

/**
 * Reads the dock UI state and tells the StreamPresenceContext when to start
 * or stop broadcasting composing_start/composing_stop. The hook intentionally
 * uses optional context access so the dock can also be hosted on the
 * new-stream screen, where there's no provider above us.
 */
function useComposingBroadcast({
  ui,
  textOpen,
  silent,
}: {
  ui: ComposeUiState;
  textOpen: boolean;
  silent: boolean;
}) {
  // Optional consumer — `useStreamComposingBroadcast` throws when used
  // outside its provider, so we trap that here. Cheap because the throw
  // happens once per mount.
  let broadcast: ReturnType<typeof useStreamComposingBroadcast> | null;
  try {
    broadcast = useStreamComposingBroadcast();
  } catch {
    broadcast = null;
  }

  const mode: ComposingMode | null =
    ui.kind === "recording" || ui.kind === "warming-up"
      ? "recording"
      : textOpen
        ? "typing"
        : null;

  useEffect(() => {
    if (silent || !broadcast) return;
    if (mode) {
      broadcast.startComposing(mode);
      return () => broadcast?.stopComposing();
    }
  }, [mode, silent, broadcast]);
}
