use crate::texture::TextureManager;
use anyhow::{anyhow, Result};
use gst::prelude::*;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gstreamer_video as gst_video;
use log::{debug, error, info, warn};
use std::collections::VecDeque;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use wgpu;

/// Cap raw PCM buffer at ~10 seconds so memory stays bounded when the
/// consumer is slow or absent. Oldest samples drop first.
const PCM_BUFFER_CAP: usize = 44100 * 10;

/// Raw PCM tap output rate — mono `f32` samples. Consumers that need a
/// different rate should resample on their side.
const PCM_SAMPLE_RATE: u32 = 44100;

/// Elements that make up the parallel raw PCM tap branch. Built optimistically
/// when audio first arrives; if construction fails the analyzer chain still
/// runs but the tap is reported absent via `has_pcm`.
struct PcmTapBranch {
    tee: gst::Element,
    queue_main: gst::Element,
    queue_pcm: gst::Element,
    audioconvert2: gst::Element,
    audioresample2: gst::Element,
    caps_filter: gst::Element,
    appsink: gst_app::AppSink,
}

/// Try to construct the PCM tap elements. Returns `None` if any element
/// fails to build — caller falls back to the plain analyzer chain.
fn build_pcm_branch() -> Option<PcmTapBranch> {
    let tee = gst::ElementFactory::make("tee").name("video_audio_tee").build().ok()?;
    let queue_main = gst::ElementFactory::make("queue").name("video_q_main").build().ok()?;
    let queue_pcm = gst::ElementFactory::make("queue").name("video_q_pcm").build().ok()?;
    let audioconvert2 = gst::ElementFactory::make("audioconvert")
        .name("video_pcm_convert")
        .build()
        .ok()?;
    let audioresample2 = gst::ElementFactory::make("audioresample")
        .name("video_pcm_resample")
        .build()
        .ok()?;
    let caps_filter = gst::ElementFactory::make("capsfilter")
        .name("video_pcm_caps")
        .build()
        .ok()?;
    caps_filter.set_property(
        "caps",
        &gst::Caps::builder("audio/x-raw")
            .field("format", "F32LE")
            .field("rate", PCM_SAMPLE_RATE as i32)
            .field("channels", 1i32)
            .build(),
    );
    let raw_sink = gst::ElementFactory::make("appsink")
        .name("video_pcm_sink")
        .build()
        .ok()?;
    let appsink = raw_sink.dynamic_cast::<gst_app::AppSink>().ok()?;
    appsink.set_caps(Some(
        &gst::Caps::builder("audio/x-raw")
            .field("format", "F32LE")
            .field("rate", PCM_SAMPLE_RATE as i32)
            .field("channels", 1i32)
            .build(),
    ));
    appsink.set_max_buffers(8);
    appsink.set_drop(true);
    appsink.set_sync(false);
    Some(PcmTapBranch {
        tee,
        queue_main,
        queue_pcm,
        audioconvert2,
        audioresample2,
        caps_filter,
        appsink,
    })
}

#[derive(Debug, Clone, Default)]
pub struct SpectrumData {
    /// Number of frequency bands
    pub bands: usize,
    /// Magnitude values for each frequency band in dB
    pub magnitudes: Vec<f32>,
    /// Phase values for each frequency band
    pub phases: Option<Vec<f32>>,
    /// Timestamp of the spectrum data
    pub timestamp: Option<gst::ClockTime>,
}

#[derive(Debug, Clone, Default)]
pub struct AudioLevel {
    /// RMS in linear scale (0.0 to 1.0)
    pub rms: f64,
    /// RMS in decibels
    pub rms_db: f64,
    /// Peak value in linear scale (0.0 to 1.0)
    pub peak: f64,
}

/// Here I created a struct to organize the video text mang.
/// Manages a video texture that can be updated frame by frame
pub struct VideoTextureManager {
    /// The underlying TextureManager that handles the WGPU resources
    texture_manager: TextureManager,
    /// The GStreamer pipeline for video decoding
    pipeline: gst::Pipeline,
    /// The AppSink element that receives decoded frames
    appsink: gst_app::AppSink,
    /// Whether the video has an audio track
    has_audio: bool,
    /// Whether this is an audio-only file (no video track)
    audio_only: Arc<Mutex<bool>>,
    /// Audio volume (0.0 to 1.0)
    volume: Arc<Mutex<f64>>,
    /// Whether audio is muted
    is_muted: Arc<Mutex<bool>>,
    /// Current video dimensions
    dimensions: (u32, u32),
    /// Video duration in nanoseconds (if available)
    duration: Option<gst::ClockTime>,
    /// Current position in the video
    position: Arc<Mutex<gst::ClockTime>>,
    /// Frame rate of the video
    framerate: Option<gst::Fraction>,
    /// Whether the video is currently playing
    is_playing: Arc<Mutex<bool>>,
    /// Whether to loop the video when it ends
    loop_playback: Arc<Mutex<bool>>,
    /// Last frame update time
    last_update: Instant,
    /// Frame buffer for the most recently decoded frame
    current_frame: Arc<Mutex<Option<image::RgbaImage>>>,
    /// Path to the video file
    video_path: String,
    /// Whether the video texture has been initialized
    texture_initialized: bool,
    /// Frame counter for debugging
    frame_count: usize,
    /// Spectrum analysis enabled
    spectrum_enabled: bool,
    /// Number of frequency bands for spectrum analysis
    spectrum_bands: usize,
    /// Threshold in dB for spectrum analysis
    spectrum_threshold: i32,
    /// Spectrum data from the most recent analysis
    spectrum_data: Arc<Mutex<SpectrumData>>,
    /// Audio level data (RMS, peak) for normalization
    audio_level: Arc<Mutex<AudioLevel>>,
    /// bpm
    bpm_value: Arc<Mutex<f32>>,
    /// Whether video track was found
    has_video: Arc<Mutex<bool>>,
    /// Whether the parallel raw PCM appsink branch attached successfully.
    /// Set inside the lazy audio-pipeline builder when the tap wires up.
    has_pcm: Arc<AtomicBool>,
    /// Bounded ring buffer of raw `f32` mono PCM samples at
    /// `PCM_SAMPLE_RATE`. Drained with `pop_pcm_samples` — oldest samples
    /// drop when the buffer fills.
    pcm_samples: Arc<Mutex<VecDeque<f32>>>,
}

