/* eslint-disable react-hooks/exhaustive-deps */
import React, {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {AppState} from 'react-native';
import {useTranslation} from 'react-i18next';
import {AudioModule, PermissionStatus} from 'expo-audio';
import {SharedValue, useSharedValue} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import {activateKeepAwakeAsync, deactivateKeepAwake} from 'expo-keep-awake';
import {TrackPlayer} from '@modules/chats/instances/TrackPlayer';
import {
  MIN_AUDIO_RECORDING_DURATION_MS,
  MAX_AUDIO_RECORDING_DURATION_MS,
} from '@modules/chats/constants';
import {showSnackbar} from '@redux/dispatchers';
import {AudioRecorderService} from '@services/AudioRecorderService';
import {isAndroid, sentryLogs} from '@shared/utils';

import {AudioRecordingAnimatedProvider} from './AudioRecordingAnimatedContext';

export interface DurationLimits {
  minMs: number;
  maxMs: number;
  onMinLimit: () => void;
  onMaxLimit: () => void;
}

export type AudioRecordingStatus = 'idle' | 'recording' | 'paused' | 'stopped';

interface AudioRecordingContextValue {
  /**
   * A shared value indicating whether the user has granted permission to record audio.
   * Using a shared value to avoid re-rendering the component & using in worklets.
   */
  isPermissionGranted: SharedValue<boolean>;
  /**
   * A shared value with the current audio metering level (0-1).
   * Updated from native every ~100ms. Drives UI-thread animations without re-renders.
   */
  meteringLevel: SharedValue<number>;
  /**
   * The current status of the audio recording
   * - idle: no audio recording is in progress
   * - recording: audio recording is in progress
   * - paused: audio recording is paused
   * - stopped: audio recording is stopped -> `audioUri` is ready to be sent
   */
  status: AudioRecordingStatus;
  /**
   * The URI of the audio recording
   */
  audioUri: Nullable<string>;
  /**
   * The duration of the audio recording in seconds
   */
  audioDurationSeconds: number;
  /**
   * A ref to the duration of the audio recording in milliseconds
   */
  audioDurationMsRef: React.RefObject<number>;
  /**
   * Start the audio recording process.
   * Optionally pass duration limits to override the defaults.
   */
  startRecording: (limits?: DurationLimits) => Promise<void>;
  /**
   * Stop the audio recording process
   */
  stopRecording: () => Promise<Nullable<string> | undefined>;
  /**
   * Pause the audio recording process
   */
  pauseRecording: () => Promise<void>;
  /**
   * Resume the audio recording process
   */
  resumeRecording: () => Promise<void>;
  /**
   * Reset the audio recording process to idle state
   */
  resetAudioRecording: () => void;
  /**
   * Cancel the audio recording process
   */
  cancelRecording: (params?: {withHaptic?: boolean}) => void;
  /**
   * Request the audio recording permission
   */
  requestPermission: (
    successCallback?: () => void,
  ) => Promise<{granted: boolean} | undefined>;
  /**
   * Save the audio and set the status to stopped.
   * This is used when audio is created outside of this context.
   */
  saveAudioFromExternalSource: (uri: string) => void;
}

interface Props {
  children: ReactNode;
}

const AudioRecordingContext = createContext<
  AudioRecordingContextValue | undefined
>(undefined);

// Android native emits linear RMS (rms/32768) while iOS emits dB-normalized.
// Convert Android's linear value to the same dB-normalized 0-1 scale (-60 dB floor)
// so downstream consumers can use a single threshold/curve.
const METERING_MIN_DB = -60;
const normalizeMetering = (value: number): number => {
  if (!isAndroid) {
    return value;
  }
  if (value <= 0) {
    return 0;
  }
  const db = 20 * Math.log10(value);
  const clamped = Math.min(Math.max(db, METERING_MIN_DB), 0);
  return (clamped - METERING_MIN_DB) / -METERING_MIN_DB;
};

export const AudioRecordingProvider = ({children}: Props) => {
  const {t} = useTranslation();
  const [status, setStatus] = useState<AudioRecordingStatus>('idle');
  const statusRef = useRef<AudioRecordingStatus>(status);
  statusRef.current = status;
  const [audioUri, setAudioUri] = useState<Nullable<string>>(null);
  const isPermissionGranted = useSharedValue<boolean>(false);
  const meteringLevel = useSharedValue(0);
  const audioDurationMsRef = useRef(0);
  const [audioDurationSeconds, setAudioDurationSeconds] = useState(0);

  const durationLimitsRef = useRef<DurationLimits | null>(null);

  const stopRecording = useCallback(async () => {
    try {
      setStatus('stopped');
      const {path, duration} = await AudioRecorderService.stop();
      const limits = durationLimitsRef.current;

      if (limits?.minMs && duration > 0 && duration < limits.minMs) {
        limits.onMinLimit();
        throw new Error(
          'Audio recording duration is less than the minimum duration',
        );
      }

      audioDurationMsRef.current = duration;
      return path;
    } catch {
      return null;
    } finally {
      deactivateKeepAwake();
    }
  }, []);

  const saveAudioFromExternalSource = useCallback((uri: string) => {
    try {
      setAudioUri(uri);
      setStatus('stopped');
    } catch {
      // DO nothing
    }
  }, []);

  const resetAudioRecording = useCallback(() => {
    setStatus('idle');
    audioDurationMsRef.current = 0;
    setAudioDurationSeconds(0);
    meteringLevel.value = 0;
    durationLimitsRef.current = null;
  }, []);

  const pauseRecording = useCallback(async () => {
    if (statusRef.current !== 'recording') {
      return;
    }

    setStatus('paused');
    await AudioRecorderService.pause().catch(() => {});
  }, []);

  const resumeRecording = useCallback(async () => {
    if (statusRef.current !== 'paused') {
      return;
    }

    setStatus('recording');
    await AudioRecorderService.resume().catch(() => {});
  }, []);

  const requestPermission = useCallback(
    async (successCallback?: () => void) => {
      try {
        if (isPermissionGranted.value) {
          showSnackbar({
            title: t('chats.record_press_error'),
            variant: 'info',
          });
          return {granted: true};
        }

        const result = await AudioModule.requestRecordingPermissionsAsync();
        isPermissionGranted.value = result.status === PermissionStatus.GRANTED;

        if (result.status === PermissionStatus.GRANTED) {
          showSnackbar({
            title: t('chats.ready_to_record'),
            variant: 'info',
          });
          successCallback?.();
        } else {
          showSnackbar({
            title: t('chats.no_permissions_error'),
            variant: 'info',
          });
        }

        return {granted: result.granted};
      } catch {
        return undefined;
      }
    },
    [],
  );

  const defaultDurationLimits: DurationLimits = {
    minMs: MIN_AUDIO_RECORDING_DURATION_MS,
    maxMs: MAX_AUDIO_RECORDING_DURATION_MS,
    onMinLimit: () => {
      Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
    },
    onMaxLimit: () => {
      showSnackbar({
        title: t('chat.recording.max_limit', {
          max: Math.floor(MAX_AUDIO_RECORDING_DURATION_MS / 60000),
        }),
        variant: 'info',
      });
      stopRecording();
    },
  };

  const startRecording = useCallback(
    async (limits: DurationLimits = defaultDurationLimits) => {
      TrackPlayer.pause();

      if (!isPermissionGranted.value) {
        const permissionResult = await requestPermission();
        if (!permissionResult?.granted) {
          return;
        }
      }

      try {
        resetAudioRecording();
        durationLimitsRef.current = limits;
        Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
        activateKeepAwakeAsync();
        setStatus('recording');
        const path = await AudioRecorderService.record();
        setAudioUri(path ?? null);
      } catch (error) {
        sentryLogs.error({
          message: 'Error starting recording',
          module: 'CHATS',
          component: 'AudioRecordingContext',
          data: {
            error: error instanceof Error ? error.message : String(error),
          },
        });
        setStatus('idle');
      }
    },
    [],
  );

  const cancelRecording = useCallback(
    ({withHaptic}: {withHaptic?: boolean} = {withHaptic: true}) => {
      if (withHaptic) {
        Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
      }
      AudioRecorderService.stop().catch(() => {});
      deactivateKeepAwake();
      resetAudioRecording();
    },

    [],
  );

  /**
   * Listen to the audio recording duration and stop the recording if the duration limit is reached
   */
  useEffect(() => {
    const listener = AudioRecorderService.addListener(
      ({duration, metering}) => {
        setAudioDurationSeconds(Math.floor(duration / 1000));
        meteringLevel.value = normalizeMetering(metering ?? 0);
        const limits = durationLimitsRef.current;
        if (limits?.maxMs && duration >= limits.maxMs) {
          limits.onMaxLimit();
        }
      },
    );

    return () => {
      listener.remove();
    };
  }, []);

  /**
   * Get the recording permissions
   */
  useEffect(() => {
    AudioModule.getRecordingPermissionsAsync()
      .then(result => {
        isPermissionGranted.value = result.status === PermissionStatus.GRANTED;
      })
      .catch(() => {});

    return () => {
      if (statusRef.current === 'recording' || statusRef.current === 'paused') {
        AudioRecorderService.stop().catch(() => {
          // Handle error silently
        });
      }
    };
  }, []);

  /**
   * Pause recording when the app goes to background or the device is locked
   */
  useEffect(() => {
    const listener = AppState.addEventListener('change', state => {
      if (state === 'background' || state === 'inactive') {
        pauseRecording();
      }
    });

    return () => {
      listener.remove();
    };
  }, []);

  const stateValue = useMemo(
    (): AudioRecordingContextValue => ({
      isPermissionGranted,
      meteringLevel,
      status,
      audioUri,
      audioDurationMsRef,
      audioDurationSeconds,
      startRecording,
      stopRecording,
      pauseRecording,
      resumeRecording,
      resetAudioRecording,
      requestPermission,
      cancelRecording,
      saveAudioFromExternalSource,
    }),
    [
      isPermissionGranted,
      meteringLevel,
      status,
      audioUri,
      audioDurationMsRef,
      audioDurationSeconds,
      startRecording,
      stopRecording,
      pauseRecording,
      resumeRecording,
      resetAudioRecording,
      requestPermission,
      cancelRecording,
      saveAudioFromExternalSource,
    ],
  );

  return (
    <AudioRecordingContext.Provider value={stateValue}>
      <AudioRecordingAnimatedProvider>
        {children}
      </AudioRecordingAnimatedProvider>
    </AudioRecordingContext.Provider>
  );
};

export const useAudioRecording = (): AudioRecordingContextValue => {
  const context = useContext(AudioRecordingContext);
  if (!context) {
    throw new Error(
      'useAudioRecording must be used within an AudioRecordingProvider',
    );
  }
  return context;
};
