import React, {useEffect, useState, useCallback, useRef} from 'react';
import {
  StyleSheet,
  View,
  Text,
  LayoutChangeEvent,
  Platform,
  Alert,
  Linking,
} from 'react-native';
import {useTranslation} from 'react-i18next';
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native';
import {
  Camera,
  useCameraDevice,
  CameraDevice,
  VideoFile,
  useCameraFormat,
  Templates,
} from 'react-native-vision-camera';
import {getVideoMetaData} from 'react-native-compressor';
import {PhotoIdentifier} from '@react-native-camera-roll/camera-roll';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  runOnJS,
  useAnimatedProps,
  cancelAnimation,
} from 'react-native-reanimated';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import {Paths, File} from 'expo-file-system';
import * as LegacyFileSystem from 'expo-file-system/legacy';
import {useTimer} from '@hooks/useTimer';
import {useAppStateChange} from '@hooks/useAppStateChange';
import {useLandscapeMode} from '@hooks/useLandscapeMode';
import {
  CHAT_ATTACHMENT_AUDIO_BITRATE,
  CHAT_ATTACHMENT_VIDEO_BITRATE,
} from '@modules/chats/constants';
import {useSelectMedia} from '@modules/chats/hooks/media/useSelectMedia';
import {DialogMissingPermission} from '@modules/chats/components';
import {MediaEntity, MediaType} from '@modules/chats/interfaces';
import {
  CameraMediaSource,
  ChatsStackParamList,
} from '@modules/chats/navigation/interfaces';
import {showSnackbar} from '@redux/dispatchers';
import {Screens} from '@shared/constants';
import {clamp, getUlid, isIos} from '@shared/utils';
import {useFeatureFlag} from '@stores/featureFlags';

import {CameraEngine} from './components/CameraEngine';
import {FOCUS_RING_SIZE, styles} from './styles';
import {CameraPreviewModal} from './components/CameraPreviewModal';
import {CameraTopControls} from './components/CameraTopControls';
import {CameraBottomControls} from './components/CameraBottomControls';
import {CameraTypeSelector} from './components/CameraTypeSelector';
import {MediaCarousel} from './components/MediaCarousel';
import {useSyncCameraOrientation, useVolumeButtonAsShutter} from './utils';

const MAX_DURATION_MINUTES = 2;

Animated.addWhitelistedUIProps({
  zoom: true,
});

const ReanimatedCamera = Animated.createAnimatedComponent(Camera);

// Normalize the img output rotation based on EXIF orientation data (iOS only, Android's CameraX handles this internally).
const getPreviewRotationFromExif = (orientation?: number) => {
  switch (orientation) {
    case 3:
      return 180;
    case 5:
    case 6:
      return 90;
    case 7:
    case 8:
      return -90;
    default:
      return 0;
  }
};

type MediaEntityPreview = MediaEntity & {
  previewRotation?: number;
};

