//! macOS account-backed Apple app integrations not covered by Calendar,
//! Contacts, Mail, or Messages.
//!
//! These capabilities are advertised by Internet Accounts on macOS. Apple
//! exposes them through a mix of public automation dictionaries and protected
//! on-disk stores. Each method returns an availability envelope instead of
//! treating TCC/Full-Disk-Access denial as an exceptional transport failure.

use crate::{Availability, IntegrationError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NoteAccount {
    pub id: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NoteSummary {
    pub id: String,
    pub name: String,
    pub folder: Option<String>,
    pub modified: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NotesAccountListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub accounts: Vec<NoteAccount>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NotesListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub notes: Vec<NoteSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReminderList {
    pub id: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReminderItem {
    pub id: String,
    pub name: String,
    pub list: Option<String>,
    pub due: Option<String>,
    pub completed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReminderListListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub lists: Vec<ReminderList>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReminderItemListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub reminders: Vec<ReminderItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PhotoAlbum {
    pub id: String,
    pub name: String,
    pub count: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PhotoAlbumListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub albums: Vec<PhotoAlbum>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Bookmark {
    pub title: String,
    pub url: Option<String>,
    pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BookmarkListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub bookmarks: Vec<Bookmark>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FileLocation {
    pub id: String,
    pub name: String,
    pub path: String,
    pub exists: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FileLocationListing {
    #[serde(flatten)]
    pub availability: Availability,
    pub locations: Vec<FileLocation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct KeychainStatus {
    #[serde(flatten)]
    pub availability: Availability,
}

pub fn notes_accounts() -> Result<NotesAccountListing, IntegrationError> {
    backend::notes_accounts()
}

pub fn notes_find(query: &str, limit: usize) -> Result<NotesListing, IntegrationError> {
    backend::notes_find(query, limit)
}

pub fn reminders_lists() -> Result<ReminderListListing, IntegrationError> {
    backend::reminders_lists()
}

pub fn reminders_items(limit: usize) -> Result<ReminderItemListing, IntegrationError> {
    backend::reminders_items(limit)
}

pub fn photos_albums() -> Result<PhotoAlbumListing, IntegrationError> {
    backend::photos_albums()
}

pub fn bookmarks_list(limit: usize) -> Result<BookmarkListing, IntegrationError> {
    backend::bookmarks_list(limit)
}

pub fn files_locations() -> Result<FileLocationListing, IntegrationError> {
    backend::files_locations()
}

pub fn keychain_status() -> Result<KeychainStatus, IntegrationError> {
    backend::keychain_status()
}

#[cfg(target_os = "macos")]
mod backend {
    use super::*;
    use serde_json::Value;
    use std::path::PathBuf;
    use std::process::Command;

    const NOTES_JXA: &str = r#"
function app() { const a = Application("/System/Applications/Notes.app"); a.includeStandardAdditions = true; return a; }
function accountOut(a) {
  let id = ""; let name = "";
  try { id = String(a.id()); } catch (e) {}
  try { name = String(a.name()); } catch (e) {}
  return {id: id || name, name: name || id};
}
function noteOut(n) {
  let id = ""; let name = ""; let folder = null; let modified = null;
  try { id = String(n.id()); } catch (e) {}
  try { name = String(n.name()); } catch (e) {}
  try { folder = String(n.container().name()); } catch (e) {}
  try { modified = String(n.modificationDate()); } catch (e) {}
  return {id: id || name, name: name || id, folder: folder, modified: modified};
}
function run(argv) {
  const mode = argv[0] || "accounts";
  try {
    const Notes = app();
    if (mode === "accounts") {
      return JSON.stringify({available:true, backend:"notes_app", reason:null, accounts: Notes.accounts().map(accountOut)});
    }
    const query = String(argv[1] || "").toLowerCase();
    const limit = Number(argv[2] || "50");
    let out = [];
    Notes.accounts().forEach(a => {
      a.notes().forEach(n => {
        let hay = "";
        try { hay += " " + String(n.name()).toLowerCase(); } catch (e) {}
        try { hay += " " + String(n.plaintext()).toLowerCase(); } catch (e) {}
        if (!query || hay.indexOf(query) >= 0) out.push(noteOut(n));
      });
    });
    return JSON.stringify({available:true, backend:"notes_app", reason:null, notes: out.slice(0, limit)});
  } catch (e) {
    if (mode === "accounts") return JSON.stringify({available:false, backend:"notes_app", reason:String(e), accounts:[]});
    return JSON.stringify({available:false, backend:"notes_app", reason:String(e), notes:[]});
  }
}
"#;

    const REMINDERS_JXA: &str = r#"
function app() { const a = Application("/System/Applications/Reminders.app"); a.includeStandardAdditions = true; return a; }
function listOut(l) {
  let id = ""; let name = "";
  try { id = String(l.id()); } catch (e) {}
  try { name = String(l.name()); } catch (e) {}
  return {id: id || name, name: name || id};
}
function itemOut(r) {
  let id = ""; let name = ""; let list = null; let due = null; let completed = false;
  try { id = String(r.id()); } catch (e) {}
  try { name = String(r.name()); } catch (e) {}
  try { list = String(r.container().name()); } catch (e) {}
  try { due = String(r.dueDate()); } catch (e) {}
  try { completed = !!r.completed(); } catch (e) {}
  return {id: id || name, name: name || id, list: list, due: due, completed: completed};
}
function run(argv) {
  const mode = argv[0] || "lists";
  try {
    const Reminders = app();
    if (mode === "lists") {
      return JSON.stringify({available:true, backend:"reminders_app", reason:null, lists: Reminders.lists().map(listOut)});
    }
    const limit = Number(argv[1] || "50");
    let out = [];
    Reminders.lists().forEach(l => l.reminders().forEach(r => { if (!r.completed()) out.push(itemOut(r)); }));
    return JSON.stringify({available:true, backend:"reminders_app", reason:null, reminders: out.slice(0, limit)});
  } catch (e) {
    if (mode === "lists") return JSON.stringify({available:false, backend:"reminders_app", reason:String(e), lists:[]});
    return JSON.stringify({available:false, backend:"reminders_app", reason:String(e), reminders:[]});
  }
}
"#;

    const PHOTOS_JXA: &str = r#"
function run(argv) {
  try {
    const Photos = Application("/System/Applications/Photos.app");
    Photos.includeStandardAdditions = true;
    const albums = Photos.albums().map(a => {
      let id = ""; let name = ""; let count = null;
      try { id = String(a.id()); } catch (e) {}
      try { name = String(a.name()); } catch (e) {}
      try { count = a.mediaItems().length; } catch (e) {}
      return {id: id || name, name: name || id, count: count};
    });
    return JSON.stringify({available:true, backend:"photos_app", reason:null, albums: albums});
  } catch (e) {
    return JSON.stringify({available:false, backend:"photos_app", reason:String(e), albums:[]});
  }
}
"#;

    pub fn notes_accounts() -> Result<NotesAccountListing, IntegrationError> {
        Ok(
            run_jxa(NOTES_JXA, &["accounts"]).unwrap_or_else(|e| NotesAccountListing {
                availability: Availability::pending("notes_app", e.to_string()),
                accounts: vec![],
            }),
        )
    }

    pub fn notes_find(query: &str, limit: usize) -> Result<NotesListing, IntegrationError> {
        Ok(
            run_jxa(NOTES_JXA, &["find", query, &limit.to_string()]).unwrap_or_else(|e| {
                NotesListing {
                    availability: Availability::pending("notes_app", e.to_string()),
                    notes: vec![],
                }
            }),
        )
    }

    pub fn reminders_lists() -> Result<ReminderListListing, IntegrationError> {
        Ok(
            run_jxa(REMINDERS_JXA, &["lists"]).unwrap_or_else(|e| ReminderListListing {
                availability: Availability::pending("reminders_app", e.to_string()),
                lists: vec![],
            }),
        )
    }

    pub fn reminders_items(limit: usize) -> Result<ReminderItemListing, IntegrationError> {
        Ok(
            run_jxa(REMINDERS_JXA, &["items", &limit.to_string()]).unwrap_or_else(|e| {
                ReminderItemListing {
                    availability: Availability::pending("reminders_app", e.to_string()),
                    reminders: vec![],
                }
            }),
        )
    }

    pub fn photos_albums() -> Result<PhotoAlbumListing, IntegrationError> {
        Ok(
            run_jxa(PHOTOS_JXA, &[]).unwrap_or_else(|e| PhotoAlbumListing {
                availability: Availability::pending("photos_app", e.to_string()),
                albums: vec![],
            }),
        )
    }

    pub fn bookmarks_list(limit: usize) -> Result<BookmarkListing, IntegrationError> {
        let mut path = home();
        path.push("Library/Safari/Bookmarks.plist");
        let output = Command::new("/usr/bin/plutil")
            .args(["-convert", "json", "-o", "-"])
            .arg(path)
            .output()
            .map_err(|e| IntegrationError::Backend(format!("bookmarks plutil: {e}")))?;
        if !output.status.success() {
            return Ok(BookmarkListing {
                availability: Availability::pending(
                    "safari_bookmarks",
                    String::from_utf8_lossy(&output.stderr).trim().to_string(),
                ),
                bookmarks: vec![],
            });
        }
        let value: Value = serde_json::from_slice(&output.stdout)
            .map_err(|e| IntegrationError::Backend(format!("bookmarks json: {e}")))?;
        let mut bookmarks = Vec::new();
        collect_bookmarks(&value, &mut bookmarks, limit);
        Ok(BookmarkListing {
            availability: Availability::available("safari_bookmarks"),
            bookmarks,
        })
    }

    pub fn files_locations() -> Result<FileLocationListing, IntegrationError> {
        let mut locations = Vec::new();
        let mut add = |id: &str, name: &str, path: PathBuf| {
            locations.push(FileLocation {
                id: id.to_string(),
                name: name.to_string(),
                exists: path.exists(),
                path: path.to_string_lossy().to_string(),
            });
        };
        let home = home();
        add(
            "icloud_drive",
            "iCloud Drive",
            home.join("Library/Mobile Documents/com~apple~CloudDocs"),
        );
        add("desktop", "Desktop", home.join("Desktop"));
        add("documents", "Documents", home.join("Documents"));
        let available = locations.iter().any(|location| location.exists);
        Ok(FileLocationListing {
            availability: if available {
                Availability::available("macos_files")
            } else {
                Availability::pending("macos_files", "No standard macOS file locations found.")
            },
            locations,
        })
    }

    pub fn keychain_status() -> Result<KeychainStatus, IntegrationError> {
        let check = car_secrets::SecretStore::new().availability();
        Ok(KeychainStatus {
            availability: if check.available {
                Availability::available("keychain")
            } else {
                Availability::pending(
                    "keychain",
                    check
                        .reason
                        .unwrap_or_else(|| "macOS Keychain is unavailable.".to_string()),
                )
            },
        })
    }

    fn run_jxa<T: serde::de::DeserializeOwned>(
        script: &str,
        args: &[&str],
    ) -> Result<T, IntegrationError> {
        let mut child = Command::new("/usr/bin/osascript")
            .arg("-l")
            .arg("JavaScript")
            .arg("-")
            .args(args)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()
            .map_err(|e| IntegrationError::Backend(format!("osascript: {e}")))?;
        {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin
                    .write_all(script.as_bytes())
                    .map_err(|e| IntegrationError::Backend(format!("osascript stdin: {e}")))?;
            }
        }

        let start = std::time::Instant::now();
        loop {
            if child
                .try_wait()
                .map_err(|e| IntegrationError::Backend(format!("osascript wait: {e}")))?
                .is_some()
            {
                break;
            }
            if start.elapsed() > std::time::Duration::from_secs(5) {
                let _ = child.kill();
                let _ = child.wait();
                return Err(IntegrationError::Backend(
                    "osascript timed out waiting for Apple Events response".to_string(),
                ));
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }

        let output = child
            .wait_with_output()
            .map_err(|e| IntegrationError::Backend(format!("osascript output: {e}")))?;
        if !output.status.success() {
            return Err(IntegrationError::Backend(format!(
                "osascript failed: {}",
                String::from_utf8_lossy(&output.stderr).trim()
            )));
        }
        serde_json::from_slice(&output.stdout)
            .map_err(|e| IntegrationError::Backend(format!("osascript json: {e}")))
    }

    fn collect_bookmarks(value: &Value, out: &mut Vec<Bookmark>, limit: usize) {
        if out.len() >= limit {
            return;
        }
        if let Some(url) = value.get("URLString").and_then(Value::as_str) {
            let title = value
                .get("URIDictionary")
                .and_then(|v| v.get("title"))
                .and_then(Value::as_str)
                .or_else(|| value.get("Title").and_then(Value::as_str))
                .unwrap_or(url);
            out.push(Bookmark {
                title: title.to_string(),
                url: Some(url.to_string()),
                source: "safari".to_string(),
            });
        }
        if let Some(children) = value.get("Children").and_then(Value::as_array) {
            for child in children {
                collect_bookmarks(child, out, limit);
                if out.len() >= limit {
                    break;
                }
            }
        }
    }

    fn home() -> PathBuf {
        PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
    }
}

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

    pub fn notes_accounts() -> Result<NotesAccountListing, IntegrationError> {
        Ok(NotesAccountListing {
            availability: pending("notes_app"),
            accounts: vec![],
        })
    }

    pub fn notes_find(_query: &str, _limit: usize) -> Result<NotesListing, IntegrationError> {
        Ok(NotesListing {
            availability: pending("notes_app"),
            notes: vec![],
        })
    }

    pub fn reminders_lists() -> Result<ReminderListListing, IntegrationError> {
        Ok(ReminderListListing {
            availability: pending("reminders_app"),
            lists: vec![],
        })
    }

    pub fn reminders_items(_limit: usize) -> Result<ReminderItemListing, IntegrationError> {
        Ok(ReminderItemListing {
            availability: pending("reminders_app"),
            reminders: vec![],
        })
    }

    pub fn photos_albums() -> Result<PhotoAlbumListing, IntegrationError> {
        Ok(PhotoAlbumListing {
            availability: pending("photos_app"),
            albums: vec![],
        })
    }

    pub fn bookmarks_list(_limit: usize) -> Result<BookmarkListing, IntegrationError> {
        Ok(BookmarkListing {
            availability: pending("safari_bookmarks"),
            bookmarks: vec![],
        })
    }

    pub fn files_locations() -> Result<FileLocationListing, IntegrationError> {
        Ok(FileLocationListing {
            availability: pending("macos_files"),
            locations: vec![],
        })
    }

    pub fn keychain_status() -> Result<KeychainStatus, IntegrationError> {
        let check = car_secrets::SecretStore::new().availability();
        Ok(KeychainStatus {
            availability: if check.available {
                Availability::available("keychain")
            } else {
                Availability::pending(
                    "keychain",
                    check
                        .reason
                        .unwrap_or_else(|| "OS keychain is unavailable.".to_string()),
                )
            },
        })
    }

    fn pending(backend: &'static str) -> Availability {
        Availability::pending(backend, "This Apple integration is only modeled on macOS.")
    }
}
