use std::collections::HashMap;

use crate::{
    consts,
    core::event_bus::EventBus,
    data::{hazard_def::HazardType, sound_bank::SoundBank},
};
use godot::{
    classes::{AudioServer, AudioStream, AudioStreamPlayer},
    prelude::*,
};

const MIN_DB: f32 = -80.0;
const MAX_DB: f32 = 5.0;
const LOW_DB: f32 = -30.0;

#[derive(GodotClass)]
#[class(base=Node)]
pub struct SoundManager {
    #[export]
    bank: Option<Gd<SoundBank>>,

    #[export]
    pool_size: i32,

    sfx_pool: Vec<Gd<AudioStreamPlayer>>,
    strain_player: Option<Gd<AudioStreamPlayer>>,
    heartbeat_player: Option<Gd<AudioStreamPlayer>>,
    music_player: Option<Gd<AudioStreamPlayer>>,

    is_lifting: bool,
    current_lift_progress: f32,
    active_hazard_sounds: HashMap<HazardType, usize>,

    master_bus_idx: i32,
    music_bus_idx: i32,
    sfx_bus_idx: i32,

    event_bus: Option<Gd<EventBus>>,
    base: Base<Node>,
}

#[godot_api]
impl INode for SoundManager {
    fn init(base: Base<Node>) -> Self {
        let audio_server = AudioServer::singleton();

        Self {
            bank: None,
            pool_size: 8,
            sfx_pool: Vec::new(),
            strain_player: None,
            heartbeat_player: None,
            music_player: None,
            current_lift_progress: 0.0,
            is_lifting: false,
            master_bus_idx: audio_server.get_bus_index("Master"),
            music_bus_idx: audio_server.get_bus_index("Music"),
            sfx_bus_idx: audio_server.get_bus_index("Sfx"),
            active_hazard_sounds: HashMap::new(),
            event_bus: None,
            base,
        }
    }

    fn ready(&mut self) {
        self.initialize_audio_players();

        let mut bus = self.base().get_node_as::<EventBus>(consts::EVENT_BUS_PATH);

        let mut connect = |signal: &str, method: &str| {
            bus.connect(signal, &self.base().callable(method));
        };

        connect(consts::LIFT_EFFORT_APPLIED, "handle_lift_effort");
        connect(consts::FOCUS_RELEASED, "handle_focus_release");
        connect(consts::FOCUS_CHANGED, "on_focus_changed");
        connect(consts::LIFT_COMPLETED, "handle_lift_complete");
        connect(consts::LIFT_PROGRESS, "on_lift_progress");
        connect(consts::CAMERA_TRAUMA, "on_camera_trauma");
        connect(consts::HAZARD_SPAWNED, "on_hazard_spawned");
        connect(consts::HAZARD_RESOLVED, "on_hazard_resolved");

        self.event_bus = Some(bus);

        if let Some(music) = self.get_stream(|b| b.game_music.clone()) {
            self.play_music(music);
        }
    }
}

#[godot_api]
impl SoundManager {
    fn initialize_audio_players(&mut self) {
        for _ in 0..self.pool_size {
            let p = self.create_player("Sfx");
            self.sfx_pool.push(p);
        }

        self.strain_player = Some(self.create_player("Sfx"));
        self.heartbeat_player = Some(self.create_player("Sfx"));
        self.music_player = Some(self.create_player("Music"));

        if let Some(clip) = self.get_stream(|b| b.heartbeat_loop.clone()) {
            if let Some(hb) = &mut self.heartbeat_player {
                hb.set_stream(&clip);
                hb.set_volume_db(MIN_DB);
                hb.play();
            }
        }
    }

    fn create_player(&mut self, bus: &str) -> Gd<AudioStreamPlayer> {
        let mut p = AudioStreamPlayer::new_alloc();
        p.set_bus(bus);
        self.base_mut().add_child(&p.clone().upcast::<Node>());
        p
    }

    fn get_stream<F>(&self, selector: F) -> Option<Gd<AudioStream>>
    where
        F: FnOnce(&SoundBank) -> Option<Gd<AudioStream>>,
    {
        self.bank.as_ref().and_then(|b| selector(&b.bind()))
    }

    fn play_bank_sfx<F>(&mut self, selector: F, pitch: f32) -> Option<usize>
    where
        F: FnOnce(&SoundBank) -> Option<Gd<AudioStream>>,
    {
        if let Some(clip) = self.get_stream(selector) {
            self.play_sfx(clip, pitch)
        } else {
            None
        }
    }

