//! Cross-platform permission preflight for Common Agent Runtime.
//!
//! Unifies macOS TCC, Windows privacy/consent APIs, and Linux
//! `xdg-desktop-portal` / polkit / filesystem-permissions semantics under
//! one surface:
//!
//! - [`status`] — current grant state
//! - [`request`] — trigger a native prompt if the OS supports one
//! - [`explain`] — human-readable diagnostic + the user-visible fix
//! - [`domains`] — which [`Domain`]s are meaningful on the current OS
//!
//! # Honest modeling
//!
//! A domain that doesn't exist on the current OS returns
//! [`PermissionStatus::NotApplicable`] — NOT `Denied`. Callers should branch
//! on `NotApplicable` and skip, rather than treating it as failure.
//!
//! # No private APIs
//!
//! Backends use only public, documented interfaces. On macOS that means
//! querying TCC through Apple's public Security/AppKit paths (or returning
//! `NotDetermined` when only private access would give better data).

use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Capability domains. A domain may be `NotApplicable` on some OSes — the
/// enum is the union across all supported platforms.
///
/// This enum is `#[non_exhaustive]` — new variants may be added in minor
/// versions. Downstream consumers MUST include a wildcard arm when
/// matching, typically handled as `Domain::<unknown>` → `NotApplicable`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Domain {
    /// Browser automation (launching Chromium, controlling it). Meaningful
    /// on every OS CAR supports — status reflects whether `car browse` can
    /// find a Chrome binary it's allowed to invoke.
    Browser,
    /// macOS TCC Contacts; Windows Contacts; Linux n/a (usually granted).
    Contacts,
    /// macOS TCC Calendar; Windows Calendar; Linux n/a.
    Calendar,
    /// Mail — no OS-level permission domain; returns `NotApplicable`
    /// everywhere. Kept so the domain list is stable across capabilities.
    Mail,
    /// Accessibility features / input synthesis. macOS TCC Accessibility;
    /// Windows UIPI / SendInput (usually granted on interactive sessions);
    /// Linux X11/Wayland distinction — partially applicable.
    Accessibility,
    /// Screen capture. macOS TCC Screen Recording; Windows/Linux typically
    /// granted.
    ScreenCapture,
    /// Automation / app control. macOS Apple Events; Windows COM; Linux n/a.
    Automation,
    /// Camera.
    Camera,
    /// Microphone.
    Microphone,
    /// Wearable / activity data read access. macOS TCC HealthKit types;
    /// Windows/Linux: per-vendor OAuth (Fitbit / Garmin / Oura / etc.).
    HealthRead,
}

impl Domain {
    pub fn as_str(self) -> &'static str {
        match self {
            Domain::Browser => "browser",
            Domain::Contacts => "contacts",
            Domain::Calendar => "calendar",
            Domain::Mail => "mail",
            Domain::Accessibility => "accessibility",
            Domain::ScreenCapture => "screen_capture",
            Domain::Automation => "automation",
            Domain::Camera => "camera",
            Domain::Microphone => "microphone",
            Domain::HealthRead => "health_read",
        }
    }
}

/// Permission state. `#[non_exhaustive]` — future variants are additive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PermissionStatus {
    /// The user has granted access.
    Granted,
    /// The user has explicitly denied access. Re-requesting usually won't
    /// re-prompt — the user has to toggle it in System Settings / Windows
    /// Settings.
    Denied,
    /// The user has not yet been prompted. `request()` will prompt.
    NotDetermined,
    /// Policy (MDM, parental controls, corp config) is blocking access.
    Restricted,
    /// This domain doesn't exist on the current OS. Skip the check.
    NotApplicable,
    /// Access was granted, but the process was running when the grant
    /// happened and must be relaunched before the capability actually
    /// works. macOS ScreenCaptureKit is the canonical case — status()
    /// reports `granted`, but `capture_screenshot` keeps returning black
    /// frames until the process restarts. Explicit state so downstream
    /// apps can show "restart required" UX instead of silently breaking.
    RestartRequired,
    /// The binary signature changed since access was granted (e.g. dev
    /// → signed release build, or a resign). macOS TCC treats this as a
    /// new, effectively-denied app, but unlike a fresh `Denied` state
    /// there is no `request()` prompt — the user must clear the grant
    /// via `tccutil reset` or toggle in System Settings. Separate state
    /// so apps can tell users to clear before re-requesting.
    SignatureChanged,
}

