//! macOS permission backend.
//!
//! TCC (Transparency, Consent, Control) has sharp edges this module has
//! to model honestly rather than paper over. Documented traps, captured
//! as explicit status values or describe() hints:
//!
//! 1. **Automation is per-(caller, target) pair.** Granting "control
//!    Mail.app" once doesn't grant "control Calendar.app" — each target
//!    gets its own TCC row. `Context::target_bundle_id` carries the
//!    target so preflight can tell you what's already authorized vs.
//!    what still prompts. Without a target the answer is meaningless.
//!
//! 2. **Screen Recording needs a process relaunch after grant.** TCC
//!    reports `granted` the instant the user checks the box, but
//!    ScreenCaptureKit keeps returning black frames in the currently-
//!    running process until it restarts. We surface this as
//!    `PermissionStatus::RestartRequired` so apps can show proper UX
//!    instead of silently breaking.
//!
//! 3. **Signature change silently invalidates grants.** If the binary
//!    signature changes (dev → signed release, or a resign), TCC treats
//!    it as a different app — but doesn't reset to `NotDetermined`, so
//!    `request()` never reprompts. Returns
//!    `PermissionStatus::SignatureChanged` so apps can tell users to
//!    clear the old grant via `tccutil reset` before retrying.

use super::{Context, Domain, PermissionError, PermissionStatus};

pub fn status_with(domain: Domain, _ctx: &Context) -> Result<PermissionStatus, PermissionError> {
    Ok(match domain {
        Domain::Browser => browser_status(),
        Domain::Automation => {
            // Without a target bundle ID, Automation status is
            // unanswerable — return NotDetermined with a hint that the
            // caller should supply one via Context::with_target.
            PermissionStatus::NotDetermined
        }
        // Accessibility + Screen Recording have canonical NON-prompting preflight
        // APIs; call them instead of hardcoding NotDetermined (PAR-7265 — they
        // reported not_determined even when granted, because this arm never asked
        // the system).
        Domain::Accessibility => accessibility_status(),
        Domain::ScreenCapture => screen_capture_status(),
        // Still stubs (no non-prompting preflight wired yet): Camera/Microphone
        // (AVCaptureDevice.authorizationStatus), Contacts (CNContactStore),
        // Calendar (EKEventStore), HealthKit. Tracked for follow-up.
        Domain::Contacts
        | Domain::Calendar
        | Domain::Camera
        | Domain::Microphone
        | Domain::HealthRead => PermissionStatus::NotDetermined,
        Domain::Mail => PermissionStatus::NotApplicable,
    })
}

pub fn request_with(domain: Domain, ctx: &Context) -> Result<PermissionStatus, PermissionError> {
    // Requesting on macOS means attempting the action that forces the TCC
    // prompt. We don't want to do that as a side effect of preflight, so
    // for most domains we return the current status and point the user at
    // the right settings pane in `describe`. A future enhancement can add
    // per-domain SDK calls that trigger the system prompts safely.
    status_with(domain, ctx)
}