    fn play_sfx(&mut self, clip: Gd<AudioStream>, pitch: f32) -> Option<usize> {
        let mut chosen_index = None;

        for (i, p) in self.sfx_pool.iter_mut().enumerate() {
            if !p.is_playing() {
                chosen_index = Some(i);
                break;
            }
        }

        if chosen_index.is_none() {
            let mut max_pos = -1.0;
            let mut best_idx = 0;
            for (i, p) in self.sfx_pool.iter_mut().enumerate() {
                let pos = p.get_playback_position();
                if pos > max_pos {
                    max_pos = pos;
                    best_idx = i;
                }
            }
            chosen_index = Some(best_idx);
        }

        if let Some(idx) = chosen_index {
            self.active_hazard_sounds.retain(|_, v| *v != idx);

            if let Some(p) = self.sfx_pool.get_mut(idx) {
                p.set_stream(&clip);
                p.set_pitch_scale(pitch);
                p.play();
            }
        }

        chosen_index
    }

    fn play_music(&mut self, clip: Gd<AudioStream>) {
        if let Some(p) = &mut self.music_player {
            if p.get_stream() != Some(clip.clone()) {
                p.set_stream(&clip);
                p.play();
            }
        }
    }

    fn update_strain_loop(&mut self) {
        let strain_clip = if !self.is_lifting {
            self.get_stream(|b| b.strain_loop.clone())
        } else {
            None
        };

        if let Some(p) = &mut self.strain_player {
            if let Some(clip) = strain_clip {
                p.set_stream(&clip);
                p.play();
            }

            let pitch = 1.0 + (self.current_lift_progress * 0.3);
            p.set_pitch_scale(pitch);
        }
    }

    pub fn toggle_master_mute(&mut self, mute: bool) {
        AudioServer::singleton().set_bus_mute(self.master_bus_idx, mute);
    }

    pub fn toggle_music_mute(&mut self, mute: bool) {
        AudioServer::singleton().set_bus_mute(self.music_bus_idx, mute);
    }

    #[func]
    fn handle_lift_effort(&mut self, _: f32) {
        if !self.is_lifting {
            self.update_strain_loop();
            self.is_lifting = true;
        } else {
            self.update_strain_loop();
        }
    }

    #[func]
    fn handle_focus_release(&mut self) {
        if self.is_lifting {
            self.is_lifting = false;

            if let Some(p) = &mut self.strain_player {
                p.stop();
            }

            self.play_bank_sfx(|b| b.effort_exhale.clone(), 1.0);
        }
    }

    #[func]
    fn handle_lift_complete(&mut self, success: bool) {
        if let Some(p) = &mut self.strain_player {
            p.stop();
        }

        let selector = if success {
            |b: &SoundBank| b.win_stinger.clone()
        } else {
            |b: &SoundBank| b.fail_stinger.clone()
        };

        self.play_bank_sfx(selector, 1.0);
    }

    #[func]
    fn on_camera_trauma(&mut self, amount: f32) {
        if amount > 0.5 {
            self.play_bank_sfx(|b| b.camera_trauma.clone(), 1.0);
        }
    }

    #[func]
    fn on_hazard_resolved(&mut self, hazard_type: HazardType) {
        if let Some(idx) = self.active_hazard_sounds.remove(&hazard_type) {
            if let Some(p) = self.sfx_pool.get_mut(idx) {
                p.stop();
            }
        }

        self.play_bank_sfx(|b| b.hazard_resolve.clone(), 1.0);
    }

    #[func]
    fn on_hazard_spawned(&mut self, hazard_type: HazardType) {
        let player_idx = match hazard_type {
            HazardType::GymBro => self.play_bank_sfx(|b| b.gym_bro.clone(), 1.0),
            HazardType::PhoneDistraction => self.play_bank_sfx(|b| b.phone_ring.clone(), 1.0),
            HazardType::Manager => self.play_bank_sfx(|b| b.manager_get_out.clone(), 1.0),
            _ => self.play_bank_sfx(|b| b.hazard_spawn.clone(), 1.0),
        };

        if let Some(idx) = player_idx {
            self.active_hazard_sounds.insert(hazard_type, idx);
        }
    }

    #[func]
    fn on_lift_progress(&mut self, progress: f32) {
        self.current_lift_progress = progress;
    }

    #[func]
    fn on_focus_changed(&mut self, focus: f32) {
        let Some(p) = &mut self.heartbeat_player else {
            return;
        };

        if focus < 0.1 {
            p.set_volume_db(MIN_DB);
        } else {
            let t = (focus - 0.1) / 0.9;

            let vol = LOW_DB + (t * (MAX_DB - LOW_DB));
            let pitch = 1.0 + (t * 0.4);

            p.set_volume_db(vol);
            p.set_pitch_scale(pitch);
        }
    }
}