export default function CameraScreen() {
  useSyncCameraOrientation();
  const navigation = useNavigation();
  const {t} = useTranslation();
  const limitInMb = useFeatureFlag<number>('CHATS_V2_MAX_MEDIA_SIZE_MB') ?? 150;
  const limitInBytes = limitInMb * 1024 * 1024;

  const {
    onMediaPicked,
    onMediaConfirmed,
    onMediaCanceled,
    totalFilesBytes = 0,
    hasMicrophonePermission = false,
  } = useRoute<RouteProp<ChatsStackParamList, Screens.NEW_CAMERA>>().params;
  const {selectMedia} = useSelectMedia();
  const cameraRef = useRef<Camera | null>(null);
  const isRecordingRef = useRef(false);
  const hasShownSizeLimitSnackbar = useRef(false);
  const controlsTopYRef = useRef<number>(Number.POSITIVE_INFINITY);
  const isFirstMount = useRef(true);
  // Source of the current preview — needed when we confirm so analytics can
  // distinguish camera-captured items from gallery-picked ones.
  const previewSourceRef = useRef<CameraMediaSource | null>(null);
  const [isFront, setIsFront] = useState(false);
  const [flash, setFlash] = useState<'on' | 'off'>('off');
  const [currentRecordType, setCurrentRecordType] = useState<'photo' | 'video'>(
    'photo',
  );
  const [previewEntity, setPreviewEntity] = useState<MediaEntityPreview | null>(
    null,
  );

  const [previewVisible, setPreviewVisible] = useState(false);
  const [showMissingPermissions, setShowMissingPermissions] = useState(false);
  const {lockToPortrait} = useLandscapeMode(previewVisible);

  const remainingSpace = clamp(limitInBytes - totalFilesBytes, 0, limitInBytes);
  const maxSecondsBySize =
    remainingSpace /
    ((CHAT_ATTACHMENT_VIDEO_BITRATE + CHAT_ATTACHMENT_AUDIO_BITRATE) / 8);
  const maxRecordingSeconds = Math.min(
    MAX_DURATION_MINUTES * 60,
    maxSecondsBySize,
  );
  const isSizeLimited = maxSecondsBySize < MAX_DURATION_MINUTES * 60;

  const {timerSeconds, startTimer, stopTimer} = useTimer({
    maxDurationSeconds: maxRecordingSeconds,
    onMaxDurationReached: () => {
      stopRecording();

      if (isSizeLimited && !hasShownSizeLimitSnackbar.current) {
        hasShownSizeLimitSnackbar.current = true;
        showSnackbar({
          title: t('chat.messages.max_video_size'),
          variant: 'info',
          hostName: 'camera-preview-modal',
        });
      }
    },
  });

  useAppStateChange({
    onChangeToBackground: () => {
      stopRecording();
    },
  });

  const recordType = useSharedValue<'photo' | 'video'>('photo');
  // zoom 0..1
  const zoom = useSharedValue(1);
  // 🔹 Focus ring state as shared values (NO React state)
  const focusX = useSharedValue(-1000); // off-screen initial
  const focusY = useSharedValue(-1000);
  const focusScale = useSharedValue(0);
  const focusOpacity = useSharedValue(0);
  // 🔴 Recording animation shared values
  const isRecordingSV = useSharedValue(0); // 0 = no, 1 = yes
  const recordTypeSelectorX = useSharedValue(0);
  const recordTypeSelectorWidth = useSharedValue(0);
  const recordTypePhotoX = useSharedValue(0);
  const recordTypePhotoWidth = useSharedValue(0);
  const recordTypeVideoX = useSharedValue(0);
  const recordTypeVideoWidth = useSharedValue(0);
  const mediaCarouselPosition = useSharedValue(0);

  const captureScale = useSharedValue(1);

  // area to ignore taps (controls)
  const onControlsLayout = (e: LayoutChangeEvent) => {
    const {y, height} = e.nativeEvent.layout;
    mediaCarouselPosition.value = height * 1.65;
    controlsTopYRef.current = y; // any tap with y >= this should be ignored for focusing
  };

  const frontDevice = useCameraDevice('front');
  const bestBackDevice = CameraEngine.useBackDevice();
  const device: CameraDevice | undefined = isFront
    ? frontDevice
    : bestBackDevice;

  const minZoom = device?.minZoom ?? 1;
  const maxZoom = device?.maxZoom ?? 10;

  const lenses = CameraEngine.useLenses(minZoom, maxZoom);

  const {selectedLens, genOnLensPress} = CameraEngine.useOnLensPress();

  useEffect(() => {
    if (Platform.OS === 'ios' && !isFront) {
      const standardLens = lenses.find(lens => lens.opticalZoom === 1);
      if (standardLens) {
        zoom.value = standardLens.zoomFactor;
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isFront, lenses]);

  const format = useCameraFormat(device, Templates.Instagram);

  const onFlip = useCallback(() => {
    setIsFront(prev => {
      if (Platform.OS === 'ios') {
        if (!prev) {
          zoom.value = 1;
        }
      } else {
        zoom.value = 1;
      }
      return !prev;
    });
  }, [zoom]);
  const onToggleFlash = useCallback(
    () => setFlash(f => (f === 'off' ? 'on' : 'off')),
    [],
  );

  /**
   * Starts the upload pipeline for an item and shows the preview screen.
   *
   * - Camera captures (`source: 'camera'`): the pipeline never rejects
   *   (limits are bypassed), so we show the preview immediately and let
   *   compression + upload run in the background.
   * - Gallery picks (`source: 'cameraGallery'`): the pipeline can reject
   *   on size/count, so we await its result first. If rejected, the
   *   pipeline has already shown a snackbar and we stay on the camera
   *   screen — no preview is shown and the user can pick something else.
   */
  const startPreviewForEntity = useCallback(
    async (entity: MediaEntity, source: CameraMediaSource) => {
      if (source === 'camera') {
        previewSourceRef.current = source;
        setPreviewEntity(entity);
        setPreviewVisible(true);
        // Fire-and-forget; pipeline accepts captures unconditionally.
        onMediaPicked(entity, source);
        return;
      }

      const result = await onMediaPicked(entity, source);
      if (!result.ok) {
        // Pipeline rejected the gallery pick (size/count). Snackbar is
        // already shown by the pipeline; just stay on the camera screen.
        return;
      }
      previewSourceRef.current = source;
      setPreviewEntity(entity);
      setPreviewVisible(true);
    },
    [onMediaPicked],
  );

  const onTakePhoto = useCallback(async () => {
    const cam = cameraRef.current;
    if (!cam) return;
    try {
      // Check if the device has flash before taking photo with flash on
      const photo = await cam.takePhoto({
        flash: device?.hasFlash ? flash : 'off',
      });
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);

      const id = getUlid();
      const localUri = photo.path.startsWith('file://')
        ? photo.path
        : `file://${photo.path}`;
      const fileInfo = new File(localUri).info();

      const entity: MediaEntityPreview = {
        id,
        type: MediaType.IMAGE,
        fileAsset: null,
        status: 'creatingAsset',
        contentType: 'image/jpeg',
        localUri,
        name: `camera_${id}.jpg`,
        width: photo.width,
        height: photo.height,
        sizeInBytes:
          fileInfo.exists && fileInfo.size != null ? fileInfo.size : 0,
        /**
         * iOS camera output may not be auto-rotated based on EXIF data, so we need to pass the orientation to the preview to rotate it correctly.
         */
        previewRotation: isIos
          ? getPreviewRotationFromExif(photo.metadata?.Orientation)
          : 0,
      };

      startPreviewForEntity(entity, 'camera');
    } catch (e) {
      // console.warn('Failed to take photo', e);
    }
  }, [flash, startPreviewForEntity, device]);

  const onAccept = useCallback(
    (caption: string) => {
      if (!previewEntity) return;
      // By the time we get here the entity is guaranteed to be in the
      // upload state (camera captures bypass validation; gallery picks
      // only show the preview after the pipeline has accepted them).
      // We just commit the caption + analytics.
      onMediaConfirmed(
        previewEntity,
        caption,
        previewSourceRef.current ?? 'camera',
      );
      previewSourceRef.current = null;
      navigation.goBack();
    },
    [previewEntity, navigation, onMediaConfirmed],
  );

  const onRetake = useCallback(() => {
    // Force lock before closing the preview model !!!
    lockToPortrait();

    // The upload pipeline is already running for this entity — tell the
    // chat to drop it before we wipe local state.
    if (previewEntity) {
      onMediaCanceled(previewEntity.id);
    }

    previewSourceRef.current = null;
    setPreviewVisible(false);
    setPreviewEntity(null);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [previewEntity, onMediaCanceled]);

  // pinch zoom (new GH API)
  const baseZoom = useSharedValue(1);

  const pinchGesture = Gesture.Pinch()
    .onBegin(() => {
      baseZoom.value = zoom.value; // store current zoom when pinch starts
    })
    .onUpdate(e => {
      const newZoom = baseZoom.value * e.scale;
      zoom.value = Math.min(maxZoom, Math.max(minZoom, newZoom));
    });

  // 👆 Tap-to-focus using shared values + runOnJS for cameraRef.focus()
  const focusAtPointJS = useCallback(
    // eslint-disable-next-line max-params
    (x: number, y: number, vw: number, vh: number) => {
      const norm = {x: x / vw, y: y / vh}; // 0..1
      cameraRef.current?.focus(norm);
    },
    [],
  );

  const tapGesture = Gesture.Tap().onEnd(e => {
    // ignore taps over the controls
    if (e.y >= controlsTopYRef.current) return;

    // update shared values for ring position
    focusX.value = e.x - FOCUS_RING_SIZE / 2; // translate positions (we'll use translateX/Y)
    focusY.value = e.y - FOCUS_RING_SIZE / 2;

    // animate ring (pop in, then fade out)
    focusScale.value = 1.5;
    focusOpacity.value = 1;
    focusScale.value = withTiming(1, {duration: 200});
    focusOpacity.value = withTiming(0, {duration: 1000});

    // call JS method to actually focus camera (cameraRef is JS-side)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const vw = (e as any).target?.width ?? 1;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const vh = (e as any).target?.height ?? 1;
    runOnJS(focusAtPointJS)(e.x, e.y, vw, vh);
  });

  const doubleTapGesture = Gesture.Tap()
    .numberOfTaps(2)
    .onEnd(e => {
      if (e.y >= controlsTopYRef.current) return;
      runOnJS(onFlip)();
    });

  const switchToPhotoMode = useCallback(() => {
    if (currentRecordType === 'photo') return;
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    recordType.value = 'photo';
    setCurrentRecordType('photo');
    recordTypeSelectorX.value = withTiming(recordTypePhotoX.value, {
      duration: 300,
    });
    recordTypeSelectorWidth.value = withTiming(recordTypePhotoWidth.value, {
      duration: 300,
    });
  }, [
    currentRecordType,
    recordType,
    recordTypeSelectorX,
    recordTypePhotoX,
    recordTypeSelectorWidth,
    recordTypePhotoWidth,
  ]);

  const switchToVideoMode = useCallback(() => {
    if (currentRecordType === 'video') return;
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    recordType.value = 'video';
    setCurrentRecordType('video');
    recordTypeSelectorX.value = withTiming(recordTypeVideoX.value, {
      duration: 300,
    });
    recordTypeSelectorWidth.value = withTiming(recordTypeVideoWidth.value, {
      duration: 300,
    });
  }, [
    currentRecordType,
    recordType,
    recordTypeSelectorX,
    recordTypeVideoX,
    recordTypeSelectorWidth,
    recordTypeVideoWidth,
  ]);

  const swipeGesture = Gesture.Pan()
    .activeOffsetX([-20, 20])
    .failOffsetY([-50, 50])
    .onEnd(e => {
      if (e.y >= controlsTopYRef.current) return;
      const isFastSwipe = Math.abs(e.velocityX) > 300;
      const isLongSwipe = Math.abs(e.translationX) > 100;

      if (isFastSwipe || isLongSwipe) {
        if (e.velocityX > 0 || (e.velocityX === 0 && e.translationX > 0)) {
          runOnJS(switchToVideoMode)();
        } else if (
          e.velocityX < 0 ||
          (e.velocityX === 0 && e.translationX < 0)
        ) {
          runOnJS(switchToPhotoMode)();
        }
      }
    });

  const gestures = Gesture.Exclusive(
    pinchGesture,
    swipeGesture,
    doubleTapGesture,
    tapGesture,
  );

  const animatedZoom = useAnimatedProps(
    () => ({
      zoom: zoom.value,
    }),
    [zoom],
  );

  const animatedFocusStyle = useAnimatedStyle(() => ({
    opacity: focusOpacity.value,
    transform: [
      {translateX: focusX.value},
      {translateY: focusY.value},
      {scale: focusScale.value},
    ],
  }));

  const handleVideoCaptured = useCallback(
    async (video: VideoFile) => {
      try {
        const id = getUlid();
        const localUri = video.path.startsWith('file://')
          ? video.path
          : `file://${video.path}`;

        let width = video.width;
        let height = video.height;
        if (!width || !height) {
          try {
            const meta = await getVideoMetaData(localUri);
            width = meta.width;
            height = meta.height;
          } catch {
            // fall through with whatever vision-camera returned
          }
        }

        const fileInfo = new File(localUri).info();

        const entity: MediaEntity = {
          id,
          type: MediaType.VIDEO,
          fileAsset: null,
          status: 'creatingAsset',
          contentType: 'video/mp4',
          localUri,
          name: `camera_${id}.mp4`,
          width,
          height,
          sizeInBytes:
            fileInfo.exists && fileInfo.size != null ? fileInfo.size : 0,
          metadata:
            video.duration != null
              ? {durationInMs: video.duration * 1000}
              : undefined,
        };

        startPreviewForEntity(entity, 'camera');
      } catch {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }
    },
    [t, startPreviewForEntity],
  );

  const startRecording = useCallback(() => {
    const cam = cameraRef.current;
    if (!cam) return;

    const microphonePermission = Camera.getMicrophonePermissionStatus();

    if (microphonePermission !== 'granted') {
      Alert.alert(
        t('general.permissions.microphone.missing'),
        t('general.permissions.microphone.missing_description'),
        [
          {text: t('general.cancel'), style: 'cancel'},
          {text: t('general.settings'), onPress: () => Linking.openSettings()},
        ],
      );
      return;
    }

    try {
      hasShownSizeLimitSnackbar.current = false;
      // start recording animations
      isRecordingSV.value = 1;
      isRecordingRef.current = true;
      startTimer();

      cam.startRecording({
        onRecordingFinished(video: VideoFile) {
          // Reset recording state synchronously so the UI reacts immediately;
          // entity construction and preview kick-off are async and live in
          // handleVideoCaptured below.
          isRecordingSV.value = 0;
          isRecordingRef.current = false;
          stopTimer();
          cancelAnimation(captureScale);
          captureScale.value = 1;

          handleVideoCaptured(video);
        },
        onRecordingError(_error: Error) {
          isRecordingSV.value = 0;
          isRecordingRef.current = false;
          stopTimer();
          // Reset capture button animated value
          cancelAnimation(captureScale);
          captureScale.value = 1;
        },
      });
    } catch (e) {
      // console.warn('Failed to start recording', e);
    }
  }, [
    captureScale,
    isRecordingSV,
    startTimer,
    stopTimer,
    handleVideoCaptured,
    t,
  ]);

  const stopRecording = useCallback(async () => {
    const cam = cameraRef.current;
    if (!cam) return;
    try {
      await cam.stopRecording();
    } catch (e) {
      // console.warn('Failed to stop recording', e);
      // ensure we reset animations even if stop throws
      isRecordingSV.value = 0;
      isRecordingRef.current = false;
      // Reset capture button animated value
      cancelAnimation(captureScale);
      captureScale.value = 1;
    }
  }, [cameraRef, captureScale, isRecordingSV]);

  useVolumeButtonAsShutter({
    onTakePhoto,
    currentRecordType,
    isPreviewVisible: previewVisible,
  });

  const onOpenImagePicker = useCallback(async () => {
    try {
      const result = await selectMedia({
        multiple: false,
        withVideo: true,
        selectionLimit: 1,
      });

      if (!result.ok && result.shouldOpenSettings) {
        setShowMissingPermissions(true);
        return;
      }

      if (!result.ok || !result.data?.length) {
        return;
      }

      const [item] = result.data;
      startPreviewForEntity(item, 'cameraGallery');
    } catch {
      // do nothing
    }
  }, [selectMedia, startPreviewForEntity]);

  const onMediaPress = useCallback(
    async (item: PhotoIdentifier) => {
      try {
        const isVideo = item.node.type.startsWith('video');
        const id = getUlid();
        const filename = `carousel_${id}.${isVideo ? 'mp4' : 'jpg'}`;
        const destination = new File(Paths.cache, filename);

        // expo-file-system's legacy copyAsync handles iOS photo-library URIs
        // (ph:// and assets-library://) via PHAssetResourceManager for both
        // images and videos — pure binary copy, no transcoding/compression.
        // The new File API dropped these handlers, hence we use legacy here.
        // The pipeline (compressImagesWithCache / compressVideoWithCache)
        // remains the single point of compression.
        await LegacyFileSystem.copyAsync({
          from: item.node.image.uri,
          to: destination.uri,
        });

        const fileInfo = destination.info();
        if (!fileInfo.exists || fileInfo.size === undefined) {
          showSnackbar({
            title: t('errors.api.500', {info: ''}),
            variant: 'error',
          });
          return;
        }

        const duration = item.node.image.playableDuration;
        const mediaEntity: MediaEntity = {
          id,
          type: isVideo ? MediaType.VIDEO : MediaType.IMAGE,
          fileAsset: null,
          status: 'creatingAsset',
          contentType: isVideo ? 'video/mp4' : 'image/jpeg',
          localUri: destination.uri,
          name: filename,
          height: item.node.image.height,
          width: item.node.image.width,
          sizeInBytes: fileInfo.size,
          ...(isVideo &&
            duration != null && {
              metadata: {durationInMs: duration * 1000},
            }),
        };

        startPreviewForEntity(mediaEntity, 'cameraGallery');
      } catch (error) {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }
    },
    [t, startPreviewForEntity],
  );

  const onLayoutPhoto = useCallback(
    (e: LayoutChangeEvent) => {
      const {x, width} = e.nativeEvent.layout;
      recordTypePhotoX.value = x;
      recordTypePhotoWidth.value = width;

      // set initial x and width to selector
      if (isFirstMount.current) {
        isFirstMount.current = false;
        recordTypeSelectorX.value = x;
        recordTypeSelectorWidth.value = width;
        return;
      }
    },
    [
      recordTypePhotoWidth,
      recordTypePhotoX,
      recordTypeSelectorWidth,
      recordTypeSelectorX,
    ],
  );

  const onLayoutVideo = useCallback(
    (e: LayoutChangeEvent) => {
      const {x, width} = e.nativeEvent.layout;
      recordTypeVideoX.value = x;
      recordTypeVideoWidth.value = width;
    },
    [recordTypeVideoWidth, recordTypeVideoX],
  );

  const onPressPhotoType = useCallback(() => {
    if (currentRecordType === 'photo') return;
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    recordType.value = 'photo';
    setCurrentRecordType('photo');
    recordTypeSelectorX.value = withTiming(recordTypePhotoX.value, {
      duration: 300,
    });
    recordTypeSelectorWidth.value = withTiming(recordTypePhotoWidth.value, {
      duration: 300,
    });
  }, [
    currentRecordType,
    recordTypePhotoWidth.value,
    recordTypePhotoX.value,
    recordTypeSelectorWidth,
    recordTypeSelectorX,
    recordType,
  ]);

  const onPressVideoType = useCallback(() => {
    if (currentRecordType === 'video') return;
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    recordType.value = 'video';
    setCurrentRecordType('video');
    recordTypeSelectorX.value = withTiming(recordTypeVideoX.value, {
      duration: 300,
    });
    recordTypeSelectorWidth.value = withTiming(recordTypeVideoWidth.value, {
      duration: 300,
    });
  }, [
    currentRecordType,
    recordTypeVideoWidth.value,
    recordTypeVideoX.value,
    recordTypeSelectorWidth,
    recordTypeSelectorX,
    recordType,
  ]);

  const onCloseDialogPermission = () => {
    setShowMissingPermissions(false);
  };

  if (!device) {
    return (
      <View style={styles.center}>
        <Text style={styles.text}>Loading camera…</Text>
      </View>
    );
  }

  const photoUri = previewEntity?.localUri;
  const isVideo = previewEntity?.type === MediaType.VIDEO;

  return (
    <View style={styles.container}>
      <GestureDetector gesture={gestures}>
        <Animated.View style={styles.flex}>
          <ReanimatedCamera
            ref={cameraRef}
            style={StyleSheet.absoluteFill}
            device={device}
            // When true - closing preview modal is smoother ( no glitchy )
            isActive={true}
            enableZoomGesture={false}
            photo
            video
            audio={hasMicrophonePermission}
            animatedProps={animatedZoom}
            format={format}
            torch={
              currentRecordType === 'video' && flash === 'on' ? 'on' : 'off'
            }
          />

          {/* Animated focus ring (positioned via translateX/Y) */}
          <Animated.View style={[styles.focusRing, animatedFocusStyle]}>
            <View style={[styles.focusRingLine, styles.focusRingLineLeft]} />
            <View style={[styles.focusRingLine, styles.focusRingLineRight]} />
            <View style={[styles.focusRingLine, styles.focusRingLineTop]} />
            <View style={[styles.focusRingLine, styles.focusRingLineBottom]} />
          </Animated.View>
        </Animated.View>
      </GestureDetector>

      {!previewVisible && (
        <>
          <CameraTopControls
            flash={flash}
            flashAvailable={device.hasFlash || device.hasTorch}
            onToggleFlash={onToggleFlash}
            recordType={recordType}
            timerSeconds={timerSeconds}
            isRecordingSV={isRecordingSV}
          />
          <MediaCarousel
            onMediaPress={onMediaPress}
            isRecordingSV={isRecordingSV}
            bottomPadding={mediaCarouselPosition}
            isFront={isFront}
          />
          <CameraBottomControls
            onOpenImagePicker={onOpenImagePicker}
            onControlsLayout={onControlsLayout}
            onFlip={onFlip}
            onTakePhoto={onTakePhoto}
            startRecording={startRecording}
            stopRecording={stopRecording}
            isRecordingRef={isRecordingRef}
            isRecordingSV={isRecordingSV}
            recordType={recordType}
            captureScale={captureScale}
            lenses={lenses}
            zoom={zoom}
            genOnLensPress={genOnLensPress}
            selectedLens={selectedLens}
            minZoom={minZoom}
            maxZoom={maxZoom}
            isFront={isFront}
          />
          <CameraTypeSelector
            recordType={recordType}
            onPressPhotoType={onPressPhotoType}
            onPressVideoType={onPressVideoType}
            onLayoutPhoto={onLayoutPhoto}
            onLayoutVideo={onLayoutVideo}
            recordTypeSelectorX={recordTypeSelectorX}
            recordTypeSelectorWidth={recordTypeSelectorWidth}
            isRecordingSV={isRecordingSV}
          />
        </>
      )}

      <CameraPreviewModal
        previewVisible={previewVisible}
        onRetake={onRetake}
        onAccept={onAccept}
        photoUri={photoUri ?? ''}
        isVideo={isVideo ?? false}
        imageRotation={previewEntity?.previewRotation ?? 0}
      />

      <DialogMissingPermission
        isVisible={showMissingPermissions}
        onClose={onCloseDialogPermission}
      />
    </View>
  );
}
