//! Service installation — launchd (macOS), systemd (Linux), Windows Service.

use std::path::PathBuf;

use crate::config::DaemonConfig;
use crate::error::{DaemonError, Result};

/// Handles installing/uninstalling autostart services.
pub struct ServiceInstaller<'a> {
    config: &'a DaemonConfig,
}

impl<'a> ServiceInstaller<'a> {
    pub fn new(config: &'a DaemonConfig) -> Self {
        Self { config }
    }

    /// Install the autostart service for the current platform.
    pub fn install(&self) -> Result<()> {
        #[cfg(target_os = "macos")]
        return self.install_launchd();

        #[cfg(target_os = "linux")]
        return self.install_systemd();

        #[cfg(target_os = "windows")]
        return self.install_windows_service();

        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
        Err(DaemonError::Unsupported(
            "autostart not supported on this platform".to_string(),
        ))
    }

    /// Uninstall the autostart service.
    pub fn uninstall(&self) -> Result<()> {
        #[cfg(target_os = "macos")]
        return self.uninstall_launchd();

        #[cfg(target_os = "linux")]
        return self.uninstall_systemd();

        #[cfg(target_os = "windows")]
        return self.uninstall_windows_service();

        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
        Err(DaemonError::Unsupported(
            "autostart not supported on this platform".to_string(),
        ))
    }

    /// Check if an autostart service is installed.
    pub fn is_installed(&self) -> bool {
        #[cfg(target_os = "windows")]
        return self.is_installed_windows();

        #[cfg(not(target_os = "windows"))]
        self.service_path().is_some_and(|p| p.exists())
    }

    /// Returns the platform-specific service file path.
    fn service_path(&self) -> Option<PathBuf> {
        let home = dirs::home_dir()?;
        #[cfg(target_os = "macos")]
        {
            Some(home.join(format!(
                "Library/LaunchAgents/com.{}.plist",
                self.config.name
            )))
        }
        #[cfg(target_os = "linux")]
        {
            Some(home.join(format!(
                ".config/systemd/user/{}.service",
                self.config.name
            )))
        }
        #[cfg(target_os = "windows")]
        {
            // Windows services are registered in the SCM, not as files.
            // We check via the service manager in is_installed() instead.
            None
        }
        #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
        {
            None
        }
    }

    // -----------------------------------------------------------------------
    // macOS — launchd
    // -----------------------------------------------------------------------

    #[cfg(target_os = "macos")]
    fn install_launchd(&self) -> Result<()> {
        let plist_path = self.service_path().ok_or_else(|| {
            DaemonError::Service("cannot determine home directory".to_string())
        })?;

        if let Some(parent) = plist_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let bin = self.config.executable.display();
        let args: String = self
            .config
            .service_args
            .iter()
            .map(|a| format!("        <string>{a}</string>"))
            .collect::<Vec<_>>()
            .join("\n");

        let log_path = self
            .config
            .log_file
            .as_deref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "/tmp/daemon-kit.log".to_string());

        let plist = format!(
            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>Label</key>
    <string>com.{name}</string>
    <key>ProgramArguments</key>
    <array>
        <string>{bin}</string>
{args}
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>{log_path}</string>
    <key>StandardErrorPath</key>
    <string>{log_path}</string>
</dict>
</plist>"#,
            name = self.config.name,
        );

        // Unload the old service if already loaded (avoids "Load failed: 5"
        // on modern macOS which rejects double-loads).
        let label = format!("com.{}", self.config.name);
        if plist_path.exists() {
            let _ = std::process::Command::new("launchctl")
                .args(["bootout", &format!("gui/{}", nix::unistd::getuid().as_raw()), &plist_path.to_string_lossy()])
                .output();
        }

        std::fs::write(&plist_path, plist)?;
        log::info!("wrote {}", plist_path.display());

        // Try modern `bootstrap` first, fall back to legacy `load`.
        let uid = nix::unistd::getuid().as_raw();
        let domain_target = format!("gui/{uid}");
        let bootstrap = std::process::Command::new("launchctl")
            .args(["bootstrap", &domain_target, &plist_path.to_string_lossy()])
            .output();

        let loaded = match &bootstrap {
            Ok(output) if output.status.success() => true,
            _ => {
                // Fallback for older macOS
                std::process::Command::new("launchctl")
                    .args(["load", &plist_path.to_string_lossy()])
                    .status()
                    .is_ok_and(|s| s.success())
            }
        };