pub fn describe(domain: Domain, status: PermissionStatus) -> (String, Option<String>) {
    match (domain, status) {
        (_, PermissionStatus::Granted) => ("Granted.".to_string(), None),
        (_, PermissionStatus::NotApplicable) => ("Not applicable on macOS.".to_string(), None),
        (_, PermissionStatus::RestartRequired) => (
            "Access was granted, but the running process must be relaunched \
             before the capability takes effect. macOS ScreenCaptureKit in \
             particular will return black frames until the host process restarts."
                .to_string(),
            Some("Quit and relaunch the app holding this CarRuntime.".to_string()),
        ),
        (_, PermissionStatus::SignatureChanged) => (
            "The binary signature changed since this permission was granted. \
             macOS TCC treats the new signature as a different app but does \
             not reset the grant to NotDetermined, so a plain request() will \
             not reprompt the user."
                .to_string(),
            Some(
                "Clear the stale grant with tccutil (e.g. `tccutil reset \
                 Accessibility com.your.bundle.id`) or toggle the entry in \
                 System Settings → Privacy & Security, then retry."
                    .to_string(),
            ),
        ),
        (Domain::Browser, _) => (
            "Browser automation depends on a Chrome/Chromium binary being \
             installed and launchable."
                .to_string(),
            Some(
                "Install Google Chrome (or any Chromium build) and ensure it \
                 is in /Applications or on PATH."
                    .to_string(),
            ),
        ),
        (Domain::Contacts, _) => (
            "macOS Contacts permission is required.".to_string(),
            Some(
                "System Settings → Privacy & Security → Contacts → enable \
                 access for the calling binary."
                    .to_string(),
            ),
        ),
        (Domain::Calendar, _) => (
            "macOS Calendar permission is required.".to_string(),
            Some(
                "System Settings → Privacy & Security → Calendar → enable \
                 access."
                    .to_string(),
            ),
        ),
        (Domain::Accessibility, _) => (
            "macOS Accessibility permission is required to synthesize input \
             or read AX trees of other processes."
                .to_string(),
            Some(
                "System Settings → Privacy & Security → Accessibility → \
                 enable access."
                    .to_string(),
            ),
        ),
        (Domain::ScreenCapture, _) => (
            "macOS Screen Recording permission is required to capture pixels. \
             Heads up: after granting, the host process must relaunch before \
             ScreenCaptureKit actually returns non-black frames."
                .to_string(),
            Some(
                "System Settings → Privacy & Security → Screen Recording → \
                 enable access, then quit and relaunch the app."
                    .to_string(),
            ),
        ),
        (Domain::Automation, _) => (
            "macOS Automation permission is per (caller, target app) pair. \
             Supply the target bundle ID via Context::with_target when \
             preflighting; otherwise you cannot tell whether your caller is \
             authorized for the specific app it is about to control, and \
             users will loop on prompts across multiple targets."
                .to_string(),
            Some(
                "System Settings → Privacy & Security → Automation → enable \
                 the specific target apps. A fresh target triggers a new \
                 prompt; the user must approve each."
                    .to_string(),
            ),
        ),
        (Domain::Camera, _) => (
            "macOS Camera permission is required.".to_string(),
            Some(
                "System Settings → Privacy & Security → Camera → enable \
                 access."
                    .to_string(),
            ),
        ),
        (Domain::Microphone, _) => (
            "macOS Microphone permission is required.".to_string(),
            Some(
                "System Settings → Privacy & Security → Microphone → enable \
                 access."
                    .to_string(),
            ),
        ),
        (Domain::HealthRead, _) => (
            "macOS HealthKit read access requires the app to request per-\
             data-type authorization (sleep, workouts, HRV, etc.) and ship \
             the com.apple.developer.healthkit entitlement in signed builds."
                .to_string(),
            Some(
                "From your app, call the HealthKit authorization API for \
                 each data type you need; the user chooses which to grant. \
                 Watch for partial grants — TCC doesn't block individual \
                 types, but reads return empty for the declined ones."
                    .to_string(),
            ),
        ),
        (Domain::Mail, _) => unreachable!("Mail returns NotApplicable"),
    }
}

// Canonical NON-prompting TCC preflight checks (PAR-7265). Both return true iff
// the current process is granted, and never display a prompt — safe to call from
// a status query. They can't distinguish "denied" from "not yet asked" without
// prompting, so a false maps to NotDetermined.
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
    // `Boolean AXIsProcessTrusted(void)` — Accessibility (AX) trust for this process.
    fn AXIsProcessTrusted() -> bool;
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
    // `bool CGPreflightScreenCaptureAccess(void)` — Screen Recording (macOS 10.15+).
    fn CGPreflightScreenCaptureAccess() -> bool;
}

fn accessibility_status() -> PermissionStatus {
    // SAFETY: AXIsProcessTrusted takes no args, returns a Boolean, no prompt.
    if unsafe { AXIsProcessTrusted() } {
        PermissionStatus::Granted
    } else {
        // Note: if the grant was made to a different-signature build of the same
        // bundle id (dev ad-hoc vs Developer ID), TCC won't honor it for THIS
        // binary and this correctly reports NotDetermined even though System
        // Settings shows the row enabled — the known signature-collision case;
        // remediation is to remove + re-add the Privacy row.
        PermissionStatus::NotDetermined
    }
}

fn screen_capture_status() -> PermissionStatus {
    // SAFETY: CGPreflightScreenCaptureAccess takes no args, returns a bool, no prompt.
    if unsafe { CGPreflightScreenCaptureAccess() } {
        PermissionStatus::Granted
    } else {
        PermissionStatus::NotDetermined
    }
}

fn browser_status() -> PermissionStatus {
    // Scan PATH + common install locations directly — don't shell to
    // `command -v` (not always present as an executable).
    let candidates = [
        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
        "/Applications/Chromium.app/Contents/MacOS/Chromium",
        "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
        "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
    ];
    for c in candidates {
        if std::path::Path::new(c).exists() {
            return PermissionStatus::Granted;
        }
    }
    if let Ok(path) = std::env::var("PATH") {
        for dir in path.split(':') {
            for name in ["google-chrome", "chromium", "chromium-browser"] {
                let p = std::path::Path::new(dir).join(name);
                if p.exists() {
                    return PermissionStatus::Granted;
                }
            }
        }
    }
    PermissionStatus::Denied
}
