use std::fs::File;
use std::path::Path;

use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;

use lumotia_core::error::{Error, Result};
use lumotia_core::types::AudioSamples;

/// Decode an audio file to mono f32 PCM samples.
/// Supports all formats symphonia handles: mp3, aac, flac, wav, ogg, etc.
///
/// Any read- or decode-side error is propagated as `Error::AudioDecodeFailed`.
/// A previous implementation `break`ed out of the packet loop on any read
/// error and skipped per-packet decode errors, so a truncated or corrupt
/// input silently returned `Ok` with whatever had decoded before the
/// failure — flagged by the 2026-04-22 review (RB-09).
pub fn decode_audio_file(path: &Path) -> Result<AudioSamples> {
    decode_audio_file_limited(path, None)
}

pub fn decode_audio_file_limited(
    path: &Path,
    max_duration_secs: Option<f64>,
) -> Result<AudioSamples> {
    let file =
        File::open(path).map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
    let mss = MediaSourceStream::new(Box::new(file), Default::default());

    let mut hint = Hint::new();
    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
        hint.with_extension(ext);
    }

    decode_media_stream(mss, &hint, max_duration_secs)
}

pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
    let file =
        File::open(path).map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
    let mss = MediaSourceStream::new(Box::new(file), Default::default());
    let mut hint = Hint::new();
    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
        hint.with_extension(ext);
    }

    let probed = symphonia::default::get_probe()
        .format(
            &hint,
            mss,
            &FormatOptions::default(),
            &MetadataOptions::default(),
        )
        .map_err(|e| Error::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
    let track = probed
        .format
        .default_track()
        .ok_or_else(|| Error::AudioDecodeFailed("No audio track found".into()))?;
    let sample_rate = track
        .codec_params
        .sample_rate
        .ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?;
    Ok(track
        .codec_params
        .n_frames
        .map(|frames| frames as f64 / sample_rate as f64))
}

