//! Contacts capability — enumerate containers, query contacts.

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

use super::{Availability, IntegrationError};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Container {
    pub id: String,
    pub name: String,
    /// Source label (account / provider) when available.
    pub source: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
    pub id: String,
    pub container_id: Option<String>,
    pub display_name: String,
    #[serde(default)]
    pub emails: Vec<String>,
    #[serde(default)]
    pub phone_numbers: Vec<String>,
    pub organization: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub containers: Vec<Container>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub contacts: Vec<Contact>,
    /// Total match count, which may be larger than `contacts.len()` when
    /// the backend paginates.
    pub total: usize,
}

pub fn list_containers() -> Result<ContainerListing, IntegrationError> {
    backend::list_containers()
}

/// Query contacts with a free-text `query` (name, email, phone substring).
/// When `container_ids` is empty, searches all accessible containers.
pub fn list_contacts(
    query: &str,
    container_ids: &[String],
    limit: usize,
) -> Result<ContactListing, IntegrationError> {
    backend::list_contacts(query, container_ids, limit)
}

#[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.
    const HELPER_VERSION: &str = "v4";

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

struct ContainerOut: Codable {
    let id: String
    let name: String
    let source: String?
}

struct ContactOut: Codable {
    let id: String
    let container_id: String?
    let display_name: String
    let emails: [String]
    let phone_numbers: [String]
    let organization: String?
}

struct ContainerListing: Codable {
    let available: Bool
    let backend: String
    let reason: String?
    let containers: [ContainerOut]
}

struct ContactListing: Codable {
    let available: Bool
    let backend: String
    let reason: String?
    let contacts: [ContactOut]
    let total: Int
}

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

func ensureAccess(_ store: CNContactStore) -> String? {
    let status = CNContactStore.authorizationStatus(for: .contacts)
    switch status {
    case .authorized:
        return nil
    case .notDetermined:
        let semaphore = DispatchSemaphore(value: 0)
        var granted = false
        store.requestAccess(for: .contacts) { ok, _ in
            granted = ok
            semaphore.signal()
        }
        _ = semaphore.wait(timeout: .now() + 60)
        return granted ? nil : "Contacts permission was not granted"
    case .restricted:
        return "Contacts permission is restricted by system policy"
    case .denied:
        return "Contacts permission is denied"
    case .limited:
        return nil
    @unknown default:
        return "Contacts permission is unavailable"
    }
}

func displayName(_ contact: CNContact) -> String {
    if let formatted = CNContactFormatter.string(from: contact, style: .fullName), !formatted.isEmpty {
        return formatted
    }
    if !contact.nickname.isEmpty { return contact.nickname }
    if !contact.organizationName.isEmpty { return contact.organizationName }
    if let first = contact.emailAddresses.first { return first.value as String }
    return contact.identifier
}

let args = CommandLine.arguments
let mode = args.count > 1 ? args[1] : "containers"
let store = CNContactStore()
if let reason = ensureAccess(store) {
    if mode == "contacts" {
        emit(ContactListing(available: false, backend: "contacts_framework", reason: reason, contacts: [], total: 0))
    } else {
        emit(ContainerListing(available: false, backend: "contacts_framework", reason: reason, containers: []))
    }
    exit(0)
}

