//! Domain types for screen-level observation and input synthesis.
//!
//! Cross-platform. Platform-specific backends convert their native
//! representations (CGWindowID, HWND, XID) into these types before
//! handing anything back to `car-desktop` callers.

use serde::{Deserialize, Serialize};

// Shared perception types live in car-browser today. This re-export
// gives car-desktop callers a stable import path, and is the pivot
// point if/when these types hoist to a shared crate (tracked in
// docs/CAR_DESKTOP.md, Sprint 4).
pub use car_browser::models::{A11yNode, Bounds, Modifier};

/// Opaque handle identifying a specific window on the current
/// display. Callers acquire these via `list_windows` and pass them to
/// `observe_window`, `focus_window`, `click`, etc. The wrapped fields
/// are implementation-defined; do not construct by hand.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WindowHandle {
    /// Owning process ID. Used by macOS AXUIElementCreateApplication
    /// and Windows GetWindowThreadProcessId.
    pub pid: u32,
    /// Platform-specific window number. On macOS this is
    /// CGWindowID from CGWindowListCopyWindowInfo; on Windows it's
    /// HWND cast to u64; on Linux (future) it's XID / surface id.
    pub window_id: u64,
}

impl WindowHandle {
    /// Construct a handle from platform-reported IDs. Platform
    /// backends use this in their enumeration code; application
    /// callers should acquire handles from `list_windows` instead.
    pub fn new(pid: u32, window_id: u64) -> Self {
        Self { pid, window_id }
    }
}

/// Uniquely identifies a display (monitor). On macOS this is a
/// CGDirectDisplayID; on Windows an HMONITOR cast; on Linux a Wayland
/// output name or Xrandr output id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DisplayId(pub u64);

impl DisplayId {
    /// Sentinel for the primary display. Backends resolve this to
    /// their concrete primary-display id at call time.
    pub const PRIMARY: DisplayId = DisplayId(0);
}

/// Rectangle describing a window's position + size in global screen
/// coordinates (origin = top-left of the primary display on macOS,
/// bottom-left with y-up on Linux X11 — the backend normalizes to
/// top-left-origin before returning).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct WindowFrame {
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
}

impl WindowFrame {
    pub fn contains_point(&self, px: f64, py: f64) -> bool {
        px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
    }

    pub fn center(&self) -> (f64, f64) {
        (self.x + self.width * 0.5, self.y + self.height * 0.5)
    }
}

/// Metadata describing a visible window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowInfo {
    pub handle: WindowHandle,
    /// User-visible title (may be empty for chromeless windows).
    pub title: String,
    /// Reverse-DNS bundle identifier on macOS (e.g. `ai.parslee.tokhn`);
    /// executable name on Windows and Linux.
    pub bundle_id: Option<String>,
    /// Human-readable owning-app name (e.g. `Tokhn`, `Calculator`).
    pub owner_name: String,
    pub frame: WindowFrame,
    /// macOS window layer / z-order hint; higher = closer to user.
    pub layer: i32,
    pub on_screen: bool,
}

/// Filter for window enumeration. All fields are optional AND
/// combined — a filter matches a window only if every set field
/// matches.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowFilter {
    #[serde(default)]
    pub pid: Option<u32>,
    #[serde(default)]
    pub bundle_id: Option<String>,
    #[serde(default)]
    pub title_contains: Option<String>,
    /// When `true` (default) skip windows the user can't see
    /// (minimized or off-screen). Missions usually want the default.
    #[serde(default = "default_visible_only")]
    pub visible_only: bool,
}

fn default_visible_only() -> bool {
    true
}

impl Default for WindowFilter {
    fn default() -> Self {
        Self {
            pid: None,
            bundle_id: None,
            title_contains: None,
            visible_only: true,
        }
    }
}

impl WindowFilter {
    pub fn by_pid(pid: u32) -> Self {
        Self {
            pid: Some(pid),
            visible_only: true,
            ..Self::default()
        }
    }

    pub fn by_bundle_id(bundle: impl Into<String>) -> Self {
        Self {
            bundle_id: Some(bundle.into()),
            visible_only: true,
            ..Self::default()
        }
    }

    pub fn by_title_contains(needle: impl Into<String>) -> Self {
        Self {
            title_contains: Some(needle.into()),
            visible_only: true,
            ..Self::default()
        }
    }