impl VideoTextureManager {
    pub fn new(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        bind_group_layout: &wgpu::BindGroupLayout,
        video_path: impl AsRef<Path>,
    ) -> Result<Self> {
        // Create a default 1x1 texture initially, note that, this going to be replaced with first video frame
        let default_image = image::RgbaImage::new(1, 1);
        let texture_manager = TextureManager::new(device, queue, &default_image, bind_group_layout);

        let path_str = video_path
            .as_ref()
            .to_str()
            .ok_or_else(|| anyhow!("Invalid video path"))?
            .to_string();

        info!("Creating video texture from: {path_str}");

        let pipeline = gst::Pipeline::new();

        // Source element - read from file
        let filesrc = gst::ElementFactory::make("filesrc")
            .name("source")
            .property("location", &path_str)
            .build()
            .map_err(|_| anyhow!("Failed to create filesrc element"))?;
        // Decoding element
        let decodebin = gst::ElementFactory::make("decodebin")
            .name("decoder")
            .build()
            .map_err(|_| anyhow!("Failed to create decodebin element"))?;

        // videorate element to enforce correct frame timing
        let videorate = gst::ElementFactory::make("videorate")
            .name("rate")
            .build()
            .map_err(|_| anyhow!("Failed to create videorate element"))?;
        // Convert to proper format
        let videoconvert = gst::ElementFactory::make("videoconvert")
            .name("convert")
            .build()
            .map_err(|_| anyhow!("Failed to create videoconvert element"))?;
        //caps filter to force framerate if needed
        let capsfilter = gst::ElementFactory::make("capsfilter")
            .name("capsfilter")
            .build()
            .map_err(|_| anyhow!("Failed to create capsfilter element"))?;

        // Output sink for video
        let appsink = gst::ElementFactory::make("appsink")
            .name("sink")
            .build()
            .map_err(|_| anyhow!("Failed to create appsink element"))?;

        let appsink = appsink
            .dynamic_cast::<gst_app::AppSink>()
            .map_err(|_| anyhow!("Failed to cast to AppSink"))?;
        // Configure appsink
        appsink.set_caps(Some(
            &gst::Caps::builder("video/x-raw")
                .field("format", gst_video::VideoFormat::Rgba.to_str())
                .build(),
        ));
        appsink.set_max_buffers(2);
        // Drop old buffers when full
        appsink.set_drop(true);
        appsink.set_sync(true);

        // video elements goes to the pipeline
        pipeline
            .add_many([
                &filesrc,
                &decodebin,
                &videorate,
                &videoconvert,
                &capsfilter,
                appsink.upcast_ref(),
            ])
            .map_err(|_| anyhow!("Failed to add video elements to pipeline"))?;

        // Link elements that can be linked statically
        gst::Element::link_many([&videorate, &videoconvert, &capsfilter, appsink.upcast_ref()])
            .map_err(|_| anyhow!("Failed to link video elements"))?;

        gst::Element::link_many([&filesrc, &decodebin])
            .map_err(|_| anyhow!("Failed to link filesrc to decodebin"))?;

        // Set up pad-added signal for dynamic linking from decodebin -> videorate
        let videorate_weak = videorate.downgrade();
        let has_audio = Arc::new(Mutex::new(false));
        let has_audio_clone = has_audio.clone();
        let has_video = Arc::new(Mutex::new(false));
        let has_video_clone = has_video.clone();

        // now audio elements reference holders
        let audioconvert_weak = Arc::new(Mutex::new(None));

        // Default spectrum configuration
        let spectrum_bands = 128;
        let spectrum_threshold = -60;
        let spectrum_enabled = true;
        let spectrum_data = Arc::new(Mutex::new(SpectrumData::default()));
        let audio_level = Arc::new(Mutex::new(AudioLevel::default()));

        // Raw PCM tap state — captured into the pad-added closure so the
        // appsink callback can write into it. `has_pcm` flips to true if
        // the tap successfully attaches when the audio pad arrives.
        let has_pcm = Arc::new(AtomicBool::new(false));
        let has_pcm_for_closure = has_pcm.clone();
        let pcm_samples = Arc::new(Mutex::new(VecDeque::with_capacity(PCM_BUFFER_CAP)));
        let pcm_samples_for_closure = pcm_samples.clone();

        decodebin.connect_pad_added(move |_, pad| {
            let caps = match pad.current_caps() {
                Some(caps) => caps,
                _ => return,
            };

            let structure = match caps.structure(0) {
                Some(s) => s,
                _ => return,
            };
            // Check if this is a video or audio stream. maybe there could be other way to handle this but I love simpicity
            if structure.name().starts_with("video/") {
                // Mark that we have a video track
                if let Ok(mut has_video_lock) = has_video_clone.lock() {
                    *has_video_lock = true;
                    info!("Video track detected");
                }
                // Handle video path
                if let Some(videorate) = videorate_weak.upgrade() {
                    let sink_pad = match videorate.static_pad("sink") {
                        Some(pad) => pad,
                        _ => return,
                    };

                    if !sink_pad.is_linked() {
                        let _ = pad.link(&sink_pad);
                        info!("Linked decoder to videorate successfully");
                    }
                }
            } else if structure.name().starts_with("audio/") {
                // has_audio flag to true - we've detected an audio stream
                if let Ok(mut has_audio_lock) = has_audio_clone.lock() {
                    *has_audio_lock = true;
                    info!("Audio track detected in video");
                }

                // Now lets dynamically create the audio processing chain
                if let Ok(mut audioconvert_lock) = audioconvert_weak.lock() {
                    // Only create audio elements once when first audio pad is detected
                    if audioconvert_lock.is_none() {
                        // Create audio elements
                        let audioconvert = match gst::ElementFactory::make("audioconvert")
                            .name("audioconvert")
                            .build()
                        {
                            Ok(e) => e,
                            Err(_) => {
                                warn!("Failed to create audioconvert");
                                return;
                            }
                        };

                        let audioresample = match gst::ElementFactory::make("audioresample")
                            .name("audioresample")
                            .build()
                        {
                            Ok(e) => e,
                            Err(_) => {
                                warn!("Failed to create audioresample");
                                return;
                            }
                        };

                        // level element for RMS/peak/decay analysis
                        let level = match gst::ElementFactory::make("level")
                            .name("level")
                            .property("interval", 50000000u64)  // 50ms intervals (matches spectrum)
                            .property("message", true)
                            .property("post-messages", true)
                            .build()
                        {
                            Ok(e) => {
                                info!("Created level analyzer element");
                                e
                            }
                            Err(_) => {
                                warn!("Failed to create level element");
                                return;
                            }
                        };

                        let bpmdetect = match gst::ElementFactory::make("bpmdetect")
                            .name("bpmdetect")
                            .build()
                        {
                            Ok(e) => {
                                info!("Created BPM detector");
                                e
                            }
                            Err(_) => {
                                warn!("Failed to create BPM detector");
                                return;
                            }
                        };
                        let spectrum = match gst::ElementFactory::make("spectrum")
                            .name("spectrum")
                            .property("bands", spectrum_bands as u32)
                            .property("threshold", spectrum_threshold)
                            .property("post-messages", true)
                            .property("message-magnitude", true)
                            .property("message-phase", false)
                            .property("interval", 50000000u64)
                            .build()
                        {
                            Ok(e) => {
                                info!(
                                    "Created spectrum analyzer with {spectrum_bands} bands and threshold {spectrum_threshold}dB"
                                );
                                e
                            }
                            Err(_) => {
                                warn!("Failed to create spectrum analyzer");
                                return;
                            }
                        };
                        let volume = match gst::ElementFactory::make("volume")
                            .name("volume")
                            .property("volume", 1.0)
                            .build()
                        {
                            Ok(e) => e,
                            Err(_) => {
                                warn!("Failed to create volume");
                                return;
                            }
                        };

                        // autoaudiosink should works on all platforms: https://gstreamer.freedesktop.org/documentation/autodetect/autoaudiosink.html?gi-language=c
                        let audio_sink = match gst::ElementFactory::make("autoaudiosink")
                            .name("audiosink")
                            .build()
                        {
                            Ok(e) => e,
                            Err(_) => {
                                warn!("Failed to create autoaudiosink");
                                return;
                            }
                        };

                        // Optional raw PCM tap. The tap lives on a parallel
                        // branch of a tee after audioresample so the analyzer
                        // chain and playback stay unchanged. If any of the
                        // tap elements fail to build, we silently skip the
                        // tap (audio playback / analysis still works) — only
                        // the consumer-facing `has_pcm()` reports false.
                        let pcm_branch = build_pcm_branch();

                        let parent_pipeline = pad
                            .parent_element()
                            .unwrap()
                            .parent()
                            .unwrap()
                            .downcast_ref::<gst::Pipeline>()
                            .unwrap()
                            .clone();

                        let analyzer_chain: &[&gst::Element] = &[
                            &audioconvert,
                            &audioresample,
                            &level,
                            &bpmdetect,
                            &spectrum,
                            &volume,
                            &audio_sink,
                        ];
                        if let Err(e) = parent_pipeline.add_many(analyzer_chain) {
                            warn!("Failed to add audio elements: {e:?}");
                            return;
                        }

                        let mut pcm_attached = false;
                        if let Some(branch) = &pcm_branch {
                            // Wire the appsink callback to the shared ring buffer.
                            let pcm_buffer = pcm_samples_for_closure.clone();
                            branch.appsink.set_callbacks(
                                gst_app::AppSinkCallbacks::builder()
                                    .new_sample(move |sink| {
                                        let sample = sink
                                            .pull_sample()
                                            .map_err(|_| gst::FlowError::Eos)?;
                                        let buffer = sample
                                            .buffer()
                                            .ok_or(gst::FlowError::Error)?;
                                        let map = buffer
                                            .map_readable()
                                            .map_err(|_| gst::FlowError::Error)?;
                                        let bytes = map.as_slice();
                                        let samples: &[f32] =
                                            bytemuck::cast_slice(bytes);
                                        if let Ok(mut q) = pcm_buffer.lock() {
                                            for &s in samples {
                                                if q.len() >= PCM_BUFFER_CAP {
                                                    q.pop_front();
                                                }
                                                q.push_back(s);
                                            }
                                        }
                                        Ok(gst::FlowSuccess::Ok)
                                    })
                                    .build(),
                            );

                            // Add tap elements to the pipeline. Failure here
                            // shouldn't kill the analyzer chain — drop the
                            // tap and continue with the plain linear link.
                            let add_ok = parent_pipeline
                                .add_many([
                                    &branch.tee,
                                    &branch.queue_main,
                                    &branch.queue_pcm,
                                    &branch.audioconvert2,
                                    &branch.audioresample2,
                                    &branch.caps_filter,
                                    branch.appsink.upcast_ref::<gst::Element>(),
                                ])
                                .is_ok();
                            if add_ok {
                                // Link audioconvert → audioresample → tee →
                                //   queue_main → level → bpmdetect → spectrum → volume → audio_sink
                                //   queue_pcm  → audioconvert2 → audioresample2 → caps → appsink
                                let upstream_ok = gst::Element::link_many([
                                    &audioconvert,
                                    &audioresample,
                                    &branch.tee,
                                ])
                                .is_ok();
                                let main_ok = gst::Element::link_many([
                                    &branch.tee,
                                    &branch.queue_main,
                                    &level,
                                    &bpmdetect,
                                    &spectrum,
                                    &volume,
                                    &audio_sink,
                                ])
                                .is_ok();
                                let pcm_ok = gst::Element::link_many([
                                    &branch.tee,
                                    &branch.queue_pcm,
                                    &branch.audioconvert2,
                                    &branch.audioresample2,
                                    &branch.caps_filter,
                                    branch.appsink.upcast_ref::<gst::Element>(),
                                ])
                                .is_ok();

                                if upstream_ok && main_ok && pcm_ok {
                                    let _ = branch.tee.sync_state_with_parent();
                                    let _ = branch.queue_main.sync_state_with_parent();
                                    let _ = branch.queue_pcm.sync_state_with_parent();
                                    let _ = branch.audioconvert2.sync_state_with_parent();
                                    let _ = branch.audioresample2.sync_state_with_parent();
                                    let _ = branch.caps_filter.sync_state_with_parent();
                                    let _ = branch.appsink.sync_state_with_parent();
                                    pcm_attached = true;
                                } else {
                                    warn!(
                                        "Video: PCM tap link failed (up={upstream_ok} \
                                         main={main_ok} pcm={pcm_ok}); falling back"
                                    );
                                }
                            } else {
                                warn!("Video: PCM tap add_many failed; falling back");
                            }
                        }

                        if !pcm_attached {
                            // Fallback path: original linear chain, no PCM tap.
                            if let Err(e) = gst::Element::link_many([
                                &audioconvert,
                                &audioresample,
                                &level,
                                &bpmdetect,
                                &spectrum,
                                &volume,
                                &audio_sink,
                            ]) {
                                warn!("Failed to link audio elements: {e:?}");
                                return;
                            }
                        } else {
                            has_pcm_for_closure.store(true, Ordering::Relaxed);
                            info!("Video: raw PCM tap attached ({PCM_SAMPLE_RATE} Hz mono f32)");
                        }

                        // Set elements to PAUSED state
                        let _ = audioconvert.sync_state_with_parent();
                        let _ = audioresample.sync_state_with_parent();
                        let _ = level.sync_state_with_parent();
                        let _ = bpmdetect.sync_state_with_parent();
                        let _ = spectrum.sync_state_with_parent();
                        let _ = volume.sync_state_with_parent();
                        let _ = audio_sink.sync_state_with_parent();

                        *audioconvert_lock = Some(audioconvert.clone());
                    }

                    // Link decoder pad to audioconvert
                    if let Some(audioconvert) = &*audioconvert_lock {
                        let sink_pad = match audioconvert.static_pad("sink") {
                            Some(pad) => pad,
                            _ => return,
                        };

                        if !sink_pad.is_linked() {
                            match pad.link(&sink_pad) {
                                Ok(_) => {
                                    info!("Linked decoder to audioconvert successfully");
                                }
                                Err(err) => {
                                    warn!("Failed to link audio pad: {err:?}");
                                }
                            }
                        }
                    }
                }
            }
        });

        // Create shared state
        let current_frame = Arc::new(Mutex::new(None));
        let current_frame_clone = current_frame.clone();
        let position = Arc::new(Mutex::new(gst::ClockTime::ZERO));
        let is_playing = Arc::new(Mutex::new(false));
        let volume_val = Arc::new(Mutex::new(1.0));
        let is_muted = Arc::new(Mutex::new(false));

        // Setup callbacks to receive frames
        appsink.set_callbacks(
            gst_app::AppSinkCallbacks::builder()
                .new_sample(move |sink| {
                    let sample = match sink.pull_sample() {
                        Ok(sample) => sample,
                        Err(_) => return Err(gst::FlowError::Eos),
                    };

                    let buffer = match sample.buffer() {
                        Some(buffer) => buffer,
                        _ => return Err(gst::FlowError::Error),
                    };

                    let caps = match sample.caps() {
                        Some(caps) => caps,
                        _ => return Err(gst::FlowError::Error),
                    };

                    let video_info = match gst_video::VideoInfo::from_caps(caps) {
                        Ok(info) => info,
                        Err(_) => return Err(gst::FlowError::Error),
                    };

                    let map = match buffer.map_readable() {
                        Ok(map) => map,
                        Err(_) => return Err(gst::FlowError::Error),
                    };

                    // Access the raw frame data
                    let frame_data = map.as_slice();
                    let width = video_info.width() as usize;
                    let height = video_info.height() as usize;

                    // Create an RgbaImage from the frame data
                    // (We need to copy the data because buffer will be unmapped after this function)
                    let mut rgba_image = image::RgbaImage::new(width as u32, height as u32);

                    // Stride might be larger than width * 4
                    let stride = video_info.stride()[0] as usize;

                    for y in 0..height {
                        let src_start = y * stride;
                        let src_end = src_start + width * 4;
                        let dst_start = y * width * 4;
                        let dst_end = dst_start + width * 4;

                        // Copy row by row to handle stride correctly
                        let dst_buffer = rgba_image.as_mut();
                        if src_end <= frame_data.len() && dst_end <= dst_buffer.len() {
                            dst_buffer[dst_start..dst_end]
                                .copy_from_slice(&frame_data[src_start..src_end]);
                        }
                    }

                    // Store the frame
                    if let Ok(mut frame_lock) = current_frame_clone.lock() {
                        *frame_lock = Some(rgba_image);
                    }

                    Ok(gst::FlowSuccess::Ok)
                })
                .build(),
        );

        // init the object
        let mut video_texture = Self {
            texture_manager,
            pipeline,
            appsink,
            has_audio: false,
            audio_only: Arc::new(Mutex::new(false)),
            volume: volume_val,
            is_muted,
            dimensions: (1, 1),
            duration: None,
            position,
            framerate: None,
            is_playing,
            loop_playback: Arc::new(Mutex::new(true)),
            last_update: Instant::now(),
            current_frame,
            video_path: path_str,
            texture_initialized: false,
            frame_count: 0,
            spectrum_enabled,
            spectrum_bands,
            spectrum_threshold,
            spectrum_data,
            audio_level,
            bpm_value: Arc::new(Mutex::new(0.0)),
            has_video: has_video.clone(),
            has_pcm,
            pcm_samples,
        };
        // Start pipeline in paused state to get video info
        if video_texture
            .pipeline
            .set_state(gst::State::Paused)
            .is_err()
        {
            return Err(anyhow!("Failed to set pipeline to PAUSED state"));
        }

        // Wait a bit for pipeline to settle
        std::thread::sleep(Duration::from_millis(200));

        let state_result = video_texture
            .pipeline
            .state(gst::ClockTime::from_seconds(1));
        if let (_, gst::State::Paused, _) = state_result {
            video_texture.query_video_info()?;
        } else {
            warn!("Pipeline not in PAUSED state, may not be able to query info");
        }
        // Set specific framerate in the caps filter if we detected one
        if let Some(framerate) = video_texture.framerate {
            if let Some(capsfilter_elem) = video_texture.pipeline.by_name("capsfilter") {
                let caps = gst::Caps::builder("video/x-raw")
                    .field("framerate", framerate)
                    .build();
                capsfilter_elem.set_property("caps", &caps);
                info!(
                    "Set capsfilter to force framerate {}/{}",
                    framerate.numer(),
                    framerate.denom()
                );
            }
        }
        video_texture.has_audio = *has_audio.lock().unwrap();
        let has_video_track = *has_video.lock().unwrap();

        // Determine if this is an audio-only file
        if video_texture.has_audio && !has_video_track {
            *video_texture.audio_only.lock().unwrap() = true;
            info!("Audio-only file detected - no video track present");

            // For audio-only files, remove unlinked video elements from the pipeline
            if let Some(videorate_elem) = video_texture.pipeline.by_name("rate") {
                let _ = videorate_elem.set_state(gst::State::Null);
                let _ = video_texture.pipeline.remove(&videorate_elem);
                info!("Removed unused videorate element");
            }
            if let Some(convert_elem) = video_texture.pipeline.by_name("convert") {
                let _ = convert_elem.set_state(gst::State::Null);
                let _ = video_texture.pipeline.remove(&convert_elem);
                info!("Removed unused videoconvert element");
            }
            if let Some(capsfilter_elem) = video_texture.pipeline.by_name("capsfilter") {
                let _ = capsfilter_elem.set_state(gst::State::Null);
                let _ = video_texture.pipeline.remove(&capsfilter_elem);
                info!("Removed unused capsfilter element");
            }
            if let Some(sink_elem) = video_texture.pipeline.by_name("sink") {
                let _ = sink_elem.set_state(gst::State::Null);
                let _ = video_texture.pipeline.remove(&sink_elem);
                info!("Removed unused appsink element");
            }
        }

        info!("Video has audio: {}, has video: {}, audio_only: {}",
              video_texture.has_audio,
              has_video_track,
              *video_texture.audio_only.lock().unwrap());

        info!("Video texture manager created successfully");
        Ok(video_texture)
    }

