use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};

use crate::config;
use crate::store::{HostSource, LauncherStore, ManagedHost};

const EXPORT_HEADER: &str = "# Generated by SSHub — do not edit by hand\n";

/// Default export path: `config_dir()/exported.conf`.
pub fn exported_conf_path() -> Result<PathBuf> {
    Ok(config::config_dir()?.join("exported.conf"))
}

/// Write launcher-native hosts to `path` using atomic replace + `.bak` backup.
pub fn export_launcher_hosts_to(store: &LauncherStore, path: &Path) -> Result<PathBuf> {
    let content = render_launcher_hosts(store)?;
    atomic_write_with_backup(path, &content)?;
    Ok(path.to_path_buf())
}

/// Write launcher-native hosts to `exported.conf` using atomic replace + `.bak` backup.
pub fn export_launcher_hosts(store: &LauncherStore) -> Result<PathBuf> {
    let path = exported_conf_path()?;
    export_launcher_hosts_to(store, &path)
}

fn render_launcher_hosts(store: &LauncherStore) -> Result<String> {
    let hosts = store.list_hosts_filtered(Some(HostSource::Launcher))?;
    let mut out = String::from(EXPORT_HEADER);
    for host in hosts {
        out.push_str(&render_host_block(&host));
        out.push('\n');
    }
    Ok(out)
}

/// Collapse anything that could break out of a single ssh_config line. A field
/// carrying an embedded newline would otherwise inject arbitrary directives
/// (e.g. a `Host *` stanza) into the exported file, so flatten CR/LF to spaces.
fn conf_val(s: &str) -> String {
    s.replace(['\n', '\r'], " ")
}

fn render_host_block(host: &ManagedHost) -> String {
    let mut lines = vec![format!("Host {}", conf_val(&host.name))];
    lines.push(format!("    HostName {}", conf_val(&host.address)));

    let user = host
        .username
        .as_deref()
        .or_else(|| host.identity.as_ref().and_then(|i| i.username.as_deref()));
    if let Some(user) = user {
        lines.push(format!("    User {}", conf_val(user)));
    }

    if host.port != 22 {
        lines.push(format!("    Port {}", host.port));
    }

    if let Some(key) = host.identity.as_ref().and_then(|i| i.private_key.as_ref()) {
        lines.push(format!(
            "    IdentityFile {}",
            conf_val(&key.to_string_lossy())
        ));
    }

    if let Some(cert) = host.identity.as_ref().and_then(|i| i.certificate.as_ref()) {
        lines.push(format!(
            "    CertificateFile {}",
            conf_val(&cert.to_string_lossy())
        ));
    }

    if let Some(jump) = &host.proxy_jump {
        if !jump.is_empty() {
            lines.push(format!("    ProxyJump {}", conf_val(jump)));
        }
    }

    if host.forward_agent {
        lines.push("    ForwardAgent yes".to_string());
    }

    if let Some(cmd) = &host.remote_command {
        if !cmd.is_empty() {
            lines.push(format!(
                "    {}",
                crate::ssh::host::format_ssh_config_option("RemoteCommand", &conf_val(cmd))
            ));
        }
    }

    lines.join("\n")
}

/// Write `content` to `path` atomically, backing up an existing file to `path.bak`.
pub fn atomic_write_with_backup(path: &Path, content: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("create export directory {}", parent.display()))?;
    }

    let tmp = path.with_extension("conf.tmp");
    fs::write(&tmp, content)
        .with_context(|| format!("write temporary export file {}", tmp.display()))?;

    if path.exists() {
        let bak = backup_path(path);
        fs::copy(path, &bak)
            .with_context(|| format!("backup {} to {}", path.display(), bak.display()))?;
    }

    fs::rename(&tmp, path)
        .with_context(|| format!("rename {} to {}", tmp.display(), path.display()))?;
    Ok(())
}

