import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
  type CameraType,
  type CameraView,
  type FlashMode,
  useCameraPermissions,
  useMicrophonePermissions,
} from 'expo-camera'
import { useRouter } from 'expo-router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useSharedValue, withTiming } from 'react-native-reanimated'
import { addStoryMedia, publishStory } from 'src/lib/api'
import {
  compressVideo,
  getMediaLimits,
  PermissionDeniedError,
  pickMedia,
  toUploadFile,
} from 'src/lib/media'
import { transcodeImage } from 'src/lib/media/transcode'
import { cameraQueries } from 'src/lib/queries/camera'
import { storyQueries } from 'src/lib/queries/stories'
import { disclose } from 'src/state/permissionDisclosure'
import { useToastController } from 'tamagui'

// Mirrors usePostEdit / useComposer: own the story composer's camera state,
// mutation, permission handling, and timing in one place so the screen file
// stays a shell.

export const MAX_VIDEO_SECONDS = 15
export const DEFAULT_PHOTO_DURATION_S = 5

export type Captured = {
  uri: string
  type: 'photo' | 'video'
  mimeType: string
  duration: number
}

export type Phase = 'capture' | 'preview' | 'uploading'

export function useStoryCompose() {
  const router = useRouter()
  const toast = useToastController()
  const queryClient = useQueryClient()

  const [facing, setFacing] = useState<CameraType>('back')
  const [flash, setFlash] = useState<FlashMode>('off')
  const [cameraPerm, requestCameraPerm] = useCameraPermissions()
  const [micPerm, requestMicPerm] = useMicrophonePermissions()
  const [isRecording, setRecording] = useState(false)
  const [captured, setCaptured] = useState<Captured | null>(null)
  const [phase, setPhase] = useState<Phase>('capture')
  const cameraRef = useRef<CameraView | null>(null)
  const recordStarted = useRef<number>(0)

  const ringProgress = useSharedValue(0)

  const { data: instance } = useQuery(cameraQueries.instance())
  const { data: composeSettings } = useQuery({
    ...cameraQueries.composeSettings(),
    enabled: !!instance,
  })
  const limits = getMediaLimits(instance ?? null, composeSettings ?? null)

  useEffect(() => {
    if (!cameraPerm || !micPerm) return
    let cancelled = false
    // D.2: gate each system permission prompt behind the in-app prominent
    // disclosure modal on Android. iOS / already-disclosed branches resolve
    // synchronously inside `disclose()` so the request fires immediately.
    void (async () => {
      if (!cameraPerm.granted && cameraPerm.canAskAgain) {
        await disclose('camera')
        if (cancelled) return
        await requestCameraPerm()
        if (cancelled) return
      }
      if (!micPerm.granted && micPerm.canAskAgain) {
        await disclose('microphone')
        if (cancelled) return
        await requestMicPerm()
      }
    })()
    return () => {
      cancelled = true
    }
  }, [cameraPerm, micPerm, requestCameraPerm, requestMicPerm])

  const resetCapture = useCallback(() => {
    setCaptured(null)
    setPhase('capture')
    ringProgress.value = 0
  }, [ringProgress])

  const toggleFacing = useCallback(
    () => setFacing((f) => (f === 'back' ? 'front' : 'back')),
    []
  )
  const toggleFlash = useCallback(() => setFlash((f) => (f === 'off' ? 'on' : 'off')), [])

  const pickFromLibrary = useCallback(async () => {
    try {
      const raws = await pickMedia({ kinds: ['photo', 'video'] })
      if (!raws.length) return
      const a = raws[0]
      setCaptured({
        uri: a.uri,
        type: a.kind === 'video' ? 'video' : 'photo',
        mimeType: a.mimeType,
        duration:
          a.kind === 'video'
            ? Math.min(
                MAX_VIDEO_SECONDS,
                Math.max(1, Math.round((a.durationMs ?? 0) / 1000))
              )
            : DEFAULT_PHOTO_DURATION_S,
      })
      setPhase('preview')
    } catch (e) {
      if (e instanceof PermissionDeniedError) {
        toast.show('Photo access is off', {
          message: 'Grant photo-library access in Settings to pick a story.',
          preset: 'error',
        })
      }
    }
  }, [toast])

  const takePhoto = useCallback(async () => {
    if (!cameraRef.current) return
    try {
      const res = await cameraRef.current.takePictureAsync({ quality: 0.92 })
      if (!res?.uri) return
      setCaptured({
        uri: res.uri,
        type: 'photo',
        mimeType: 'image/jpeg',
        duration: DEFAULT_PHOTO_DURATION_S,
      })
      setPhase('preview')
    } catch {
      toast.show('Camera error', { message: 'Could not capture photo.', preset: 'error' })
    }
  }, [toast])

  const startRecording = useCallback(async () => {
    if (!cameraRef.current || isRecording) return
    setRecording(true)
    recordStarted.current = Date.now()
    ringProgress.value = withTiming(1, { duration: MAX_VIDEO_SECONDS * 1000 })
    try {
      const res = await cameraRef.current.recordAsync({ maxDuration: MAX_VIDEO_SECONDS })
      if (res?.uri) {
        const elapsed = Math.max(
          1,
          Math.round((Date.now() - recordStarted.current) / 1000)
        )
        setCaptured({
          uri: res.uri,
          type: 'video',
          mimeType: 'video/mp4',
          duration: Math.min(MAX_VIDEO_SECONDS, elapsed),
        })
        setPhase('preview')
      }
    } catch {
      // user cancelled — nothing to do, the ring animation resets below.
    } finally {
      setRecording(false)
      ringProgress.value = 0
    }
  }, [isRecording, ringProgress])

  const stopRecording = useCallback(() => {
    if (!cameraRef.current) return
    try {
      cameraRef.current.stopRecording()
    } catch {}
  }, [])

  const prepareForUpload = useCallback(
    async (c: Captured): Promise<Captured | null> => {
      if (c.type === 'photo') {
        // Pixelfed's stories endpoint enforces a four-mimetype allowlist
        // (jpeg/jpg/png/mp4) against the file's *content*, not the
        // client-declared `mimeType`. iOS hands back HEIC by default from
        // the photo library, which the server rejects with a quiet 422; the
        // user then sees a spinner-then-retake bounce. Stories are stricter
        // than posts (no WebP, no GIF), so this passes JPEG explicitly to
        // the shared transcoder rather than letting it pick WebP off the
        // post-side capability cache.
        const lower = c.mimeType.toLowerCase()
        if (lower === 'image/jpeg' || lower === 'image/jpg' || lower === 'image/png') {
          return c
        }
        try {
          const saved = await transcodeImage(c.uri, 'jpeg', { quality: 0.9 })
          return { ...c, uri: saved.uri, mimeType: saved.mimeType }
        } catch {
          toast.show('Image conversion failed', {
            message: 'Could not prepare image for upload.',
            preset: 'error',
          })
          return null
        }
      }
      const result = await compressVideo(
        { uri: c.uri, kind: 'video', mimeType: c.mimeType },
        { maxVideoBytes: limits.maxVideoBytes, forceMp4: true }
      )
      if (result.ok) {
        return {
          ...c,
          uri: result.asset.uri,
          mimeType: result.asset.mimeType,
        }
      }
      if (result.reason === 'too_large') {
        toast.show('Video too large', {
          message: `Story is ${result.sizeMb.toFixed(1)} MB after compression; your instance caps at ${result.limitMb} MB.`,
          preset: 'error',
        })
      } else if (result.reason !== 'aborted') {
        toast.show('Compression failed', {
          message: 'Could not prepare video for upload.',
          preset: 'error',
        })
      }
      return null
    },
    [limits.maxVideoBytes, toast]
  )

  const uploadMutation = useMutation({
    mutationFn: async (c: Captured) => {
      const prepared = await prepareForUpload(c)
      if (!prepared) throw new Error('aborted')
      const fileParam = toUploadFile({ uri: prepared.uri, mimeType: prepared.mimeType })
      const add = (await addStoryMedia({
        file: fileParam,
        duration: prepared.duration,
      })) as { media_id?: string; id?: string; status?: string }
      const mediaId = add?.media_id ?? add?.id
      if (!mediaId) throw new Error('Upload did not return a media id')
      return await publishStory({
        media_id: String(mediaId),
        duration: prepared.duration,
        can_reply: true,
        can_react: true,
      })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: storyQueries.carousel().queryKey })
      toast.show('Story posted', {
        message: "Your story will appear in your followers' Stories rail for 24 hours.",
        preset: 'success',
      })
      router.back()
    },
    onError: (err: Error) => {
      setPhase('preview')
      if (err.message !== 'aborted') {
        toast.show('Story not published', { message: err.message, preset: 'error' })
      }
    },
  })

  const publish = useCallback(() => {
    if (!captured) return
    setPhase('uploading')
    uploadMutation.mutate(captured)
  }, [captured, uploadMutation])

  return {
    // refs
    cameraRef,
    ringProgress,

    // camera state
    facing,
    flash,
    toggleFacing,
    toggleFlash,

    // permissions
    cameraPerm,
    requestCameraPerm,

    // phase + capture
    phase,
    captured,
    isRecording,
    resetCapture,

    // actions
    pickFromLibrary,
    takePhoto,
    startRecording,
    stopRecording,
    publish,
    cancel: () => router.back(),
  }
}
