//! Calendar capability — list calendars, list upcoming events.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[cfg(target_os = "macos")]
use std::process::Command;

use super::{Availability, IntegrationError};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Calendar {
    /// Stable identifier within the current host.
    pub id: String,
    /// Display name as shown in the user's Calendar app.
    pub title: String,
    /// Source label (account / provider) where the OS exposes it.
    pub source: Option<String>,
    /// Calendar color as `#RRGGBB` hex, when available.
    pub color: Option<String>,
    /// Whether the user can create/update events on this calendar.
    pub writable: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub id: String,
    pub calendar_id: String,
    pub title: String,
    pub start: DateTime<Utc>,
    pub end: DateTime<Utc>,
    #[serde(default)]
    pub all_day: bool,
    pub location: Option<String>,
    pub notes: Option<String>,
    /// Participants with their RSVP status — enriched from EventKit
    /// (`EKParticipant`) rather than a bare display name, so a consumer can tell
    /// a firm commitment from a tentative "maybe" (e.g. conflict detection that
    /// must not flag two overlapping events as a hard clash when the user is only
    /// tentative on one). See #68.
    #[serde(default)]
    pub attendees: Vec<Attendee>,
    /// Overall event status — `confirmed` | `tentative` | `canceled` | `none`
    /// (`EKEvent.status`). `None` when the backend doesn't report it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

/// One calendar event participant, from `EKParticipant`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attendee {
    /// Display name, when known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Email, parsed from the participant's `mailto:` URL when present.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
    /// RSVP status — `accepted` | `declined` | `tentative` | `pending` |
    /// `delegated` | `completed` | `in_process` | `unknown`
    /// (`EKParticipant.participantStatus`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Role — `required` | `optional` | `chair` | `non_participant` | `unknown`
    /// (`EKParticipant.participantRole`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    /// Whether this participant is the current user (`EKParticipant.isCurrentUser`).
    #[serde(default)]
    pub is_current_user: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub calendars: Vec<Calendar>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub events: Vec<Event>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventCreateInput {
    pub calendar_id: String,
    pub title: String,
    pub start: DateTime<Utc>,
    pub end: DateTime<Utc>,
    #[serde(default)]
    pub all_day: bool,
    #[serde(default)]
    pub notes: Option<String>,
    #[serde(default)]
    pub location: Option<String>,
    #[serde(default)]
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventUpdateInput {
    pub event_id: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub start: Option<DateTime<Utc>>,
    #[serde(default)]
    pub end: Option<DateTime<Utc>>,
    #[serde(default)]
    pub all_day: Option<bool>,
    #[serde(default)]
    pub notes: Option<String>,
    #[serde(default)]
    pub location: Option<String>,
    #[serde(default)]
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventMutationResult {
    pub ok: bool,
    #[serde(default)]
    pub event: Option<Event>,
    #[serde(default)]
    pub reason: Option<String>,
}

pub fn list_calendars() -> Result<CalendarListing, IntegrationError> {
    backend::list_calendars()
}

/// Current calendar authorization status — a non-prompting query of the real
/// OS permission (`EKEventStore.authorizationStatus` on macOS). Returns one of
/// `granted` | `write_only` | `denied` | `restricted` | `not_determined` |
/// `not_applicable` | `unknown`, so the permissions surface can report an honest
/// gate instead of a stub (car-releases#71). Never triggers a TCC prompt.
pub fn authorization_status() -> String {
    backend::authorization_status()
}

/// List events between `start` and `end`. When no calendar IDs are
/// supplied, backends include all accessible calendars.
pub fn list_events(
    start: DateTime<Utc>,
    end: DateTime<Utc>,
    calendar_ids: &[String],
) -> Result<EventListing, IntegrationError> {
    backend::list_events(start, end, calendar_ids)
}

/// Create a new event on the calendar identified by `input.calendar_id`.
/// The calendar must allow content modifications. Returns the created
/// event with its host-assigned id, or `ok: false` with a reason.
pub fn create_event(input: EventCreateInput) -> Result<EventMutationResult, IntegrationError> {
    backend::create_event(input)
}

/// Update fields on an existing event. Any `None` field on
/// `EventUpdateInput` leaves the event's existing value unchanged
/// (except for `notes`/`location`/`url`, where an empty-string `Some`
/// value clears the field — matches the semantics callers had via the
/// previous shell-out helper).
pub fn update_event(input: EventUpdateInput) -> Result<EventMutationResult, IntegrationError> {
    backend::update_event(input)
}

/// Delete an event by its host-assigned id. Returns `ok: true` with the
/// id echoed when the event was removed, or `ok: false` with a reason
/// when it wasn't (event_not_found, write access denied, etc.).
pub fn delete_event(event_id: &str) -> Result<EventMutationResult, IntegrationError> {
    backend::delete_event(event_id)
}

#[cfg(target_os = "macos")]
mod backend {
    use super::*;

    // The on-disk cache key is `HELPER_VERSION + source_fingerprint(SCRIPT)`
    // (see helper_cache_key), so a SCRIPT change already auto-invalidates the
    // cached helper — the fingerprint, added after car#64's stale-binary bug,
    // is the load-bearing guard. This version string is the human-readable
    // marker of intentional helper revisions; bump it on each SCRIPT edit as
    // belt-and-suspenders. v5: enriched attendees (status/role/email/
    // is_current_user) + event status (#68). Mirrors the contacts helper (#200).
    // v6: non-prompting `auth-status` command (car-releases#71).
    const HELPER_VERSION: &str = "v6";

    const SCRIPT: &str = r##"
import EventKit
import Foundation
import Dispatch

struct Availability: Codable {
    let available: Bool
    let backend: String
    let reason: String?
}

struct CalendarOut: Codable {
    let id: String
    let title: String
    let source: String?
    let color: String?
    let writable: Bool
}

struct AttendeeOut: Codable {
    let name: String?
    let email: String?
    let status: String?
    let role: String?
    let is_current_user: Bool
}

struct EventOut: Codable {
    let id: String
    let calendar_id: String
    let title: String
    let start: Date
    let end: Date
    let all_day: Bool
    let location: String?
    let notes: String?
    let attendees: [AttendeeOut]
    let status: String
}

struct CalendarListing: Codable {
    let available: Bool
    let backend: String
    let reason: String?
    let calendars: [CalendarOut]
}

struct EventListing: Codable {
    let available: Bool
    let backend: String
    let reason: String?
    let events: [EventOut]
}

struct EventCreateInput: Codable {
    let calendar_id: String
    let title: String
    let start: Date
    let end: Date
    let all_day: Bool?
    let notes: String?
    let location: String?
    let url: String?
}

struct EventUpdateInput: Codable {
    let event_id: String
    let title: String?
    let start: Date?
    let end: Date?
    let all_day: Bool?
    let notes: String?
    let location: String?
    let url: String?
}

struct EventMutationOut: Codable {
    let ok: Bool
    let event: EventOut?
    let reason: String?
}

func emit<T: Encodable>(_ value: T) {
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .iso8601
    let data = try! encoder.encode(value)
    FileHandle.standardOutput.write(data)
}

func attendeeStatus(_ s: EKParticipantStatus) -> String {
    switch s {
    case .accepted: return "accepted"
    case .declined: return "declined"
    case .tentative: return "tentative"
    case .pending: return "pending"
    case .delegated: return "delegated"
    case .completed: return "completed"
    case .inProcess: return "in_process"
    default: return "unknown"
    }
}

func attendeeRole(_ r: EKParticipantRole) -> String {
    switch r {
    case .required: return "required"
    case .optional: return "optional"
    case .chair: return "chair"
    case .nonParticipant: return "non_participant"
    default: return "unknown"
    }
}

func attendeeOut(_ p: EKParticipant) -> AttendeeOut {
    // EventKit exposes the address as a `mailto:` URL; surface the bare email.
    var email: String? = nil
    let urlStr = p.url.absoluteString
    if urlStr.lowercased().hasPrefix("mailto:") {
        email = String(urlStr.dropFirst("mailto:".count))
    }
    return AttendeeOut(
        name: p.name,
        email: email,
        status: attendeeStatus(p.participantStatus),
        role: attendeeRole(p.participantRole),
        is_current_user: p.isCurrentUser
    )
}

func eventStatusString(_ s: EKEventStatus) -> String {
    switch s {
    case .confirmed: return "confirmed"
    case .tentative: return "tentative"
    case .canceled: return "canceled"
    default: return "none"
    }
}

func eventOut(_ event: EKEvent) -> EventOut {
    return EventOut(
        id: event.eventIdentifier ?? "\(event.calendarItemIdentifier)-\(event.startDate.timeIntervalSince1970)",
        calendar_id: event.calendar.calendarIdentifier,
        title: event.title ?? "",
        start: event.startDate,
        end: event.endDate,
        all_day: event.isAllDay,
        location: event.location,
        notes: event.notes,
        attendees: (event.attendees ?? []).map { attendeeOut($0) },
        status: eventStatusString(event.status)
    )
}

func unavailable<T: Encodable>(_ reason: String, empty: T) {
    emit(empty)
}

let internetDateFormatter: ISO8601DateFormatter = {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime]
    return formatter
}()

let fractionalDateFormatter: ISO8601DateFormatter = {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    return formatter
}()

func decodeDate(_ decoder: Decoder) throws -> Date {
    let container = try decoder.singleValueContainer()
    let raw = try container.decode(String.self)
    if let date = fractionalDateFormatter.date(from: raw) ?? internetDateFormatter.date(from: raw) {
        return date
    }
    throw DecodingError.dataCorruptedError(
        in: container,
        debugDescription: "Invalid RFC3339 date: \(raw)"
    )
}

func ensureAccess(_ store: EKEventStore) -> String? {
    let status = EKEventStore.authorizationStatus(for: .event)
    switch status {
    case .authorized:
        return nil
    case .notDetermined:
        let semaphore = DispatchSemaphore(value: 0)
        var granted = false
        if #available(macOS 14.0, *) {
            store.requestFullAccessToEvents { ok, _ in
                granted = ok
                semaphore.signal()
            }
        } else {
            store.requestAccess(to: .event) { ok, _ in
                granted = ok
                semaphore.signal()
            }
        }
        _ = semaphore.wait(timeout: .now() + 60)
        return granted ? nil : "Calendar permission was not granted"
    case .restricted:
        return "Calendar permission is restricted by system policy"
    case .denied:
        return "Calendar permission is denied"
    case .writeOnly:
        return "Calendar permission is write-only; read access is required"
    case .fullAccess:
        return nil
    @unknown default:
        return "Calendar permission is unavailable"
    }
}

func hexColor(_ cgColor: CGColor?) -> String? {
    guard let cgColor = cgColor, let components = cgColor.components else { return nil }
    let r: CGFloat
    let g: CGFloat
    let b: CGFloat
    if components.count >= 3 {
        r = components[0]
        g = components[1]
        b = components[2]
    } else if components.count >= 1 {
        r = components[0]
        g = components[0]
        b = components[0]
    } else {
        return nil
    }
    return String(format: "#%02X%02X%02X", Int(max(0, min(1, r)) * 255), Int(max(0, min(1, g)) * 255), Int(max(0, min(1, b)) * 255))
}

let args = CommandLine.arguments
let mode = args.count > 1 ? args[1] : "calendars"

// Non-prompting authorization-status query (car-releases#71). Reports the
// CURRENT EKEventStore.authorizationStatus — never instantiates a store or
// triggers a TCC prompt, unlike ensureAccess below — so `permissionStatus`
// can be an honest gate. Handles the macOS 14 split (.fullAccess/.writeOnly
// replaced .authorized).
if mode == "auth-status" {
    struct AuthStatusOut: Codable { let status: String }
    let label: String
    switch EKEventStore.authorizationStatus(for: .event) {
    case .authorized, .fullAccess: label = "granted"
    case .writeOnly: label = "write_only"
    case .denied: label = "denied"
    case .restricted: label = "restricted"
    case .notDetermined: label = "not_determined"
    @unknown default: label = "unknown"
    }
    emit(AuthStatusOut(status: label))
    exit(0)
}

let store = EKEventStore()
if let reason = ensureAccess(store) {
    if mode == "events" {
        emit(EventListing(available: false, backend: "eventkit", reason: reason, events: []))
    } else if mode == "create" || mode == "update" || mode == "delete" {
        emit(EventMutationOut(ok: false, event: nil, reason: reason))
    } else {
        emit(CalendarListing(available: false, backend: "eventkit", reason: reason, calendars: []))
    }
    exit(0)
}

if mode == "events" {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    let fallback = ISO8601DateFormatter()
    guard args.count >= 4,
          let start = formatter.date(from: args[2]) ?? fallback.date(from: args[2]),
          let end = formatter.date(from: args[3]) ?? fallback.date(from: args[3]) else {
        emit(EventListing(available: false, backend: "eventkit", reason: "Invalid RFC3339 date range", events: []))
        exit(0)
    }
    let requested = Set(args.dropFirst(4))
    let calendars = store.calendars(for: .event).filter { requested.isEmpty || requested.contains($0.calendarIdentifier) }
    let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars)
    let events = store.events(matching: predicate).map { eventOut($0) }
    emit(EventListing(available: true, backend: "eventkit", reason: nil, events: events))
} else if mode == "create" {
    guard args.count >= 3, let payload = args[2].data(using: .utf8) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "create requires JSON payload as args[2]"))
        exit(0)
    }
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom(decodeDate)
    guard let input = try? decoder.decode(EventCreateInput.self, from: payload) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "invalid_create_input_json"))
        exit(0)
    }
    guard let calendar = store.calendar(withIdentifier: input.calendar_id) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "calendar_not_found"))
        exit(0)
    }
    guard calendar.allowsContentModifications else {
        emit(EventMutationOut(ok: false, event: nil, reason: "calendar_is_read_only"))
        exit(0)
    }
    let event = EKEvent(eventStore: store)
    event.calendar = calendar
    event.title = input.title
    event.startDate = input.start
    event.endDate = input.end
    event.isAllDay = input.all_day ?? false
    if let notes = input.notes, !notes.isEmpty { event.notes = notes }
    if let location = input.location, !location.isEmpty { event.location = location }
    if let urlRaw = input.url, !urlRaw.isEmpty, let parsed = URL(string: urlRaw) { event.url = parsed }
    do {
        try store.save(event, span: .thisEvent)
        emit(EventMutationOut(ok: true, event: eventOut(event), reason: nil))
    } catch {
        emit(EventMutationOut(ok: false, event: nil, reason: "save_failed: \(error.localizedDescription)"))
    }
} else if mode == "update" {
    guard args.count >= 3, let payload = args[2].data(using: .utf8) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "update requires JSON payload as args[2]"))
        exit(0)
    }
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom(decodeDate)
    guard let input = try? decoder.decode(EventUpdateInput.self, from: payload) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "invalid_update_input_json"))
        exit(0)
    }
    guard let event = store.event(withIdentifier: input.event_id) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "event_not_found"))
        exit(0)
    }
    guard event.calendar.allowsContentModifications else {
        emit(EventMutationOut(ok: false, event: nil, reason: "calendar_is_read_only"))
        exit(0)
    }
    if let title = input.title { event.title = title }
    if let start = input.start { event.startDate = start }
    if let end = input.end { event.endDate = end }
    if let allDay = input.all_day { event.isAllDay = allDay }
    // Empty-string for notes/location/url means "clear the field". The
    // None case (field absent in JSON) leaves the existing value alone.
    if let notes = input.notes { event.notes = notes.isEmpty ? nil : notes }
    if let location = input.location { event.location = location.isEmpty ? nil : location }
    if let urlRaw = input.url {
        event.url = urlRaw.isEmpty ? nil : URL(string: urlRaw)
    }
    do {
        try store.save(event, span: .thisEvent)
        emit(EventMutationOut(ok: true, event: eventOut(event), reason: nil))
    } catch {
        emit(EventMutationOut(ok: false, event: nil, reason: "save_failed: \(error.localizedDescription)"))
    }
} else if mode == "delete" {
    guard args.count >= 3 else {
        emit(EventMutationOut(ok: false, event: nil, reason: "delete requires event_id as args[2]"))
        exit(0)
    }
    let eventId = args[2]
    guard let event = store.event(withIdentifier: eventId) else {
        emit(EventMutationOut(ok: false, event: nil, reason: "event_not_found"))
        exit(0)
    }
    guard event.calendar.allowsContentModifications else {
        emit(EventMutationOut(ok: false, event: nil, reason: "calendar_is_read_only"))
        exit(0)
    }
    do {
        try store.remove(event, span: .thisEvent)
        emit(EventMutationOut(ok: true, event: nil, reason: nil))
    } catch {
        emit(EventMutationOut(ok: false, event: nil, reason: "delete_failed: \(error.localizedDescription)"))
    }
} else {
    let calendars = store.calendars(for: .event).map { cal in
        CalendarOut(
            id: cal.calendarIdentifier,
            title: cal.title,
            source: cal.source?.title,
            color: hexColor(cal.cgColor),
            writable: cal.allowsContentModifications
        )
    }
    emit(CalendarListing(available: true, backend: "eventkit", reason: nil, calendars: calendars))
}
"##;

    pub fn list_calendars() -> Result<CalendarListing, IntegrationError> {
        run_swift(&["calendars"])
    }

    #[derive(serde::Deserialize)]
    struct AuthStatus {
        status: String,
    }

    pub fn authorization_status() -> String {
        // Non-prompting; on any helper failure report "unknown" rather than
        // fabricating a grant/denial. Log the discarded error so a cold-start
        // compile failure (swiftc missing, signing/entitlement breakage) is
        // observable instead of an invisible "unknown".
        match run_swift::<AuthStatus>(&["auth-status"]) {
            Ok(a) => a.status,
            Err(e) => {
                tracing::debug!(error = %e, "calendar auth-status helper failed; reporting unknown");
                "unknown".to_string()
            }
        }
    }

    pub fn list_events(
        start: DateTime<Utc>,
        end: DateTime<Utc>,
        calendar_ids: &[String],
    ) -> Result<EventListing, IntegrationError> {
        let start = start.to_rfc3339();
        let end = end.to_rfc3339();
        let mut args = vec!["events", start.as_str(), end.as_str()];
        args.extend(calendar_ids.iter().map(String::as_str));
        run_swift(&args)
    }

    pub fn create_event(input: EventCreateInput) -> Result<EventMutationResult, IntegrationError> {
        let payload = serde_json::to_string(&input)
            .map_err(|e| IntegrationError::Backend(format!("encode create input: {e}")))?;
        run_swift(&["create", &payload])
    }

    pub fn update_event(input: EventUpdateInput) -> Result<EventMutationResult, IntegrationError> {
        let payload = serde_json::to_string(&input)
            .map_err(|e| IntegrationError::Backend(format!("encode update input: {e}")))?;
        run_swift(&["update", &payload])
    }

    pub fn delete_event(event_id: &str) -> Result<EventMutationResult, IntegrationError> {
        run_swift(&["delete", event_id])
    }

    fn run_swift<T: serde::de::DeserializeOwned>(args: &[&str]) -> Result<T, IntegrationError> {
        let helper = ensure_helper()?;
        let output = Command::new(helper)
            .env(
                "SWIFT_MODULE_CACHE_PATH",
                std::env::temp_dir().join("car-swift-module-cache"),
            )
            .env(
                "CLANG_MODULE_CACHE_PATH",
                std::env::temp_dir().join("car-clang-module-cache"),
            )
            .args(args)
            .output()
            .map_err(|e| IntegrationError::Backend(format!("swift: {e}")))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            return Err(IntegrationError::Backend(format!(
                "eventkit swift failed: {stderr}"
            )));
        }

        serde_json::from_slice(&output.stdout)
            .map_err(|e| IntegrationError::Backend(format!("eventkit json: {e}")))
    }

    /// Cache key for the compiled helper. Combines the manual `HELPER_VERSION`
    /// with a content hash of the embedded Swift `SCRIPT`, so ANY change to the
    /// source auto-invalidates the cache. A manual bump alone was the car#64
    /// regression: the Swift gained `create`/`update`/`delete` modes but the
    /// cache key stayed `v4`, so upgraders kept reusing a stale pre-mutation
    /// `v4.app` (every mutation fell through to the calendar-listing `else`,
    /// failing Rust deserialization with `missing field \`ok\``). DefaultHasher
    /// is deterministic for the same bytes; a hash change across a toolchain
    /// bump just triggers one harmless recompile.
    fn source_fingerprint(s: &str) -> String {
        use std::hash::{Hash, Hasher};
        let mut h = std::collections::hash_map::DefaultHasher::new();
        s.hash(&mut h);
        format!("{:016x}", h.finish())
    }

    fn helper_cache_key() -> String {
        format!("{HELPER_VERSION}-{}", source_fingerprint(SCRIPT))
    }

    #[cfg(test)]
    mod helper_cache_tests {
        use super::{helper_cache_key, source_fingerprint, HELPER_VERSION};

        #[test]
        fn fingerprint_is_deterministic_and_input_sensitive() {
            // Same source → same key (cache hit); any change → new key (forces
            // a recompile). This is what prevents the car#64 stale-helper class.
            assert_eq!(source_fingerprint("abc"), source_fingerprint("abc"));
            assert_ne!(
                source_fingerprint("mode == create"),
                source_fingerprint("mode == delete")
            );
            assert_eq!(source_fingerprint("abc").len(), 16);
        }

        #[test]
        fn cache_key_carries_version_prefix_and_source_hash() {
            let key = helper_cache_key();
            assert!(key.starts_with(&format!("{HELPER_VERSION}-")), "key: {key}");
            // version prefix + '-' + 16 hex chars
            assert_eq!(key.len(), HELPER_VERSION.len() + 1 + 16);
        }
    }

    fn ensure_helper() -> Result<std::path::PathBuf, IntegrationError> {
        let dir = helper_cache_dir();
        let key = helper_cache_key();
        let app = dir.join(format!("CAR EventKit Helper {key}.app"));
        let contents = app.join("Contents");
        let macos = contents.join("MacOS");
        let helper = macos.join("CAR EventKit Helper");
        if helper.exists() {
            return Ok(helper);
        }

        std::fs::create_dir_all(&macos)
            .map_err(|e| IntegrationError::Backend(format!("helper cache: {e}")))?;
        let source = dir.join(format!("car-eventkit-helper-{key}.swift"));
        let plist = contents.join("Info.plist");
        let entitlements = dir.join(format!("car-eventkit-helper-{key}.entitlements"));
        std::fs::write(&source, SCRIPT)
            .map_err(|e| IntegrationError::Backend(format!("eventkit helper source: {e}")))?;
        std::fs::write(
            &plist,
            r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleIdentifier</key>
  <string>ai.parslee.car.eventkit-helper</string>
  <key>CFBundleExecutable</key>
  <string>CAR EventKit Helper</string>
  <key>CFBundleName</key>
  <string>CAR EventKit Helper</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>0.9.0</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>NSCalendarsUsageDescription</key>
  <string>CAR reads calendars when an agent uses the calendar capability.</string>
  <key>NSCalendarsFullAccessUsageDescription</key>
  <string>CAR reads calendars when an agent uses the calendar capability.</string>
</dict>
</plist>
"#,
        )
        .map_err(|e| IntegrationError::Backend(format!("eventkit helper plist: {e}")))?;
        std::fs::write(
            &entitlements,
            r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.personal-information.calendars</key>
  <true/>
</dict>
</plist>
"#,
        )
        .map_err(|e| IntegrationError::Backend(format!("eventkit helper entitlements: {e}")))?;

        let status = Command::new("/usr/bin/swiftc")
            .env(
                "SWIFT_MODULE_CACHE_PATH",
                std::env::temp_dir().join("car-swift-module-cache"),
            )
            .env(
                "CLANG_MODULE_CACHE_PATH",
                std::env::temp_dir().join("car-clang-module-cache"),
            )
            .arg(&source)
            .arg("-o")
            .arg(&helper)
            .arg("-Xlinker")
            .arg("-sectcreate")
            .arg("-Xlinker")
            .arg("__TEXT")
            .arg("-Xlinker")
            .arg("__info_plist")
            .arg("-Xlinker")
            .arg(&plist)
            .status()
            .map_err(|e| IntegrationError::Backend(format!("swiftc: {e}")))?;

        if !status.success() {
            return Err(IntegrationError::Backend(format!(
                "eventkit helper compile failed with status {status}"
            )));
        }

        sign_helper(&app, &entitlements)?;
        Ok(helper)
    }

    fn helper_cache_dir() -> std::path::PathBuf {
        if let Some(path) = std::env::var_os("CAR_NATIVE_HELPER_DIR") {
            return std::path::PathBuf::from(path);
        }
        if let Some(home) = std::env::var_os("HOME") {
            return std::path::PathBuf::from(home)
                .join("Library")
                .join("Application Support")
                .join("CAR")
                .join("NativeHelpers");
        }
        std::env::temp_dir().join("car-native-helpers")
    }

    fn sign_helper(
        app: &std::path::Path,
        entitlements: &std::path::Path,
    ) -> Result<(), IntegrationError> {
        let status = Command::new("/usr/bin/codesign")
            .arg("--force")
            .arg("--sign")
            .arg("-")
            .arg("--entitlements")
            .arg(entitlements)
            .arg(app)
            .status()
            .map_err(|e| IntegrationError::Backend(format!("codesign: {e}")))?;
        if !status.success() {
            return Err(IntegrationError::Backend(format!(
                "eventkit helper codesign failed with status {status}"
            )));
        }
        Ok(())
    }
}

