use forge_ipc::{AppIdMatcher, GestureSector, KeyTrigger, Keysyms, Modifiers};
use serde::{Deserialize, Serialize};
use tracing::{info, warn};

#[derive(Debug)]
pub struct GestureBinding {
    pub app_id: AppIdMatcher,
    pub start: GestureSector,
    pub end: GestureSector,
    pub action: GestureAction,
}

#[derive(Clone, Debug)]
pub enum GestureAction {
    Exec { program: String, args: Vec<String> },
    Key { keysym: u32, mods: Modifiers },
}

#[derive(Debug)]
pub struct KeyBinding {
    pub app_id: AppIdMatcher,
    pub mods: Modifiers,
    pub keys: Keysyms,
    pub trigger: KeyTrigger,
    pub program: String,
    pub arguments: Vec<String>,
}

// ============================================================================
// Forge compositor config file (~/.config/forge/forge.toml)
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForgeConfig {
    #[serde(default)]
    pub statusbar: StatusBarConfig,
    #[serde(default)]
    pub decoration: DecorationConfig,
    #[serde(default)]
    pub display: DisplayConfig,
    #[serde(default)]
    pub windows: WindowsConfig,
    #[serde(default)]
    pub corners: CornersConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CornersConfig {
    /// Logical pixels the pointer must keep pushing into a corner, once
    /// pinned there, before the corner action fires.
    ///
    /// ```toml
    /// [corners]
    /// push_distance = 40.0
    /// pin_radius = 4.0
    /// ```
    #[serde(default = "default_push_distance")]
    pub push_distance: f64,
    /// Distance from the vertex, in logical pixels, within which the
    /// pointer counts as pinned to the corner.
    #[serde(default = "default_pin_radius")]
    pub pin_radius: f64,
}

fn default_push_distance() -> f64 {
    crate::corner_action::DEFAULT_PUSH_DISTANCE
}

fn default_pin_radius() -> f64 {
    crate::corner_action::DEFAULT_PIN_RADIUS
}

impl Default for CornersConfig {
    fn default() -> Self {
        Self { push_distance: default_push_distance(), pin_radius: default_pin_radius() }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowsConfig {
    /// Depth of the edge bands that arm a snap zone during a
    /// modifier-click drag (Ctrl+Alt / Super + left button), as a fraction
    /// of the work area: side bands are this fraction of the width, top
    /// and bottom bands this fraction of the height. Clamped to 0.01–0.5.
    ///
    /// ```toml
    /// [windows]
    /// snap_edge_fraction = 0.25
    /// ```
    #[serde(default = "default_snap_edge_fraction")]
    pub snap_edge_fraction: f64,
}

fn default_snap_edge_fraction() -> f64 {
    0.25
}

impl Default for WindowsConfig {
    fn default() -> Self {
        Self { snap_edge_fraction: default_snap_edge_fraction() }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DisplayConfig {
    /// Output scale pinned by the user from Settings. `None` leaves the
    /// decision to the panel DPI and, while mirroring, to the coarsest
    /// connected screen. `FORGE_SCALE` still overrides both.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scale: Option<f64>,

    /// How multiple connected screens are used: `mirror` shows the same
    /// image everywhere, `extend` gives each screen its own desktop region.
    #[serde(default)]
    pub mode: DisplayMode,

    /// Per-connector display settings, keyed by connector name ("eDP-1",
    /// "DP-2"). Absent connectors fall back to automatic placement and the
    /// panel-DPI scale.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub outputs: Vec<OutputConfig>,
}

impl DisplayConfig {
    fn output_index(&self, connector: &str, identity: Option<&str>) -> Option<usize> {
        identity
            .and_then(|id| {
                self.outputs
                    .iter()
                    .position(|o| o.identity.as_deref() == Some(id))
            })
            .or_else(|| self.outputs.iter().position(|o| o.connector == connector))
    }

    /// The entry for a screen: matched by EDID identity when both sides carry
    /// one (the connector index can move between boots), by connector name
    /// otherwise.
    pub fn output_for(&self, connector: &str, identity: Option<&str>) -> Option<&OutputConfig> {
        self.output_index(connector, identity).map(|i| &self.outputs[i])
    }

    pub fn output_for_mut(
        &mut self,
        connector: &str,
        identity: Option<&str>,
    ) -> Option<&mut OutputConfig> {
        self.output_index(connector, identity).map(move |i| &mut self.outputs[i])
    }

    /// Stamp `identity` on the entry that only knew the connector so far, and
    /// refresh the connector name of an identity-matched entry that has moved
    /// ports. Returns whether anything changed.
    pub fn learn_identity(&mut self, connector: &str, identity: Option<&str>) -> bool {
        let Some(id) = identity else { return false };
        let Some(entry) = self.output_for_mut(connector, Some(id)) else { return false };
        let mut changed = false;
        if entry.identity.as_deref() != Some(id) {
            entry.identity = Some(id.to_string());
            changed = true;
        }
        if entry.connector != connector {
            entry.connector = connector.to_string();
            changed = true;
        }
        changed
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DisplayMode {
    #[default]
    Mirror,
    Extend,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    pub connector: String,
    /// EDID identity ("make model serial") of the screen last seen on this
    /// connector; when present it takes precedence over `connector`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity: Option<String>,
    /// Interface scale for this screen; `None` = automatic (panel DPI).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scale: Option<f64>,
    /// Mode as "1920x1080@60"; `None` = the connector's preferred mode.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mode: Option<String>,
    /// Top-left corner in global logical coordinates; `None` = automatic
    /// placement (externals to the right of the internal panel).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub position: Option<(i32, i32)>,
    #[serde(default = "default_enabled")]
    pub enabled: bool,
    #[serde(default)]
    pub primary: bool,
}

fn default_enabled() -> bool {
    true
}

/// Decoration policy. The xdg-decoration protocol is the primary mechanism
/// for clients to opt in/out of server-side decorations; this section is the
/// safety valve for clients that paint their own title bar without binding
/// the protocol (and therefore would otherwise end up with two stacked
/// title bars: the client's own + forge's SSD).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecorationConfig {
    /// Regex patterns matched against toplevel `app_id`. A match suppresses
    /// forge's SSD title bar for that window, even when the client has not
    /// requested ClientSide via xdg-decoration. Examples for `forge.toml`:
    ///
    /// ```toml
    /// [decoration]
    /// csd_apps = [
    ///     "^org\\.mozilla\\.firefox$",
    ///     "^Electron$",
    ///     "^code-oss$",
    /// ]
    /// ```
    ///
    /// The default list covers families that always draw their own header
    /// bar (GNOME / libadwaita) so they do not end up with two stacked
    /// title bars out of the box. A user-provided `forge.toml` replaces
    /// this list entirely — set it to `[]` to opt back into SSD on
    /// everything, or extend it for additional CSD apps.
    #[serde(default = "default_csd_apps")]
    pub csd_apps: Vec<String>,
}

fn default_csd_apps() -> Vec<String> {
    vec![
        // GNOME / libadwaita apps draw their own header bar; without
        // matching, forge's SSD strip sits on top of the client's own
        // close/maximize/minimize buttons.
        r"^org\.gnome\..*".to_string(),
        // Firefox paints its own tab strip / window controls. App_id
        // varies by packaging: RPM/DEB use `firefox` (sometimes `Firefox`
        // with capital F), Flatpak uses `org.mozilla.firefox`, ESR uses
        // `firefox-esr`, Developer Edition `firefox-developer-edition`,
        // Nightly `firefox-nightly`. Match all variants case-insensitively
        // so the SSD strip is suppressed regardless of distribution.
        r"(?i)^firefox(-esr|-developer-edition|-nightly|-aurora)?$".to_string(),
        r"(?i)^org\.mozilla\.(firefox|firefox-?nightly|firefox-?esr)$".to_string(),
        // Chromium family: Chrome, Chromium and Brave all paint their
        // own frame and window controls. The default `app_id` strings
        // differ across distributions (`Chromium`, `chromium-browser`,
        // `google-chrome`, `Brave-browser`, `brave-browser`), so match
        // case-insensitively on the common stem.
        r"(?i)^(chromium|chromium-browser|google-chrome|brave|brave-browser)$".to_string(),
        // Electron-family apps (VS Code, Discord, …) follow the same
        // pattern as Chromium. VS Code in particular announces
        // `code` or `code-oss`.
        r"(?i)^(electron|code|code-oss|code-insiders)$".to_string(),
    ]
}

impl Default for DecorationConfig {
    fn default() -> Self {
        Self {
            csd_apps: default_csd_apps(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusBarConfig {
    /// Enable the built-in status bar. Disabled by default — the external
    /// top bar (crustace shell / liberux-topbar) is the primary status bar.
    /// Set to true only when running forge without crustace.
    #[serde(default)]
    pub enabled: bool,
    /// Status bar height in logical pixels.
    #[serde(default = "default_statusbar_height")]
    pub height: i32,
}

fn default_statusbar_height() -> i32 {
    28
}

impl Default for StatusBarConfig {
    fn default() -> Self {
        Self { enabled: false, height: 28 }
    }
}

impl Default for ForgeConfig {
    fn default() -> Self {
        Self {
            statusbar:  StatusBarConfig::default(),
            decoration: DecorationConfig::default(),
            display:    DisplayConfig::default(),
            windows:    WindowsConfig::default(),
            corners:    CornersConfig::default(),
        }
    }
}

impl ForgeConfig {
    pub fn load() -> Self {
        let config_dir = dirs::config_dir().unwrap_or_default().join("forge");
        let path = config_dir.join("forge.toml");

        if path.exists() {
            match std::fs::read_to_string(&path) {
                Ok(content) => match toml::from_str::<ForgeConfig>(&content) {
                    Ok(config) => {
                        info!("Loaded forge config from {}", path.display());
                        return config;
                    }
                    Err(e) => {
                        warn!("Failed to parse {}: {e}, using defaults", path.display());
                    }
                },
                Err(e) => {
                    warn!("Failed to read {}: {e}, using defaults", path.display());
                }
            }
        }

        Self::default()
    }

    /// Rewrite `forge.toml` from the in-memory config. Round-trips through
    /// serde, so comments in a hand-written file are lost.
    pub fn save(&self) -> std::io::Result<()> {
        let config_dir = dirs::config_dir().unwrap_or_default().join("forge");
        std::fs::create_dir_all(&config_dir)?;
        let path = config_dir.join("forge.toml");
        let text = toml::to_string_pretty(self)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        std::fs::write(&path, text)?;
        info!("Wrote forge config to {}", path.display());
        Ok(())
    }
}