    /// Check whether a `WindowInfo` satisfies this filter.
    pub fn matches(&self, info: &WindowInfo) -> bool {
        if self.visible_only && !info.on_screen {
            return false;
        }
        if let Some(p) = self.pid {
            if info.handle.pid != p {
                return false;
            }
        }
        if let Some(b) = &self.bundle_id {
            match &info.bundle_id {
                Some(bid) if bid == b => {}
                _ => return false,
            }
        }
        if let Some(needle) = &self.title_contains {
            if !info.title.contains(needle.as_str()) {
                return false;
            }
        }
        true
    }
}

/// A pixel buffer captured from the screen or a window. Encoded as
/// tight RGBA8 rows, top-left origin.
#[derive(Debug, Clone)]
pub struct Frame {
    pub width: u32,
    pub height: u32,
    /// Device-pixel-ratio for high-DPI displays (Retina = 2.0).
    pub scale_factor: f32,
    /// RGBA8 pixel data, row-major, top-left origin, no padding.
    /// Length must equal `width * height * 4`.
    pub rgba: Vec<u8>,
    pub captured_at: chrono::DateTime<chrono::Utc>,
}

impl Frame {
    /// Sanity check that the byte buffer matches the advertised
    /// dimensions. Called by every capture backend before returning.
    pub fn validate(&self) -> Result<(), String> {
        let expected = (self.width as usize)
            .saturating_mul(self.height as usize)
            .saturating_mul(4);
        if self.rgba.len() != expected {
            return Err(format!(
                "Frame byte length {} does not match {}x{}x4 = {}",
                self.rgba.len(),
                self.width,
                self.height,
                expected
            ));
        }
        Ok(())
    }
}

/// Compact per-element record stored in `UiMap.a11y_by_id`.
///
/// The AX walk emits an id-indexed lookup alongside the tree so
/// `click(element_id=...)` resolves to an absolute point in O(1)
/// without re-walking the tree. Title is carried separately
/// because destructive-label safety gating reads it directly.
#[derive(Debug, Clone)]
pub struct A11yElementRecord {
    pub bounds: Bounds,
    pub role: String,
    pub name: Option<String>,
    pub value: Option<String>,
    pub focusable: bool,
    pub focused: bool,
    pub disabled: bool,
}

/// Unified perception payload — the structured output of
/// `observe_window`. Combines a `Frame` with an `A11yNode` tree plus
/// an id-indexed lookup so callers can resolve element_ids to
/// bounds + metadata without walking the tree.
#[derive(Debug, Clone)]
pub struct UiMap {
    pub window: WindowInfo,
    pub frame: Option<Frame>,
    pub a11y_root: Option<A11yNode>,
    /// All node ids encountered during the AX walk, in breadth-first
    /// order. `a11y_root.children` contains only the direct children;
    /// this list lets the planner resolve deeper ids without a
    /// recursive re-walk of the tree.
    pub a11y_index: Vec<String>,
    /// O(1) id → element record lookup. Populated alongside
    /// `a11y_index`. Empty when `a11y_empty = true`.
    pub a11y_by_id: std::collections::HashMap<String, A11yElementRecord>,
    /// `true` if the AX walk hit the depth or node-count cap and
    /// some subtree was dropped. Present so the planner can decide
    /// whether to request a focused sub-observation.
    pub a11y_truncated: bool,
    /// `true` when the OS returned no AX nodes for this window.
    /// Bevy-rendered surfaces (including Tokhn's own GUI) land in
    /// this branch; the planner should fall back to pixel diffs.
    pub a11y_empty: bool,
    pub observed_at: chrono::DateTime<chrono::Utc>,
}

/// Parameters for `click()`. Either `element_id` (refers to a node
/// in the most recent UiMap) or `point` (absolute screen coords
/// inside the target window) must be set; if both are set, the
/// element id wins.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickRequest {
    pub window: WindowHandle,
    pub element_id: Option<String>,
    /// Absolute screen-space point in pixels. Used when element_id
    /// is `None`. Must fall inside `window`'s frame.
    pub point: Option<(f64, f64)>,
    #[serde(default)]
    pub button: MouseButton,
    #[serde(default)]
    pub modifiers: Vec<Modifier>,
    /// Opt-in to clicks on targets matching the destructive-word
    /// regex (delete / quit / etc.). Default is `false`; false
    /// triggers `DestructiveActionGated`.
    #[serde(default)]
    pub unsafe_ok: bool,
    /// Don't post the event — just log + return as if it succeeded.
    /// Used by CI dry-run.
    #[serde(default)]
    pub dry_run: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MouseButton {
    #[default]
    Left,
    Right,
    Middle,
}

