//! Focused, app-local keyboard shortcuts.
//!
//! These helpers are intentionally pure: key serialization, formatting, lookup,
//! and conflict detection live here, while iced event handling stays at the app
//! edge. There are no OS-global shortcuts.

use iced::keyboard;
use serde::{Deserialize, Serialize};

/// A serializable key identity. Modifiers are deliberately out of scope for this
/// first pass; iced delivers the focused app key and we compare that exact key.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KeyBinding {
    Named(String),
    Character(String),
}

impl KeyBinding {
    pub fn from_key(key: &keyboard::Key) -> Option<Self> {
        match key {
            keyboard::Key::Named(named) => Some(Self::Named(format!("{named:?}"))),
            keyboard::Key::Character(ch) => {
                let s = ch.to_string();
                if s.is_empty() {
                    None
                } else {
                    Some(Self::Character(s.to_lowercase()))
                }
            }
            keyboard::Key::Unidentified => None,
        }
    }

    pub fn label(&self) -> String {
        match self {
            KeyBinding::Named(name) => name.clone(),
            KeyBinding::Character(ch) => ch.to_uppercase(),
        }
    }
}

/// Parse a hand-editable binding string from config/docs/tests. Empty and
/// `"unset"` are unbound.
pub fn parse_binding(input: &str) -> Option<KeyBinding> {
    let trimmed = input.trim();
    if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("unset") {
        return None;
    }
    if trimmed.chars().count() == 1 {
        Some(KeyBinding::Character(trimmed.to_lowercase()))
    } else {
        Some(KeyBinding::Named(trimmed.to_string()))
    }
}