fn backup_path(path: &Path) -> PathBuf {
    PathBuf::from(format!("{}.bak", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::store::{LauncherStore, NewHost};

    #[test]
    fn render_includes_identity_and_connection_fields() {
        let store = LauncherStore::open_in_memory().unwrap();
        let default_id = store.get_identity_by_name("Default").unwrap().unwrap().id;
        store
            .update_identity(
                default_id,
                &crate::store::IdentityUpdate {
                    username: Some(Some("admin".into())),
                    private_key: Some(Some(PathBuf::from("/home/user/.ssh/id_ed25519"))),
                    ..Default::default()
                },
            )
            .unwrap();

        store
            .create_host(&NewHost {
                name: "dev-partners".into(),
                label: Some("Dev Partners".into()),
                address: "10.100.19.123".into(),
                port: 22,
                group_id: None,
                identity_id: Some(default_id),
                tags: vec![],
                notes: None,
                ..Default::default()
            })
            .unwrap();

        let content = render_launcher_hosts(&store).unwrap();
        assert!(content.contains("Host dev-partners"));
        assert!(content.contains("HostName 10.100.19.123"));
        assert!(content.contains("User admin"));
        assert!(content.contains("IdentityFile /home/user/.ssh/id_ed25519"));
        assert!(!content.contains("Port 22"));
    }

    #[test]
    fn render_includes_certificate_file() {
        let store = LauncherStore::open_in_memory().unwrap();
        let default_id = store.get_identity_by_name("Default").unwrap().unwrap().id;
        store
            .update_identity(
                default_id,
                &crate::store::IdentityUpdate {
                    private_key: Some(Some(PathBuf::from("/home/user/.ssh/id_ed25519"))),
                    certificate: Some(Some(PathBuf::from("/home/user/.ssh/id_ed25519-cert.pub"))),
                    ..Default::default()
                },
            )
            .unwrap();

        store
            .create_host(&NewHost {
                name: "cert-host".into(),
                address: "10.0.0.1".into(),
                port: 22,
                identity_id: Some(default_id),
                ..Default::default()
            })
            .unwrap();

        let content = render_launcher_hosts(&store).unwrap();
        assert!(content.contains("IdentityFile /home/user/.ssh/id_ed25519"));
        assert!(content.contains("CertificateFile /home/user/.ssh/id_ed25519-cert.pub"));
    }

    #[test]
    fn render_includes_remote_command() {
        let store = LauncherStore::open_in_memory().unwrap();
        store
            .create_host(&NewHost {
                name: "tmux-host".into(),
                address: "10.0.0.1".into(),
                port: 22,
                remote_command: Some("tmux attach".into()),
                ..Default::default()
            })
            .unwrap();

        let content = render_launcher_hosts(&store).unwrap();
        assert!(content.contains("RemoteCommand=\"tmux attach\""));
    }

    #[test]
    fn export_skips_ssh_config_rows() {
        let store = LauncherStore::open_in_memory().unwrap();
        store
            .upsert_ssh_config_host(&crate::store::SshConfigHostImport {
                name: "imported".into(),
                address: "10.0.0.9".into(),
                port: 22,
                proxy_jump: None,
                forward_agent: false,
                remote_command: None,
                ssh_config_hash: "hash".into(),
                tags: vec![],
                notes: None,
                environment: None,
                favorite: false,
                last_connected: None,
            })
            .unwrap();

        let content = render_launcher_hosts(&store).unwrap();
        assert_eq!(content, EXPORT_HEADER);
    }

    #[test]
    fn atomic_write_creates_backup() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("exported.conf");
        atomic_write_with_backup(&path, "first\n").unwrap();
        atomic_write_with_backup(&path, "second\n").unwrap();

        assert_eq!(fs::read_to_string(&path).unwrap(), "second\n");
        let bak = backup_path(&path);
        assert_eq!(fs::read_to_string(bak).unwrap(), "first\n");
    }
}
