//! 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>,
    #[serde(default)]
    pub attendees: Vec<String>,
}

#[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()
}

/// 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::*;

    // Bump on any SCRIPT edit. ensure_helper()'s on-disk cache is keyed on
    // this string so stale binaries are superseded automatically — without
    // it, the helper compiled on first use is reused forever even after
    // bug fixes ship in the Rust source. Mirrors the contacts helper's
    // convention (#200).
    const HELPER_VERSION: &str = "v4";

    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 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: [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 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 ?? []).compactMap { $0.name ?? $0.url.absoluteString }
    )
}

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"
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"])
    }

    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 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.")
        }
    }
}