    /// Query video information (dimensions, duration, framerate)
    fn query_video_info(&mut self) -> Result<()> {
        // Query duration
        if let Some(duration) = self.pipeline.query_duration::<gst::ClockTime>() {
            self.duration = Some(duration);
            info!(
                "Video duration: {:?} ({:.2} seconds)",
                duration,
                duration.seconds() as f64
            );
        }

        // Now try to get video info
        if let Some(pad) = self.appsink.static_pad("sink") {
            if let Some(caps) = pad.current_caps() {
                if let Some(s) = caps.structure(0) {
                    //  dims
                    if let (Ok(width), Ok(height)) = (s.get::<i32>("width"), s.get::<i32>("height"))
                    {
                        self.dimensions = (width as u32, height as u32);
                        info!("Video dimensions: {width}x{height}");
                    }

                    // framerate
                    if let Ok(framerate) = s.get::<gst::Fraction>("framerate") {
                        self.framerate = Some(framerate);
                        info!(
                            "Video framerate: {}/{} (approx. {:.2} fps)",
                            framerate.numer(),
                            framerate.denom(),
                            framerate.numer() as f64 / framerate.denom() as f64
                        );
                    }
                }
            }
        }

        Ok(())
    }

    /// Get the texture manager (for binding to shaders)
    pub fn texture_manager(&self) -> &TextureManager {
        &self.texture_manager
    }