/// Parameters for `type_text()`. The text is delivered to the
/// currently-focused element of the target window. Newlines are
/// synthesized as Return key events; other whitespace is preserved.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeRequest {
    pub window: WindowHandle,
    pub text: String,
    #[serde(default)]
    pub dry_run: bool,
}

/// Parameters for `keypress()`. The `key` identifier names a
/// logical key independent of physical keyboard layout — backends
/// translate to platform-native key codes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyPressRequest {
    pub window: WindowHandle,
    pub key: Key,
    #[serde(default)]
    pub modifiers: Vec<Modifier>,
    #[serde(default)]
    pub dry_run: bool,
}

/// Logical key identifier. Expanded as backends need more keys;
/// every variant here must map to a concrete platform-native code.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Key {
    Return,
    Escape,
    Tab,
    Space,
    Backspace,
    Delete,
    ArrowUp,
    ArrowDown,
    ArrowLeft,
    ArrowRight,
    Home,
    End,
    PageUp,
    PageDown,
    F1,
    F2,
    F3,
    F4,
    F5,
    F6,
    F7,
    F8,
    F9,
    F10,
    F11,
    F12,
    // Printable single-character keys are named by their character.
    Char(char),
    Comma,
    Period,
    Slash,
}

/// Snapshot of permission state at a point in time. Does not prompt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionSnapshot {
    pub screen_recording: bool,
    pub accessibility: bool,
    /// Set to `true` after a fresh grant — macOS won't apply
    /// newly-granted permissions until the app is relaunched. The
    /// caller must show the user a "quit and relaunch" prompt.
    pub needs_restart: bool,
}

/// Which permissions to request in `request_permissions`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct PermissionRequest {
    pub screen_recording: bool,
    pub accessibility: bool,
}

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

    #[test]
    fn window_frame_contains_and_center() {
        let f = WindowFrame {
            x: 10.0,
            y: 20.0,
            width: 100.0,
            height: 50.0,
        };
        assert!(f.contains_point(50.0, 40.0));
        assert!(!f.contains_point(5.0, 40.0));
        assert!(!f.contains_point(200.0, 40.0));
        let (cx, cy) = f.center();
        assert!((cx - 60.0).abs() < 1e-6);
        assert!((cy - 45.0).abs() < 1e-6);
    }

    fn sample_info(title: &str, pid: u32, bundle: Option<&str>, on_screen: bool) -> WindowInfo {
        WindowInfo {
            handle: WindowHandle::new(pid, 1),
            title: title.into(),
            bundle_id: bundle.map(|s| s.into()),
            owner_name: "test".into(),
            frame: WindowFrame {
                x: 0.0,
                y: 0.0,
                width: 800.0,
                height: 600.0,
            },
            layer: 0,
            on_screen,
        }
    }

    #[test]
    fn filter_default_is_visible_only() {
        let f = WindowFilter::default();
        assert!(f.visible_only);
        assert!(f.matches(&sample_info("x", 1, None, true)));
        assert!(!f.matches(&sample_info("x", 1, None, false)));
    }

    #[test]
    fn filter_by_pid_and_bundle_conjunctive() {
        let f = WindowFilter::by_bundle_id("ai.parslee.tokhn");
        assert!(f.matches(&sample_info("Tokhn", 42, Some("ai.parslee.tokhn"), true)));
        assert!(!f.matches(&sample_info("Tokhn", 42, Some("com.other"), true)));
        assert!(!f.matches(&sample_info("Tokhn", 42, None, true)));
    }

    #[test]
    fn filter_title_contains_matches_substring() {
        let f = WindowFilter::by_title_contains("Settings");
        assert!(f.matches(&sample_info("Tokhn Settings", 1, None, true)));
        assert!(!f.matches(&sample_info("Tokhn", 1, None, true)));
    }

    #[test]
    fn filter_may_bypass_visible_only() {
        let mut f = WindowFilter::by_pid(1);
        f.visible_only = false;
        assert!(f.matches(&sample_info("x", 1, None, false)));
    }

    #[test]
    fn frame_validate_rejects_size_mismatch() {
        let f = Frame {
            width: 2,
            height: 2,
            scale_factor: 1.0,
            rgba: vec![0u8; 7], // too short
            captured_at: chrono::Utc::now(),
        };
        assert!(f.validate().is_err());
    }

    #[test]
    fn frame_validate_accepts_correct_size() {
        let f = Frame {
            width: 2,
            height: 2,
            scale_factor: 1.0,
            rgba: vec![0u8; 16],
            captured_at: chrono::Utc::now(),
        };
        assert!(f.validate().is_ok());
    }
}