        if loaded {
            log::info!("loaded launchd service {label}");
        } else {
            log::warn!("could not load service {label}");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    fn uninstall_launchd(&self) -> Result<()> {
        let Some(plist_path) = self.service_path() else {
            return Ok(());
        };
        if !plist_path.exists() {
            log::info!("no launchd service found");
            return Ok(());
        }
        // Try modern `bootout` first, fall back to legacy `unload`.
        let uid = nix::unistd::getuid().as_raw();
        let domain_target = format!("gui/{uid}");
        let bootout = std::process::Command::new("launchctl")
            .args(["bootout", &domain_target, &plist_path.to_string_lossy()])
            .output();
        if !bootout.is_ok_and(|o| o.status.success()) {
            let _ = std::process::Command::new("launchctl")
                .args(["unload", &plist_path.to_string_lossy()])
                .status();
        }
        std::fs::remove_file(&plist_path)?;
        log::info!("removed launchd service");
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Linux — systemd
    // -----------------------------------------------------------------------

    #[cfg(target_os = "linux")]
    fn install_systemd(&self) -> Result<()> {
        let unit_path = self.service_path().ok_or_else(|| {
            DaemonError::Service("cannot determine home directory".to_string())
        })?;

        if let Some(parent) = unit_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let bin = self.config.executable.display();
        let args = self.config.service_args.join(" ");

        let unit = format!(
            r#"[Unit]
Description={description}

[Service]
ExecStart={bin} {args}
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
"#,
            description = self.config.description,
        );

        std::fs::write(&unit_path, unit)?;
        log::info!("wrote {}", unit_path.display());

        let _ = std::process::Command::new("systemctl")
            .args(["--user", "daemon-reload"])
            .status();

        let status = std::process::Command::new("systemctl")
            .args(["--user", "enable", "--now", &self.config.name])
            .status();

        match status {
            Ok(s) if s.success() => log::info!("enabled and started systemd service"),
            _ => log::warn!(
                "could not enable service — run: systemctl --user enable --now {}",
                self.config.name
            ),
        }

        Ok(())
    }

    #[cfg(target_os = "linux")]
    fn uninstall_systemd(&self) -> Result<()> {
        let Some(unit_path) = self.service_path() else {
            return Ok(());
        };
        if !unit_path.exists() {
            log::info!("no systemd service found");
            return Ok(());
        }
        let _ = std::process::Command::new("systemctl")
            .args(["--user", "disable", "--now", &self.config.name])
            .status();
        std::fs::remove_file(&unit_path)?;
        let _ = std::process::Command::new("systemctl")
            .args(["--user", "daemon-reload"])
            .status();
        log::info!("removed systemd service");
        Ok(())
    }

    // -----------------------------------------------------------------------
    // Windows — Windows Service via SCM
    // -----------------------------------------------------------------------

    #[cfg(target_os = "windows")]
    fn install_windows_service(&self) -> Result<()> {
        use windows_service::service::{
            ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
        };
        use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};

        let manager = ServiceManager::local_computer(
            None::<&str>,
            ServiceManagerAccess::CREATE_SERVICE | ServiceManagerAccess::CONNECT,
        )
        .map_err(|e| DaemonError::Service(format!("failed to connect to SCM: {e}")))?;

        // Remove existing service if present (makes reinstall idempotent).
        if let Ok(existing) = manager.open_service(
            &self.config.name,
            ServiceAccess::STOP | ServiceAccess::DELETE | ServiceAccess::QUERY_STATUS,
        ) {
            // Best-effort stop before deletion.
            let _ = existing.stop();
            // Brief wait for the service to stop before deleting.
            std::thread::sleep(std::time::Duration::from_millis(500));
            if let Err(e) = existing.delete() {
                log::warn!("could not remove old service '{}': {e}", self.config.name);
            } else {
                log::info!("removed old Windows service '{}'", self.config.name);
                // SCM needs a moment after delete before re-create succeeds.
                std::thread::sleep(std::time::Duration::from_millis(500));
            }
        }

        let service_info = ServiceInfo {
            name: std::ffi::OsString::from(&self.config.name),
            display_name: std::ffi::OsString::from(&self.config.description),
            service_type: ServiceType::OWN_PROCESS,
            start_type: ServiceStartType::AutoStart,
            error_control: ServiceErrorControl::Normal,
            executable_path: self.config.executable.clone(),
            launch_arguments: self
                .config
                .service_args
                .iter()
                .map(|s| std::ffi::OsString::from(s))
                .collect(),
            dependencies: vec![],
            account_name: None,
            account_password: None,
        };

        let service = manager
            .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
            .map_err(|e| DaemonError::Service(format!("failed to create service: {e}")))?;

        // Configure restart-on-failure so the SCM relaunches after upgrade detection exits.
        use windows_service::service::{ServiceAction, ServiceActionType, ServiceFailureActions, ServiceFailureResetPeriod};
        let failure_actions = ServiceFailureActions {
            reset_period: ServiceFailureResetPeriod::After(std::time::Duration::from_secs(60)),
            reboot_msg: None,
            command: None,
            actions: Some(vec![
                ServiceAction { action_type: ServiceActionType::Restart, delay: std::time::Duration::from_secs(5) },
                ServiceAction { action_type: ServiceActionType::Restart, delay: std::time::Duration::from_secs(10) },
                ServiceAction { action_type: ServiceActionType::None, delay: std::time::Duration::ZERO },
            ]),
        };
        let _ = service.update_failure_actions(failure_actions);

        log::info!("installed Windows service '{}'", self.config.name);
        Ok(())
    }

    #[cfg(target_os = "windows")]
    fn uninstall_windows_service(&self) -> Result<()> {
        use windows_service::service::ServiceAccess;
        use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};

        let manager =
            ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
                .map_err(|e| DaemonError::Service(format!("failed to connect to SCM: {e}")))?;

        let service = manager
            .open_service(&self.config.name, ServiceAccess::DELETE)
            .map_err(|e| {
                DaemonError::Service(format!(
                    "failed to open service '{}': {e}",
                    self.config.name
                ))
            })?;

        service
            .delete()
            .map_err(|e| DaemonError::Service(format!("failed to delete service: {e}")))?;

        log::info!("removed Windows service '{}'", self.config.name);
        Ok(())
    }

    #[cfg(target_os = "windows")]
    fn is_installed_windows(&self) -> bool {
        use windows_service::service::ServiceAccess;
        use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};

        let Ok(manager) =
            ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
        else {
            return false;
        };
        manager
            .open_service(&self.config.name, ServiceAccess::QUERY_STATUS)
            .is_ok()
    }
}