#[cfg(not(target_os = "macos"))]
mod backend {
    use super::*;

    pub fn authorization_status() -> String {
        // No OS calendar permission model off macOS — there's no TCC gate.
        "not_applicable".to_string()
    }

    pub fn list_calendars() -> Result<CalendarListing, IntegrationError> {
        Ok(CalendarListing {
            availability: current_backend_pending(),
            calendars: vec![],
        })
    }

    pub fn list_events(
        _start: DateTime<Utc>,
        _end: DateTime<Utc>,
        _calendar_ids: &[String],
    ) -> Result<EventListing, IntegrationError> {
        Ok(EventListing {
            availability: current_backend_pending(),
            events: vec![],
        })
    }

    pub fn create_event(_input: EventCreateInput) -> Result<EventMutationResult, IntegrationError> {
        Ok(EventMutationResult {
            ok: false,
            event: None,
            reason: Some("calendar event creation is not yet implemented on this platform".into()),
        })
    }

    pub fn update_event(_input: EventUpdateInput) -> Result<EventMutationResult, IntegrationError> {
        Ok(EventMutationResult {
            ok: false,
            event: None,
            reason: Some("calendar event updates are not yet implemented on this platform".into()),
        })
    }

    pub fn delete_event(_event_id: &str) -> Result<EventMutationResult, IntegrationError> {
        Ok(EventMutationResult {
            ok: false,
            event: None,
            reason: Some("calendar event deletion is not yet implemented on this platform".into()),
        })
    }