pub fn format_binding(binding: Option<&KeyBinding>) -> String {
    binding
        .map(KeyBinding::label)
        .unwrap_or_else(|| "unset".to_string())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HotkeyAction {
    ToggleMute,
    ToggleDeafen,
    OpenSettings,
    PushToTalk,
    LeaveRoom,
}

impl HotkeyAction {
    pub const ALL: [HotkeyAction; 5] = [
        HotkeyAction::ToggleMute,
        HotkeyAction::ToggleDeafen,
        HotkeyAction::OpenSettings,
        HotkeyAction::PushToTalk,
        HotkeyAction::LeaveRoom,
    ];

    pub fn label(self) -> &'static str {
        match self {
            HotkeyAction::ToggleMute => "Toggle mute",
            HotkeyAction::ToggleDeafen => "Toggle deafen",
            HotkeyAction::OpenSettings => "Open Settings",
            HotkeyAction::PushToTalk => "Push-to-talk",
            HotkeyAction::LeaveRoom => "Leave room",
        }
    }

    pub fn tier(self) -> HotkeyTier {
        match self {
            HotkeyAction::ToggleMute | HotkeyAction::ToggleDeafen | HotkeyAction::OpenSettings => {
                HotkeyTier::AppWide
            }
            HotkeyAction::PushToTalk | HotkeyAction::LeaveRoom => HotkeyTier::RoomOnly,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HotkeyTier {
    AppWide,
    RoomOnly,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HotkeyContext {
    pub in_call: bool,
}

impl HotkeyContext {
    fn allows(self, action: HotkeyAction) -> bool {
        matches!(action.tier(), HotkeyTier::AppWide) || self.in_call
    }
}

/// Persisted shortcut map. Defaults preserve the old Space push-to-talk binding
/// and add a few function-key app shortcuts that do not collide with typing.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HotkeyMap {
    #[serde(default = "default_mute")]
    pub toggle_mute: Option<KeyBinding>,
    #[serde(default = "default_deafen")]
    pub toggle_deafen: Option<KeyBinding>,
    #[serde(default = "default_settings")]
    pub open_settings: Option<KeyBinding>,
    #[serde(default = "default_ptt")]
    pub push_to_talk: Option<KeyBinding>,
    #[serde(default)]
    pub leave_room: Option<KeyBinding>,
}

impl Default for HotkeyMap {
    fn default() -> Self {
        Self {
            toggle_mute: default_mute(),
            toggle_deafen: default_deafen(),
            open_settings: default_settings(),
            push_to_talk: default_ptt(),
            leave_room: None,
        }
    }
}

fn named(name: &str) -> Option<KeyBinding> {
    Some(KeyBinding::Named(name.to_string()))
}

fn default_mute() -> Option<KeyBinding> {
    named("F9")
}

fn default_deafen() -> Option<KeyBinding> {
    named("F10")
}

fn default_settings() -> Option<KeyBinding> {
    named("F2")
}

fn default_ptt() -> Option<KeyBinding> {
    named("Space")
}

impl HotkeyMap {
    pub fn binding(&self, action: HotkeyAction) -> Option<&KeyBinding> {
        match action {
            HotkeyAction::ToggleMute => self.toggle_mute.as_ref(),
            HotkeyAction::ToggleDeafen => self.toggle_deafen.as_ref(),
            HotkeyAction::OpenSettings => self.open_settings.as_ref(),
            HotkeyAction::PushToTalk => self.push_to_talk.as_ref(),
            HotkeyAction::LeaveRoom => self.leave_room.as_ref(),
        }
    }

    pub fn set_binding(&mut self, action: HotkeyAction, binding: Option<KeyBinding>) {
        match action {
            HotkeyAction::ToggleMute => self.toggle_mute = binding,
            HotkeyAction::ToggleDeafen => self.toggle_deafen = binding,
            HotkeyAction::OpenSettings => self.open_settings = binding,
            HotkeyAction::PushToTalk => self.push_to_talk = binding,
            HotkeyAction::LeaveRoom => self.leave_room = binding,
        }
    }

    pub fn lookup_key(&self, key: &keyboard::Key, context: HotkeyContext) -> Option<HotkeyAction> {
        let pressed = KeyBinding::from_key(key)?;
        HotkeyAction::ALL
            .into_iter()
            .find(|&action| context.allows(action) && self.binding(action) == Some(&pressed))
    }

    pub fn lookup_binding(
        &self,
        binding: &KeyBinding,
        context: HotkeyContext,
    ) -> Option<HotkeyAction> {
        HotkeyAction::ALL
            .into_iter()
            .find(|&action| context.allows(action) && self.binding(action) == Some(binding))
    }

    pub fn conflicts(&self) -> Vec<HotkeyConflict> {
        let mut conflicts = Vec::new();
        let actions = HotkeyAction::ALL;
        for i in 0..actions.len() {
            for j in (i + 1)..actions.len() {
                let a = actions[i];
                let b = actions[j];
                if let (Some(ab), Some(bb)) = (self.binding(a), self.binding(b))
                    && ab == bb
                {
                    conflicts.push(HotkeyConflict {
                        binding: ab.clone(),
                        first: a,
                        second: b,
                    });
                }
            }
        }
        conflicts
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HotkeyConflict {
    pub binding: KeyBinding,
    pub first: HotkeyAction,
    pub second: HotkeyAction,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn unset_actions_format_as_unset() {
        assert_eq!(format_binding(None), "unset");
        assert_eq!(parse_binding("unset"), None);
        assert_eq!(parse_binding(""), None);
    }

    #[test]
    fn duplicate_binding_is_detected() {
        let mut map = HotkeyMap::default();
        map.set_binding(HotkeyAction::ToggleMute, parse_binding("M"));
        map.set_binding(HotkeyAction::ToggleDeafen, parse_binding("m"));
        let conflicts = map.conflicts();
        assert_eq!(conflicts.len(), 1);
        assert_eq!(conflicts[0].first, HotkeyAction::ToggleMute);
        assert_eq!(conflicts[0].second, HotkeyAction::ToggleDeafen);
    }

    #[test]
    fn lookup_respects_room_tier() {
        let mut map = HotkeyMap::default();
        map.set_binding(HotkeyAction::LeaveRoom, parse_binding("Escape"));
        let binding = parse_binding("Escape").unwrap();
        assert_eq!(
            map.lookup_binding(&binding, HotkeyContext { in_call: false }),
            None,
            "room-only shortcuts should not fire outside a call"
        );
        assert_eq!(
            map.lookup_binding(&binding, HotkeyContext { in_call: true }),
            Some(HotkeyAction::LeaveRoom)
        );
    }

    #[test]
    fn default_ptt_is_space() {
        let map = HotkeyMap::default();
        assert_eq!(
            format_binding(map.binding(HotkeyAction::PushToTalk)),
            "Space"
        );
    }

    #[test]
    fn parse_single_character_case_folds() {
        assert_eq!(
            parse_binding("M"),
            Some(KeyBinding::Character("m".to_string()))
        );
        assert_eq!(format_binding(parse_binding("m").as_ref()), "M");
    }
}
