import { useEffect, useState } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import { Mic, Video as VideoIcon } from 'lucide-react-native';
import { useEventListener } from 'expo';
import { useVideoPlayer, VideoView, type VideoPlayerStatus } from 'expo-video';
import type { Particle } from '@/api/types';
import { apiClient } from '@/api/client';
import { logError } from '@/lib/errors';
import { useEvent } from '@/hooks/use-event';
import { useTranscriptPlayback } from '@/hooks/use-transcript-playback';
import { TranscriptOverlay } from './TranscriptOverlay';
import { useStreamSafeArea } from './stream-safe-area';

type MediaParticle = Extract<Particle, { type: 'media' }>;

interface MediaParticleViewProps {
  particle: MediaParticle;
  paused: boolean;
  onEnded: () => void;
  onProgress: (ratio: number) => void;
  /** "cover" fills the screen (may crop); "contain" fits the whole frame. */
  contentFit?: 'cover' | 'contain';
}

const TICK_MS = 150;

/**
 * Plays MP4 / MOV / m4a content via expo-video. Desktop currently records
 * WebM, which AVPlayer can't decode; the particle processor worker produces
 * an iOS-playable MP4/m4a variant and writes `transcoded_object_id` /
 * `transcoded_mime_type` to the particle. While that work is in flight, we
 * show a placeholder and let the Firestore listener
 * swap us into the playable state once the worker finishes.
 *
 * The signed download URL is fetched lazily via apiClient.getParticleDownloadUrl
 * (Orion-issued, time-limited). We show a spinner while that resolves, then
 * mount the player and report progress via a 150ms tick reading the player's
 * currentTime — same model as desktop's MediaParticleView.
 */
export function MediaParticleView({
  particle,
  paused,
  onEnded,
  onProgress,
  contentFit = 'cover',
}: MediaParticleViewProps) {
  const activeObjectId =
    particle.properties.transcoded_object_id ?? particle.properties.object_id;
  const activeMime =
    particle.properties.transcoded_mime_type ?? particle.properties.mime_type;
  const isAudio = activeMime.startsWith('audio/');
  const isPlayable = isPlayableMime(activeMime);

  // Reset progress as the active particle changes — independent of playback
  // state — so the segmented bar drops back to 0 immediately.
  useEffect(() => {
    onProgress(0);
  }, [particle.id, onProgress]);

  if (!isPlayable) {
    return <ProcessingForMobilePlaceholder isAudio={isAudio} />;
  }

  return (
    <PlayableMediaView
      particle={particle}
      activeObjectId={activeObjectId}
      isAudio={isAudio}
      paused={paused}
      onEnded={onEnded}
      onProgress={onProgress}
      contentFit={contentFit}
    />
  );
}