/// Decode from an already-constructed `MediaSourceStream`. Split out so
/// tests can inject a custom `MediaSource` (for example, one that
/// returns a mid-stream I/O error) to verify error propagation.
fn decode_media_stream(
    mss: MediaSourceStream,
    hint: &Hint,
    max_duration_secs: Option<f64>,
) -> Result<AudioSamples> {
    let probed = symphonia::default::get_probe()
        .format(
            hint,
            mss,
            &FormatOptions::default(),
            &MetadataOptions::default(),
        )
        .map_err(|e| Error::AudioDecodeFailed(format!("Unsupported format: {e}")))?;

    let mut format = probed.format;

    let track = format
        .default_track()
        .ok_or_else(|| Error::AudioDecodeFailed("No audio track found".into()))?;
    let sample_rate = track
        .codec_params
        .sample_rate
        .ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?;

    if sample_rate == 0 {
        return Err(Error::AudioDecodeFailed("Invalid sample rate: 0".into()));
    }

    let track_id = track.id;
    let max_samples = max_duration_secs.map(|secs| (secs * sample_rate as f64).ceil() as usize);

    let mut decoder = symphonia::default::get_codecs()
        .make(&track.codec_params, &DecoderOptions::default())
        .map_err(|e| Error::AudioDecodeFailed(format!("Codec error: {e}")))?;

    let mut samples: Vec<f32> = Vec::new();

    loop {
        let packet = match format.next_packet() {
            Ok(p) => p,
            Err(SymphoniaError::IoError(ref e))
                if e.kind() == std::io::ErrorKind::UnexpectedEof =>
            {
                // Normal end of stream — symphonia signals EOF via UnexpectedEof.
                break;
            }
            Err(SymphoniaError::ResetRequired) => {
                return Err(Error::AudioDecodeFailed(
                    "decoder reset required mid-stream — input contains a discontinuity".into(),
                ));
            }
            Err(e) => {
                return Err(Error::AudioDecodeFailed(format!("packet read failed: {e}")));
            }
        };

        if packet.track_id() != track_id {
            continue;
        }

        let decoded = decoder
            .decode(&packet)
            .map_err(|e| Error::AudioDecodeFailed(format!("packet decode failed: {e}")))?;

        let spec = *decoded.spec();
        let channels = spec.channels.count();
        let mut sample_buf = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
        sample_buf.copy_interleaved_ref(decoded);

        let buf = sample_buf.samples();
        if channels == 1 {
            samples.extend_from_slice(buf);
        } else {
            for chunk in buf.chunks(channels) {
                let sum: f32 = chunk.iter().sum();
                samples.push(sum / channels as f32);
            }
        }
        if max_samples
            .map(|limit| samples.len() > limit)
            .unwrap_or(false)
        {
            return Err(Error::AudioDecodeFailed(format!(
                "Audio is longer than the {:.0} minute import limit",
                max_duration_secs.unwrap_or(0.0) / 60.0
            )));
        }
    }

    if samples.is_empty() {
        return Err(Error::AudioDecodeFailed("No audio data decoded".into()));
    }

    Ok(AudioSamples::new(samples, sample_rate, 1))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::wav::write_wav;
    use std::io::{Cursor, Read, Seek, SeekFrom};
    use symphonia::core::io::MediaSource;

    fn temp_path(name: &str) -> std::path::PathBuf {
        let mut p = std::env::temp_dir();
        p.push(name);
        let _ = std::fs::remove_file(&p);
        p
    }

    fn valid_wav_bytes(sample_count: usize) -> Vec<u8> {
        let path = temp_path("lumotia_decode_tmp_for_bytes.wav");
        let samples: Vec<f32> = (0..sample_count).map(|i| (i as f32) / 1000.0).collect();
        let audio = AudioSamples::mono_16khz(samples);
        write_wav(&path, &audio).unwrap();
        let bytes = std::fs::read(&path).unwrap();
        std::fs::remove_file(&path).ok();
        bytes
    }

    /// A `MediaSource` that wraps a byte buffer and returns an injected
    /// I/O error once more than `fail_after_bytes` total bytes have been
    /// returned successfully. Simulates real-world disk or network read
    /// failure mid-stream.
    struct FlakyCursor {
        inner: Cursor<Vec<u8>>,
        fail_after_bytes: u64,
        bytes_read: u64,
    }

    impl Read for FlakyCursor {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            if self.bytes_read >= self.fail_after_bytes {
                return Err(std::io::Error::other("injected mid-stream read error"));
            }
            let n = self.inner.read(buf)?;
            self.bytes_read = self.bytes_read.saturating_add(n as u64);
            Ok(n)
        }
    }

    impl Seek for FlakyCursor {
        fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
            self.inner.seek(pos)
        }
    }

    impl MediaSource for FlakyCursor {
        fn is_seekable(&self) -> bool {
            true
        }
        fn byte_len(&self) -> Option<u64> {
            Some(self.inner.get_ref().len() as u64)
        }
    }

    #[test]
    fn decodes_valid_wav_successfully() {
        let path = temp_path("lumotia_decode_valid.wav");
        let samples: Vec<f32> = (0..4_000).map(|i| (i as f32) / 1000.0).collect();
        write_wav(&path, &AudioSamples::mono_16khz(samples)).unwrap();

        let loaded = decode_audio_file(&path).expect("valid WAV must decode");
        assert_eq!(loaded.sample_rate(), 16_000);
        assert!(!loaded.samples().is_empty());

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn missing_file_surfaces_error() {
        let path = temp_path("lumotia_decode_missing.wav");
        let result = decode_audio_file(&path);
        assert!(result.is_err(), "missing file must error, got: {result:?}");
    }

    // RB-09 regression: once probe has succeeded, any mid-stream I/O
    // error must surface as `Err(AudioDecodeFailed)` rather than being
    // silently swallowed and returning whatever was decoded so far.
    //
    // Pre-fix behaviour: the packet loop had `Err(_) => break`, so an
    // I/O error during `format.next_packet()` dropped out of the loop
    // and the function returned `Ok` with partial samples.
    #[test]
    fn mid_stream_io_error_propagates_instead_of_returning_partial_audio() {
        let bytes = valid_wav_bytes(16_000);
        // Fail after ~1 KiB — probe has seen the RIFF/WAVE header by then,
        // so probing succeeds. The packet loop hits our injected error
        // before the stream reaches its natural EOF.
        let flaky = FlakyCursor {
            inner: Cursor::new(bytes),
            fail_after_bytes: 1024,
            bytes_read: 0,
        };
        let mss = MediaSourceStream::new(Box::new(flaky), Default::default());
        let mut hint = Hint::new();
        hint.with_extension("wav");

        let result = decode_media_stream(mss, &hint, None);
        assert!(
            result.is_err(),
            "mid-stream I/O error must surface, got: {result:?}"
        );
    }
}