/// Per-domain diagnostic + user-visible fix hint. `#[non_exhaustive]`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PermissionExplanation {
    pub domain: Domain,
    pub status: PermissionStatus,
    /// Short human-readable description of the current state.
    pub message: String,
    /// Suggested next step for the user, or `None` if none applies
    /// (e.g. already granted, or `NotApplicable`).
    pub fix: Option<String>,
}

#[derive(Debug, Error)]
pub enum PermissionError {
    #[error("permission subsystem unavailable: {0}")]
    Unavailable(String),
    #[error("backend error: {0}")]
    Backend(String),
}

/// Context modifiers for a status/request/explain call. Most domains
/// ignore all fields; `Automation` on macOS requires `target_bundle_id`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Context {
    /// For `Domain::Automation` on macOS: the bundle ID of the target
    /// app you want to control (e.g. `"com.apple.mail"`). TCC Automation
    /// grants are per-(caller, target) pairs — without this, preflight
    /// can't tell you whether your caller is authorized for the
    /// *specific* app it's about to control, and users loop on prompts
    /// because each target gets its own approval row.
    pub target_bundle_id: Option<String>,
}

impl Context {
    pub fn with_target(bundle_id: impl Into<String>) -> Self {
        Self {
            target_bundle_id: Some(bundle_id.into()),
        }
    }
}

/// Current status of a single domain.
pub fn status(domain: Domain) -> Result<PermissionStatus, PermissionError> {
    status_with(domain, &Context::default())
}

/// Status with per-domain context — see [`Context`].
pub fn status_with(domain: Domain, ctx: &Context) -> Result<PermissionStatus, PermissionError> {
    backend::status_with(domain, ctx)
}

/// Trigger a native prompt (where the OS supports one). Returns the new
/// status after the user responds, or the current status if no prompt was
/// shown.
///
/// On macOS, first-time prompts are specific — you can only ask once per
/// domain, then the user must change it in System Settings.
pub fn request(domain: Domain) -> Result<PermissionStatus, PermissionError> {
    request_with(domain, &Context::default())
}

pub fn request_with(domain: Domain, ctx: &Context) -> Result<PermissionStatus, PermissionError> {
    backend::request_with(domain, ctx)
}

/// Human-readable explanation + fix suggestion.
pub fn explain(domain: Domain) -> Result<PermissionExplanation, PermissionError> {
    explain_with(domain, &Context::default())
}

pub fn explain_with(
    domain: Domain,
    ctx: &Context,
) -> Result<PermissionExplanation, PermissionError> {
    let s = status_with(domain, ctx)?;
    let (message, fix) = backend::describe(domain, s);
    Ok(PermissionExplanation {
        domain,
        status: s,
        message,
        fix,
    })
}

/// All domains defined by this crate. Callers can filter to the ones that
/// are meaningful on the current OS by checking `status() != NotApplicable`.
pub fn domains() -> Vec<Domain> {
    vec![
        Domain::Browser,
        Domain::Contacts,
        Domain::Calendar,
        Domain::Mail,
        Domain::Accessibility,
        Domain::ScreenCapture,
        Domain::Automation,
        Domain::Camera,
        Domain::Microphone,
        Domain::HealthRead,
    ]
}

// ---------------------------------------------------------------------------
// Backends
// ---------------------------------------------------------------------------

mod backend;

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

    #[test]
    fn domains_is_stable() {
        assert_eq!(domains().len(), 10);
    }

    #[test]
    fn every_domain_reports_something() {
        for d in domains() {
            // Should not error regardless of OS; NotApplicable is a valid
            // answer, not an error.
            let s = status(d).expect("status should not error");
            let e = explain(d).expect("explain should not error");
            assert_eq!(e.status, s);
            assert_eq!(e.domain, d);
        }
    }

    #[test]
    fn explain_includes_fix_when_not_granted() {
        // We can't assume any specific status, but we can assert that
        // `explain` returns coherent data — if status is denied or
        // restricted, `fix` should be Some.
        for d in domains() {
            let e = explain(d).unwrap();
            match e.status {
                PermissionStatus::Denied
                | PermissionStatus::Restricted
                | PermissionStatus::NotDetermined => {
                    assert!(
                        e.fix.is_some(),
                        "domain {:?} has status {:?} but no fix hint",
                        d,
                        e.status
                    );
                }
                PermissionStatus::Granted | PermissionStatus::NotApplicable => {
                    // Fix may be None.
                }
                PermissionStatus::RestartRequired | PermissionStatus::SignatureChanged => {
                    // Both carry actionable fix text, but asserting on them
                    // is environment-dependent (signature / restart state
                    // depends on the running process), so we don't enforce
                    // here — the describe() match arm handles messaging.
                }
            }
        }
    }
}