    /// Update the texture with the current video frame
    pub fn update_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        bind_group_layout: &wgpu::BindGroupLayout,
    ) -> Result<bool> {
        // No update needed if video is not playing
        if !*self.is_playing.lock().unwrap() {
            return Ok(false);
        }

        let is_audio_only = *self.audio_only.lock().unwrap();
        let mut audio_updated = false;

        // ALWAYS poll for audio messages, regardless of video frame availability
        // This decouples audio processing from video frame rate
        if self.has_audio && self.spectrum_enabled {
            audio_updated = self.poll_audio_messages();
        }

        // Check if we have a NEW frame to process
        let frame_to_process = {
            let mut frame_lock = self.current_frame.lock().unwrap();
            frame_lock.take()
        };

        // If we have a frame, update the texture
        if let Some(frame) = frame_to_process {
            self.frame_count += 1;

            // Get frame dimensions
            let width = frame.width();
            let height = frame.height();

            // Log less frequently to reduce spam
            if self.frame_count % 30 == 0 {
                debug!(
                    "Processing video frame #{} (dimensions: {}x{})",
                    self.frame_count, width, height
                );
            }

            // ALWAYS recreate the texture for the first frame or if dimensions don't match
            let should_recreate = !self.texture_initialized
                || self.dimensions != (width, height)
                || self.dimensions.0 <= 1
                || self.dimensions.1 <= 1
                || self.frame_count <= 3;

            if should_recreate {
                info!("Creating new texture with dimensions: {width}x{height}");

                // Create a completely new texture with the frame's dimensions
                let new_texture_manager =
                    TextureManager::new(device, queue, &frame, bind_group_layout);

                self.texture_manager = new_texture_manager;
                self.dimensions = (width, height);
                self.texture_initialized = true;
            } else {
                self.texture_manager.update(queue, &frame);
            }

            // Get current position
            if let Some(position) = self.pipeline.query_position::<gst::ClockTime>() {
                *self.position.lock().unwrap() = position;

                // Check if we reached the end of the video
                if let Some(duration) = self.duration {
                    let near_end_threshold =
                        duration.saturating_sub(gst::ClockTime::from_mseconds(100));

                    if position >= near_end_threshold {
                        debug!(
                            "Near end of video (position: {position:?}, duration: {duration:?})"
                        );

                        if *self.loop_playback.lock().unwrap() {
                            debug!("Looping video");
                            self.seek(gst::ClockTime::ZERO)?;
                        } else {
                            debug!("Pausing at end of video");
                            self.pause()?;
                        }
                    }
                }
            }

            // Update the last update time
            self.last_update = Instant::now();

            Ok(true) // Texture was updated
        } else {
            // For audio-only files or when audio data was updated, return true
            // to signal that the shader should re-render (even without new video frames)
            if is_audio_only && audio_updated {
                // Update position for audio-only files
                if let Some(position) = self.pipeline.query_position::<gst::ClockTime>() {
                    *self.position.lock().unwrap() = position;

                    // Check if we reached the end
                    if let Some(duration) = self.duration {
                        let near_end_threshold =
                            duration.saturating_sub(gst::ClockTime::from_mseconds(100));

                        if position >= near_end_threshold {
                            if *self.loop_playback.lock().unwrap() {
                                debug!("Looping audio");
                                self.seek(gst::ClockTime::ZERO)?;
                            } else {
                                debug!("Pausing at end of audio");
                                self.pause()?;
                            }
                        }
                    }
                }
                self.last_update = Instant::now();
                return Ok(true);
            }
            // For video with low FPS but audio updated, still signal update
            if audio_updated {
                return Ok(true);
            }
            Ok(false) // No update
        }
    }

    /// Poll the GStreamer bus for audio-related messages (spectrum, level, BPM)
    /// Returns true if any audio data was updated
    fn poll_audio_messages(&mut self) -> bool {
        let mut updated = false;

        if let Some(bus) = self.pipeline.bus() {
            // Poll for pending messages
            while let Some(message) = bus.pop() {
                match message.view() {
                    gst::MessageView::Element(element) => {
                        if let Some(structure) = element.structure() {
                            // spectrum data
                            if structure.name() == "spectrum" {
                                // Extract magnitude values - two possible approaches
                                let mut magnitude_values = Vec::with_capacity(128);

                                // APPROACH 1: Parse from structure string
                                let struct_str = structure.to_string();
                                if struct_str.contains("magnitude=(float){") {
                                    // Extract magnitude values from string
                                    if let Some(start_idx) =
                                        struct_str.find("magnitude=(float){")
                                    {
                                        if let Some(end_idx) =
                                            struct_str[start_idx..].find("}")
                                        {
                                            let magnitude_str = &struct_str[start_idx
                                                + "magnitude=(float){".len()
                                                ..start_idx + end_idx];
                                            let values: Vec<&str> =
                                                magnitude_str.split(',').collect();

                                            for value_str in values {
                                                if let Ok(value) =
                                                    value_str.trim().parse::<f32>()
                                                {
                                                    magnitude_values.push(value);
                                                }
                                            }
                                        }
                                    }
                                }

                                // APPROACH 2: Try to access directly by index if approach 1 fails
                                if magnitude_values.is_empty() {
                                    for i in 0..128 {
                                        let field_name = format!("magnitude[{i}]");
                                        if let Ok(value) = structure.get::<f32>(&field_name)
                                        {
                                            magnitude_values.push(value);
                                        } else {
                                            break;
                                        }
                                    }
                                }

                                // Process spectrum data if we have it
                                if !magnitude_values.is_empty() {
                                    let bands = magnitude_values.len();

                                    // Update spectrum data
                                    if let Ok(mut data) = self.spectrum_data.lock() {
                                        *data = SpectrumData {
                                            bands,
                                            magnitudes: magnitude_values,
                                            phases: None,
                                            timestamp: structure.get("timestamp").ok(),
                                        };
                                    }
                                    updated = true;
                                }
                            }

                            // Check for level messages (RMS/peak/decay)
                            if structure.name() == "level" {
                                // Extract RMS and peak values
                                let mut rms_db_val = None;
                                let mut peak_val = None;

                                if let Ok(rms_list) =
                                    structure.get::<gst::glib::ValueArray>("rms")
                                {
                                    for val in rms_list.iter() {
                                        if let Ok(rms_db) = val.get::<f64>() {
                                            rms_db_val = Some(rms_db);
                                            break; // Use first channel
                                        }
                                    }
                                }

                                if let Ok(peak_list) =
                                    structure.get::<gst::glib::ValueArray>("peak")
                                {
                                    for val in peak_list.iter() {
                                        if let Ok(peak_db) = val.get::<f64>() {
                                            // Convert dB to linear (0.0 to 1.0)
                                            peak_val = Some(10.0_f64.powf(peak_db / 20.0));
                                            break; // Use first channel
                                        }
                                    }
                                }

                                if let (Some(rms_db), Some(peak)) = (rms_db_val, peak_val) {
                                    // Convert RMS from dB to linear
                                    let rms_linear = 10.0_f64.powf(rms_db / 20.0);

                                    // Update audio_level data with level element values
                                    if let Ok(mut level) = self.audio_level.lock() {
                                        *level = AudioLevel {
                                            rms: rms_linear,
                                            rms_db,
                                            peak,
                                        };
                                    }
                                    updated = true;
                                }
                            }

                            // Check for BPM messages from bpmdetect (structure name is "tempo")
                            if structure.name() == "tempo" {
                                if let Ok(bpm_val) = structure.get::<f64>("tempo") {
                                    let bpm_val = bpm_val as f32;
                                    if bpm_val > 0.0 {
                                        if let Ok(mut bpm_lock) = self.bpm_value.lock() {
                                            *bpm_lock = bpm_val;
                                            info!("BPM detected: {:.1}", bpm_val);
                                        }
                                        updated = true;
                                    }
                                }
                            }
                        }
                    }
                    // Also check for tag messages - bpmdetect posts BPM as tags!
                    gst::MessageView::Tag(tag) => {
                        let tags = tag.tags();

                        // Check for BPM tag from bpmdetect
                        if let Some(bpm) = tags.get::<gst::tags::BeatsPerMinute>() {
                            let bpm_val = bpm.get() as f32;

                            if bpm_val > 0.0 {
                                if let Ok(mut bpm_lock) = self.bpm_value.lock() {
                                    *bpm_lock = bpm_val;
                                    info!("BPM tag detected: {:.1}", bpm_val);
                                }
                                updated = true;
                            }
                        }
                    }
                    _ => (),
                }
            }
        }

        updated
    }

    /// Returns true if this is an audio-only file (no video track)
    pub fn is_audio_only(&self) -> bool {
        *self.audio_only.lock().unwrap()
    }

    /// Returns true if the media has a video track
    pub fn has_video(&self) -> bool {
        *self.has_video.lock().unwrap()
    }

    /// Start playing the video
    pub fn play(&mut self) -> Result<()> {
        info!("Playing video");
        match self.pipeline.set_state(gst::State::Playing) {
            Ok(_) => {
                *self.is_playing.lock().unwrap() = true;
                Ok(())
            }
            Err(e) => Err(anyhow!("Failed to start playback: {:?}", e)),
        }
    }

    /// Pause the video
    pub fn pause(&mut self) -> Result<()> {
        info!("Pausing video");
        match self.pipeline.set_state(gst::State::Paused) {
            Ok(_) => {
                *self.is_playing.lock().unwrap() = false;
                Ok(())
            }
            Err(e) => Err(anyhow!("Failed to pause playback: {:?}", e)),
        }
    }

    /// Seek to a specific position in the video
    pub fn seek(&mut self, position: gst::ClockTime) -> Result<()> {
        debug!("Seeking to position: {position:?}");

        // are we sure the pipeline is in a state that can handle seeking??
        let current_state = self.pipeline.current_state();
        if current_state == gst::State::Null || current_state == gst::State::Ready {
            warn!("Cannot seek in current state: {current_state:?}");
            return Ok(());
        }

        // exec the seek operation
        let seek_flags = gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT;
        if self.pipeline.seek_simple(seek_flags, position).is_ok() {
            debug!("Seek successful");
            *self.position.lock().unwrap() = position;
            Ok(())
        } else {
            let err = anyhow!("Failed to seek to {:?}", position);
            error!("{err}");
            Err(err)
        }
    }

    pub fn set_loop(&mut self, should_loop: bool) {
        *self.loop_playback.lock().unwrap() = should_loop;
        info!("Video loop set to: {should_loop}");
    }

    /// audio volume (between 0.0 and 1.0)
    pub fn set_volume(&mut self, volume: f64) -> Result<()> {
        if !self.has_audio {
            debug!("Ignoring volume change request - video has no audio");
            return Ok(());
        }

        let clamped_volume = volume.max(0.0).min(1.0);
        *self.volume.lock().unwrap() = clamped_volume;

        if let Some(volume_elem) = self.pipeline.by_name("volume") {
            volume_elem.set_property("volume", clamped_volume);
            debug!("Set volume to {clamped_volume:.2}");
            Ok(())
        } else {
            warn!("Volume element not found in pipeline");
            Ok(()) // Don't fail if element not found - video might still work
        }
    }

    /// Mutes or unmutes the audio
    pub fn set_mute(&mut self, muted: bool) -> Result<()> {
        if !self.has_audio {
            debug!("Ignoring mute request - video has no audio");
            return Ok(());
        }

        *self.is_muted.lock().unwrap() = muted;

        if let Some(volume_elem) = self.pipeline.by_name("volume") {
            volume_elem.set_property("mute", muted);
            debug!("Set mute to {muted}");
            Ok(())
        } else {
            warn!("Volume element not found in pipeline");
            Ok(()) // Don't fail if element not found - video might still work
        }
    }

    pub fn toggle_mute(&mut self) -> Result<()> {
        let current_mute = *self.is_muted.lock().unwrap();
        self.set_mute(!current_mute)
    }

    /// Returns true if the video has an audio track
    pub fn has_audio(&self) -> bool {
        self.has_audio
    }

    /// Gets the current volume (0.0 to 1.0)
    pub fn volume(&self) -> f64 {
        *self.volume.lock().unwrap()
    }

    /// Returns true if audio is currently muted
    pub fn is_muted(&self) -> bool {
        *self.is_muted.lock().unwrap()
    }

    pub fn position(&self) -> gst::ClockTime {
        *self.position.lock().unwrap()
    }

    pub fn duration(&self) -> Option<gst::ClockTime> {
        self.pipeline.query_duration::<gst::ClockTime>().or(self.duration)
    }

    pub fn dimensions(&self) -> (u32, u32) {
        self.dimensions
    }

    pub fn framerate(&self) -> Option<(i32, i32)> {
        self.framerate.map(|f| (f.numer(), f.denom()))
    }

    pub fn is_playing(&self) -> bool {
        *self.is_playing.lock().unwrap()
    }

    pub fn is_looping(&self) -> bool {
        *self.loop_playback.lock().unwrap()
    }

    pub fn path(&self) -> &str {
        &self.video_path
    }

    /// Configure spectrum analysis parameters
    pub fn configure_spectrum(&mut self, bands: usize, threshold: i32) -> Result<()> {
        if !self.has_audio {
            debug!("Ignoring spectrum configuration - video has no audio");
            return Ok(());
        }

        self.spectrum_bands = bands;
        self.spectrum_threshold = threshold;

        if let Some(spectrum_elem) = self.pipeline.by_name("spectrum") {
            spectrum_elem.set_property("bands", bands as u32);
            spectrum_elem.set_property("threshold", threshold);
            info!("Configured spectrum: {bands} bands, {threshold}dB threshold");
            Ok(())
        } else {
            warn!("Spectrum element not found in pipeline");
            Ok(()) // Don't fail if element not found
        }
    }

    /// Enable or disable spectrum analysis
    pub fn enable_spectrum(&mut self, enabled: bool) -> Result<()> {
        if !self.has_audio {
            debug!("Ignoring spectrum enable request - video has no audio");
            return Ok(());
        }

        self.spectrum_enabled = enabled;

        if let Some(spectrum_elem) = self.pipeline.by_name("spectrum") {
            spectrum_elem.set_property("post-messages", enabled);
            info!(
                "Spectrum analysis {} ",
                if enabled { "enabled" } else { "disabled" }
            );
            Ok(())
        } else {
            warn!("Spectrum element not found in pipeline");
            Ok(()) // Don't fail if element not found
        }
    }

    /// Get current spectrum data
    pub fn spectrum_data(&self) -> SpectrumData {
        match self.spectrum_data.lock() {
            Ok(data) => data.clone(),
            Err(_) => SpectrumData::default(),
        }
    }

    /// Get current audio level data (RMS, peak)
    pub fn audio_level(&self) -> AudioLevel {
        match self.audio_level.lock() {
            Ok(data) => data.clone(),
            Err(_) => AudioLevel::default(),
        }
    }

    /// Whether the raw PCM tap is delivering samples. False if the media
    /// has no audio track or the appsink branch failed to attach — spectrum
    /// and level may still be working in either case.
    pub fn has_pcm(&self) -> bool {
        self.has_pcm.load(Ordering::Relaxed)
    }

    /// Source sample rate of the raw PCM samples (44.1kHz). Consumers that
    /// need a different rate should resample on their side.
    pub fn pcm_sample_rate(&self) -> u32 {
        PCM_SAMPLE_RATE
    }

    /// Number of raw PCM samples currently buffered. Useful when you need
    /// to wait for a full chunk before processing.
    pub fn pcm_available(&self) -> usize {
        self.pcm_samples.lock().map(|q| q.len()).unwrap_or(0)
    }

    /// Drain up to `count` raw PCM samples in FIFO order. Returns fewer
    /// than `count` (possibly zero) when the buffer is short. Samples are
    /// mono `f32` in `[-1.0, 1.0]` at `pcm_sample_rate()`.
    pub fn pop_pcm_samples(&mut self, count: usize) -> Vec<f32> {
        let Ok(mut q) = self.pcm_samples.lock() else {
            return Vec::new();
        };
        let n = count.min(q.len());
        q.drain(..n).collect()
    }

    pub fn get_bpm(&self) -> f32 {
        if !self.has_audio {
            return 0.0;
        }

        match self.bpm_value.lock() {
            Ok(bpm) => *bpm,
            Err(_) => 0.0,
        }
    }
    /// Set interval between spectrum updates in milliseconds
    pub fn set_spectrum_interval(&mut self, interval_ms: u64) -> Result<()> {
        if !self.has_audio {
            debug!("Ignoring spectrum interval request - video has no audio");
            return Ok(());
        }

        if let Some(spectrum_elem) = self.pipeline.by_name("spectrum") {
            // Convert milliseconds to nanoseconds for GStreamer
            let interval_ns = interval_ms * 1_000_000;
            spectrum_elem.set_property("interval", interval_ns);
            info!("Set spectrum interval to {interval_ms}ms");
            Ok(())
        } else {
            warn!("Spectrum element not found in pipeline");
            Ok(()) // Don't fail if element not found
        }
    }
}

impl Drop for VideoTextureManager {
    fn drop(&mut self) {
        info!("Shutting down video pipeline");
        let _ = self.pipeline.set_state(gst::State::Null);
    }
}