    fn current_backend_pending() -> Availability {
        #[cfg(target_os = "windows")]
        {
            Availability::pending(
                "msgraph",
                "MS Graph / Outlook MAPI backends not yet wired. API shape \
             is stable; downstream apps can code against it now.",
            )
        }
        #[cfg(target_os = "linux")]
        {
            Availability::pending(
                "eds",
                "Evolution Data Server + CalDAV backends not yet wired. \
             API shape is stable; downstream apps can code against it now.",
            )
        }
        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
        {
            Availability::pending("none", "Unsupported OS — no calendar backend modeled.")
        }
    }
}

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

    #[test]
    fn enriched_attendees_and_status_deserialize() {
        // Exactly the JSON shape the macOS EventKit helper (SCRIPT) now emits for
        // one event. Deserializing it into `EventListing` is the contract test
        // between the Swift `EventOut`/`AttendeeOut` and the Rust types (#68) —
        // a non-tentative/tentative distinction must survive the boundary.
        let json = r#"{
            "available": true,
            "backend": "eventkit",
            "events": [{
                "id": "evt-1",
                "calendar_id": "cal-1",
                "title": "Design sync",
                "start": "2026-06-21T15:00:00Z",
                "end": "2026-06-21T16:00:00Z",
                "all_day": false,
                "location": null,
                "notes": null,
                "status": "confirmed",
                "attendees": [
                    {"name": "Matt Liotta", "email": "matt@parslee.ai", "status": "accepted", "role": "chair", "is_current_user": true},
                    {"name": "Dan Capri", "email": "dan@example.com", "status": "tentative", "role": "required", "is_current_user": false}
                ]
            }]
        }"#;
        let listing: EventListing = serde_json::from_str(json).expect("deserialize");
        let e = &listing.events[0];
        assert_eq!(e.status.as_deref(), Some("confirmed"));
        assert_eq!(e.attendees.len(), 2);
        // The current user firmly accepted...
        assert_eq!(e.attendees[0].status.as_deref(), Some("accepted"));
        assert!(e.attendees[0].is_current_user);
        assert_eq!(e.attendees[0].email.as_deref(), Some("matt@parslee.ai"));
        assert_eq!(e.attendees[0].role.as_deref(), Some("chair"));
        // ...while another attendee is only tentative — the distinction #68 needs.
        assert_eq!(e.attendees[1].status.as_deref(), Some("tentative"));
        assert!(!e.attendees[1].is_current_user);
    }

    #[test]
    fn authorization_status_returns_a_known_label() {
        // Real, non-prompting query (the macOS EventKit helper or the off-macOS
        // stub). Must be a known label, never empty/garbage — this is the gate
        // permissionStatus("calendar") reports (car-releases#71).
        let s = authorization_status();
        assert!(
            matches!(
                s.as_str(),
                "granted" | "write_only" | "denied" | "restricted"
                    | "not_determined" | "not_applicable" | "unknown"
            ),
            "unexpected status label: {s}"
        );
        eprintln!("calendar authorization_status() = {s}");
    }

    #[test]
    fn legacy_event_without_enrichment_still_deserializes() {
        // Back-compat: an event JSON missing the new fields (older cached helper,
        // or a non-macOS backend) defaults to empty attendees + no status.
        let json = r#"{"available":true,"backend":"none","events":[{
            "id":"e","calendar_id":"c","title":"t",
            "start":"2026-06-21T15:00:00Z","end":"2026-06-21T16:00:00Z"
        }]}"#;
        let listing: EventListing = serde_json::from_str(json).expect("deserialize");
        assert!(listing.events[0].attendees.is_empty());
        assert_eq!(listing.events[0].status, None);
    }
}