function PlayableMediaView({
  particle,
  activeObjectId,
  isAudio,
  paused,
  onEnded,
  onProgress,
  contentFit,
}: {
  particle: MediaParticle;
  activeObjectId: string;
  isAudio: boolean;
  paused: boolean;
  onEnded: () => void;
  onProgress: (ratio: number) => void;
  contentFit: 'cover' | 'contain';
}) {
  const [sourceUri, setSourceUri] = useState<string | null>(null);
  const [resolveError, setResolveError] = useState<Error | null>(null);
  const [currentTime, setCurrentTime] = useState(0);

  // Fetch the signed download URL once per active object. Orion URLs are
  // time-limited — we treat the URL as one-shot for this view's lifetime.
  // Re-runs when the worker writes `transcoded_object_id` and the parent
  // resolves a new active object id.
  useEffect(() => {
    let cancelled = false;
    apiClient
      .getParticleDownloadUrl(activeObjectId)
      .then((url) => {
        if (!cancelled) setSourceUri(url);
      })
      .catch((err) => {
        logError(err, { scope: 'media.download-url' });
        if (!cancelled) setResolveError(err as Error);
      });
    return () => {
      cancelled = true;
      setSourceUri(null);
      setResolveError(null);
      setCurrentTime(0);
    };
  }, [activeObjectId, particle.id]);

  const player = useVideoPlayer(sourceUri ?? '', (p) => {
    p.loop = false;
    p.muted = false;
    p.timeUpdateEventInterval = 0.15;
    // Don't take exclusive ownership of the iOS AVAudioSession. Without this
    // the player blocks expo-camera from acquiring the session for video
    // recording (audio works because expo-audio deactivates other sessions
    // natively before claiming the session).
    p.audioMixingMode = 'mixWithOthers';
  });

  // Drive play/pause from the suspender store. The player itself is forgiving
  // about extra play/pause calls so we don't gate this.
  useEffect(() => {
    if (!sourceUri) return;
    if (paused) {
      player.pause();
    } else {
      player.play();
    }
  }, [paused, sourceUri, player]);

  // End-of-clip → advance. We listen to status flips rather than computing
  // duration ratios because video duration may be 0 for the first frame or two.
  useEventListener(player, 'statusChange', ({ status }) => {
    if (status === ('idle' satisfies VideoPlayerStatus)) {
      // ignored — happens during source swap
    }
  });

  // Drive caption highlighting from the player's own timeUpdate cadence
  // (timeUpdateEventInterval = 0.15s above). Pausing halts the events, which
  // naturally freezes the active word/sentence — no extra plumbing needed.
  useEventListener(player, 'timeUpdate', ({ currentTime: t }) => {
    setCurrentTime(t);
  });

  const transcript = particle.properties.transcript;
  const { activeSentence, activeWordIndex } = useTranscriptPlayback(
    transcript,
    currentTime,
  );
  const safeArea = useStreamSafeArea();
  // Sit just above the compose dock. Pills moved to the top of the screen so
  // the only thing this offset has to clear is the dock itself plus a small
  // gap — captions can ride lower than they did before.
  const captionBottomOffset = safeArea.bottom;

  const onEndedStable = useEvent(onEnded);
  const onProgressStable = useEvent(onProgress);

  // Progress tick: report currentTime / duration each TICK_MS. Bail when the
  // player isn't ready yet (duration = 0).
  useEffect(() => {
    if (!sourceUri || paused) return;
    const interval = setInterval(() => {
      const duration = player.duration;
      if (!duration || duration <= 0) return;
      const ratio = Math.min(player.currentTime / duration, 1);
      onProgressStable(ratio);
      if (ratio >= 0.999) {
        clearInterval(interval);
        onEndedStable();
      }
    }, TICK_MS);
    return () => clearInterval(interval);
  }, [sourceUri, paused, player, onEndedStable, onProgressStable]);

  if (resolveError) {
    return (
      <View className="flex-1 items-center justify-center px-8">
        <Text className="text-white/80 text-base text-center">
          Couldn’t load this {isAudio ? 'voice message' : 'video'}.
        </Text>
        <Text className="text-white/50 text-sm text-center mt-2">
          Tap forward to continue.
        </Text>
      </View>
    );
  }

  if (!sourceUri) {
    return (
      <View className="flex-1 items-center justify-center bg-black">
        <ActivityIndicator color="white" />
      </View>
    );
  }

  // Audio-only: hide the (blank) video surface and show a static face. The
  // VideoView still renders 0×0 so the audio track keeps playing.
  if (isAudio) {
    return (
      <View className="flex-1 items-center justify-center px-8">
        <View
          className="absolute"
          style={{ width: 0, height: 0, opacity: 0 }}
          pointerEvents="none"
        >
          <VideoView
            style={{ width: 1, height: 1 }}
            player={player}
            nativeControls={false}
            allowsFullscreen={false}
            allowsPictureInPicture={false}
          />
        </View>
        <View className="bg-white/10 h-24 w-24 items-center justify-center rounded-full">
          <Mic color="white" size={36} strokeWidth={1.5} />
        </View>
        <Text className="text-white mt-6 text-lg font-medium">
          Voice message
        </Text>
        {transcript ? (
          <TranscriptOverlay
            transcript={transcript}
            activeSentence={activeSentence}
            activeWordIndex={activeWordIndex}
            bottomOffset={captionBottomOffset}
          />
        ) : null}
      </View>
    );
  }

  // Video: full-bleed. Default cover so portrait mobile captures fill the
  // screen; the user can flip to contain via the top-right toggle when desktop
  // captures at odd aspect ratios get cropped uncomfortably.
  return (
    <View className="flex-1 bg-black">
      <VideoView
        style={{ flex: 1 }}
        player={player}
        nativeControls={false}
        contentFit={contentFit}
        allowsFullscreen={false}
        allowsPictureInPicture={false}
      />
      {transcript ? (
        <TranscriptOverlay
          transcript={transcript}
          activeSentence={activeSentence}
          activeWordIndex={activeWordIndex}
          bottomOffset={captionBottomOffset}
        />
      ) : null}
    </View>
  );
}

// Shown while the particle processor worker is producing the iOS-playable
// MP4/m4a variant. The Firestore listener will re-render this view once
// `transcoded_object_id` lands on the particle, which swaps us into
// PlayableMediaView. We deliberately do not auto-advance — the user is here
// to consume this content; if the worker is slow they can tap forward.
function ProcessingForMobilePlaceholder({ isAudio }: { isAudio: boolean }) {
  return (
    <View className="flex-1 items-center justify-center px-8">
      <View className="bg-white/10 h-24 w-24 items-center justify-center rounded-full">
        {isAudio ? (
          <Mic color="white" size={36} strokeWidth={1.5} />
        ) : (
          <VideoIcon color="white" size={36} strokeWidth={1.5} />
        )}
      </View>
      <Text className="text-white mt-6 text-lg font-medium">
        {isAudio ? 'Voice message' : 'Video message'}
      </Text>
      <View className="flex-row items-center mt-3">
        <Text className="text-white/60 ml-3 text-sm">View on desktop</Text>
      </View>
      <Text className="text-white/40 mt-2 text-xs text-center">
        Please view this on desktop only.
      </Text>
    </View>
  );
}

function isPlayableMime(mime: string): boolean {
  // expo-video uses AVPlayer on iOS — reliable for h264 in mp4 / mov / m4a.
  // WebM/VP9 (the legacy desktop format) is not decodable.
  return (
    mime === 'video/mp4' ||
    mime === 'video/quicktime' ||
    mime === 'audio/mp4' ||
    mime === 'audio/aac' ||
    mime === 'audio/x-m4a' ||
    mime === 'audio/mpeg'
  );
}