do {
    if mode == "contacts" {
        let query = args.count > 2 ? args[2].lowercased() : ""
        let limit = args.count > 3 ? (Int(args[3]) ?? 50) : 50
        let requested = Set(args.dropFirst(4))
        let containers = try store.containers(matching: nil).filter { requested.isEmpty || requested.contains($0.identifier) }
        // CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
        // covers given/family/middle/prefix/suffix and anything else the
        // formatter reads internally. Hand-rolling the key list previously
        // omitted CNContactMiddleNameKey, which crashed displayName() with
        // EXC_CRASH (SIGABRT) via -[CNContact middleName] → NSException
        // when the formatter touched a contact with a middle name set.
        let keys: [CNKeyDescriptor] = [
            CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
            CNContactIdentifierKey as CNKeyDescriptor,
            CNContactNicknameKey as CNKeyDescriptor,
            CNContactOrganizationNameKey as CNKeyDescriptor,
            CNContactEmailAddressesKey as CNKeyDescriptor,
            CNContactPhoneNumbersKey as CNKeyDescriptor
        ]
        var contacts: [ContactOut] = []
        var total = 0
        for container in containers {
            let predicate = CNContact.predicateForContactsInContainer(withIdentifier: container.identifier)
            for contact in try store.unifiedContacts(matching: predicate, keysToFetch: keys) {
                let name = displayName(contact)
                let emails = contact.emailAddresses.map { $0.value as String }
                let phones = contact.phoneNumbers.map { $0.value.stringValue }
                let haystack = ([name, contact.organizationName] + emails + phones).joined(separator: " ").lowercased()
                if !query.isEmpty && !haystack.contains(query) { continue }
                total += 1
                if contacts.count < limit {
                    contacts.append(ContactOut(
                        id: contact.identifier,
                        container_id: container.identifier,
                        display_name: name,
                        emails: emails,
                        phone_numbers: phones,
                        organization: contact.organizationName.isEmpty ? nil : contact.organizationName
                    ))
                }
            }
        }
        emit(ContactListing(available: true, backend: "contacts_framework", reason: nil, contacts: contacts, total: total))
    } else {
        let containers = try store.containers(matching: nil).map { container in
            ContainerOut(id: container.identifier, name: container.name, source: nil)
        }
        emit(ContainerListing(available: true, backend: "contacts_framework", reason: nil, containers: containers))
    }
} catch {
    if mode == "contacts" {
        emit(ContactListing(available: false, backend: "contacts_framework", reason: String(describing: error), contacts: [], total: 0))
    } else {
        emit(ContainerListing(available: false, backend: "contacts_framework", reason: String(describing: error), containers: []))
    }
}
"#;

    pub fn list_containers() -> Result<ContainerListing, IntegrationError> {
        run_swift(&["containers"])
    }

    pub fn list_contacts(
        query: &str,
        container_ids: &[String],
        limit: usize,
    ) -> Result<ContactListing, IntegrationError> {
        let limit = limit.to_string();
        let mut args = vec!["contacts", query, limit.as_str()];
        args.extend(container_ids.iter().map(String::as_str));
        run_swift(&args)
    }

    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!(
                "contacts swift failed: {stderr}"
            )));
        }

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

    fn ensure_helper() -> Result<std::path::PathBuf, IntegrationError> {
        let dir = helper_cache_dir();
        let app = dir.join(format!("CAR Contacts Helper {HELPER_VERSION}.app"));
        let contents = app.join("Contents");
        let macos = contents.join("MacOS");
        let helper = macos.join("CAR Contacts 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-contacts-helper-{HELPER_VERSION}.swift"));
        let plist = contents.join("Info.plist");
        let entitlements = dir.join(format!("car-contacts-helper-{HELPER_VERSION}.entitlements"));
        std::fs::write(&source, SCRIPT)
            .map_err(|e| IntegrationError::Backend(format!("contacts 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.contacts-helper</string>
  <key>CFBundleExecutable</key>
  <string>CAR Contacts Helper</string>
  <key>CFBundleName</key>
  <string>CAR Contacts Helper</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>0.9.0</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>NSContactsUsageDescription</key>
  <string>CAR reads contacts when an agent uses the contacts capability.</string>
</dict>
</plist>
"#,
        )
        .map_err(|e| IntegrationError::Backend(format!("contacts 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.addressbook</key>
  <true/>
</dict>
</plist>
"#,
        )
        .map_err(|e| IntegrationError::Backend(format!("contacts 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!(
                "contacts 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!(
                "contacts helper codesign failed with status {status}"
            )));
        }
        Ok(())
    }
}

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

    pub fn list_containers() -> Result<ContainerListing, IntegrationError> {
        Ok(ContainerListing {
            availability: current_backend_pending(),
            containers: vec![],
        })
    }

    pub fn list_contacts(
        _query: &str,
        _container_ids: &[String],
        _limit: usize,
    ) -> Result<ContactListing, IntegrationError> {
        Ok(ContactListing {
            availability: current_backend_pending(),
            contacts: vec![],
            total: 0,
        })
    }

    fn current_backend_pending() -> Availability {
        #[cfg(target_os = "windows")]
        {
            Availability::pending(
                "windows_contacts",
                "Windows Contacts API + MS Graph 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 + CardDAV 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 contacts backend modeled.")
        }
    }
}
