use anyhow::{Result, anyhow, bail, ensure};
use chrono::Utc;

use super::{
    SshService,
    context::SshMountRuntimeContext,
    support::{remote_command_failed, validate_remote_path},
    types::{SshMountRequest, SshUnmountRequest, SshUnmountResult},
};

fn mount_description(description: Option<String>, remote_path: &str, target_path: &str) -> String {
    description
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| format!("SSH mount: {remote_path} -> {target_path}"))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParsedRemoteMountPath<'a> {
    Absolute(&'a str),
    Home,
    HomeRelative(&'a str),
}

fn parse_mount_remote_path(remote_path: &str) -> Result<ParsedRemoteMountPath<'_>> {
    let remote_path = validate_remote_path(remote_path, "ssh_mount remote_path")?;
    if remote_path.starts_with('/') {
        return Ok(ParsedRemoteMountPath::Absolute(remote_path));
    }
    if remote_path == "~" {
        return Ok(ParsedRemoteMountPath::Home);
    }
    if let Some(rest) = remote_path.strip_prefix("~/") {
        return Ok(ParsedRemoteMountPath::HomeRelative(rest));
    }
    if remote_path.starts_with('~') {
        bail!(
            "ssh_mount remote_path only supports ~ or ~/..., not user-relative paths: remote_path={remote_path}"
        );
    }

    bail!(
        "ssh_mount remote_path must be an absolute path or home-relative path: remote_path={remote_path}"
    );
}

fn join_home_relative_remote_path(home: &str, rest: &str) -> String {
    if rest.is_empty() {
        return home.to_string();
    }
    if home == "/" {
        return format!("/{rest}");
    }
    format!("{home}/{rest}")
}

impl SshService {
    pub async fn mount(&self, request: SshMountRequest) -> Result<crate::ssh::SshMountSummary> {
        let connection = self.require_ready_connection(&request.connection_id, "mounting")?;

        if !self.mount_feature_available() {
            bail!(
                "ssh mount capability is unavailable on this host: capabilities={:?}",
                self.context.ssh_capabilities
            );
        }

        let backend = request
            .backend
            .unwrap_or(crate::ssh::SshMountBackend::Sshfs);
        let target_path = self
            .context
            .resolve_mount_local_path(&request.target_path)?;
        let remote_path = self
            .resolve_mount_remote_path(&request.connection_id, &request.remote_path)
            .await?;
        let validated = self.context.ssh_guard.validate_mount_request(
            &self.context.ssh_config,
            crate::ssh::guard::SshMountValidationInput {
                local_path: &target_path,
                remote_path: &remote_path,
            },
        )?;

        let created_local_path = self
            .context
            .ensure_mount_local_path(&validated.local_path, request.create_target)?;
        let mount = crate::ssh::SshMountSummary {
            mount_id: crate::ssh::SshMountId::new(),
            title: request.title,
            description: Some(mount_description(
                request.description,
                &validated.remote_path,
                &validated.local_path.display().to_string(),
            )),
            connection_id: connection.connection_id.clone(),
            target_summary: connection.target_summary.clone(),
            status: crate::ssh::SshMountStatus::Mounting,
            backend,
            local_path: validated.local_path.display().to_string(),
            remote_path: validated.remote_path,
            read_only: request.read_only,
            mounted_at: Utc::now(),
            last_error: None,
        };

        self.context.ssh_registry.upsert_mount(mount.clone());
        self.context.remember_mount_runtime_context(
            &mount.mount_id,
            SshMountRuntimeContext {
                managed_path: validated.is_managed_path,
                created_local_path,
            },
        );

        let connection_context = self.context.runtime_context_for_connection(&connection);
        let result = self
            .context
            .ssh_runtime
            .mount(crate::ssh::runtime::SshMountPlanRequest {
                mount: mount.clone(),
                connection: connection.clone(),
                auth_kind: connection_context.auth_kind,
                identity_path: connection_context.identity_path.clone(),
                verify_host_key: connection_context.verify_host_key,
                macos_block_apple_metadata: self.context.ssh_config.macos_block_apple_metadata,
                sshfs_bin_path: self.context.ssh_config.resolved_sshfs_bin_path(),
            })
            .await;

        match result {
            Ok(()) => {
                let mut mounted = mount;
                mounted.status = crate::ssh::SshMountStatus::Mounted;
                mounted.last_error = None;
                self.context.ssh_registry.upsert_mount(mounted.clone());
                Ok(mounted)
            }
            Err(error) => {
                let mut failed = mount;
                failed.status = crate::ssh::SshMountStatus::Failed;
                failed.last_error = Some(error.to_string());
                self.context.ssh_registry.upsert_mount(failed);
                Err(error)
            }
        }
    }

    pub async fn unmount(&self, request: SshUnmountRequest) -> Result<SshUnmountResult> {
        let mount = self
            .context
            .ssh_registry
            .get_mount(&request.mount_id)
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "ssh mount not found: mount_id={}",
                    request.mount_id.as_str()
                )
            })?;

        let context = self
            .context
            .mount_runtime_context_for_mount(&request.mount_id);
        let previous_status = mount.status.clone();
        let mut unmounting = mount.clone();
        unmounting.status = crate::ssh::SshMountStatus::Unmounting;
        self.context.ssh_registry.upsert_mount(unmounting.clone());

        let result = self
            .context
            .ssh_runtime
            .unmount(crate::ssh::runtime::SshUnmountRequest {
                mount: unmounting.clone(),
                force: request.force,
                umount_bin_path: self.context.ssh_config.resolved_umount_bin_path(),
                diskutil_bin_path: self.context.ssh_config.resolved_diskutil_bin_path(),
            })
            .await;

        match result {
            Ok(()) => {
                let cleanup_local_path = if request.cleanup_target {
                    match self
                        .context
                        .cleanup_mount_local_path_if_allowed(&mount, &context)
                    {
                        Ok(cleaned) => cleaned,
                        Err(error) => {
                            let mut failed = mount;
                            failed.status = crate::ssh::SshMountStatus::Failed;
                            failed.last_error = Some(error.to_string());
                            self.context.ssh_registry.upsert_mount(failed);
                            return Err(error);
                        }
                    }
                } else {
                    false
                };

                let mut unmounted = mount;
                unmounted.status = crate::ssh::SshMountStatus::Unmounted;
                unmounted.last_error = None;
                self.context.ssh_registry.upsert_mount(unmounted.clone());

                Ok(SshUnmountResult {
                    mount: unmounted,
                    previous_status,
                    cleanup_target: cleanup_local_path,
                })
            }
            Err(error) => {
                let mut failed = mount;
                failed.status = crate::ssh::SshMountStatus::Failed;
                failed.last_error = Some(error.to_string());
                self.context.ssh_registry.upsert_mount(failed);
                Err(error)
            }
        }
    }

    async fn resolve_mount_remote_path(
        &self,
        connection_id: &crate::ssh::SshConnectionId,
        remote_path: &str,
    ) -> Result<String> {
        match parse_mount_remote_path(remote_path)? {
            ParsedRemoteMountPath::Absolute(path) => Ok(path.to_string()),
            ParsedRemoteMountPath::Home => self.resolve_remote_home_for_mount(connection_id).await,
            ParsedRemoteMountPath::HomeRelative(rest) => {
                let home = self.resolve_remote_home_for_mount(connection_id).await?;
                Ok(join_home_relative_remote_path(&home, rest))
            }
        }
    }

    async fn resolve_remote_home_for_mount(
        &self,
        connection_id: &crate::ssh::SshConnectionId,
    ) -> Result<String> {
        let output = self
            .run_ssh_capture(
                connection_id,
                "set -eu\nprintf '%s' \"${HOME:?}\"",
                Some("failed to resolve remote home for ssh_mount"),
                None,
            )
            .await?;

        if !output.status.success() {
            return Err(remote_command_failed(
                "failed to resolve remote home for ssh_mount",
                connection_id,
                None,
                output,
            ));
        }

        let home = String::from_utf8(output.stdout).map_err(|_| {
            anyhow!(
                "failed to resolve remote home for ssh_mount: connection_id={} remote HOME is not valid UTF-8",
                connection_id.as_str()
            )
        })?;
        let home = home.trim();
        ensure!(
            !home.is_empty() && home.starts_with('/'),
            "failed to resolve remote home for ssh_mount: connection_id={} remote_home={home:?}",
            connection_id.as_str()
        );

        Ok(home.to_string())
    }
}

#[cfg(test)]
mod tests {
    use crate::Config;

    use super::*;

    #[test]
    fn parse_mount_remote_path_accepts_absolute_and_home_relative_forms() {
        assert_eq!(
            parse_mount_remote_path("/srv/project").unwrap(),
            ParsedRemoteMountPath::Absolute("/srv/project")
        );
        assert_eq!(
            parse_mount_remote_path("~").unwrap(),
            ParsedRemoteMountPath::Home
        );
        assert_eq!(
            parse_mount_remote_path("~/workspace/sdc-skill").unwrap(),
            ParsedRemoteMountPath::HomeRelative("workspace/sdc-skill")
        );
        assert_eq!(
            parse_mount_remote_path("~/").unwrap(),
            ParsedRemoteMountPath::HomeRelative("")
        );
    }

    #[test]
    fn parse_mount_remote_path_rejects_invalid_forms() {
        let relative = parse_mount_remote_path("relative/path").expect_err("relative path");
        assert!(format!("{relative:#}").contains("absolute path or home-relative path"));

        let user_relative = parse_mount_remote_path("~alice/project").expect_err("user-relative");
        assert!(format!("{user_relative:#}").contains("only supports ~ or ~/..."));

        let empty = parse_mount_remote_path(" ").expect_err("empty path");
        assert!(format!("{empty:#}").contains("cannot be empty"));
    }

    #[test]
    fn join_home_relative_remote_path_preserves_literal_suffix() {
        assert_eq!(
            join_home_relative_remote_path("/home/alice", ""),
            "/home/alice"
        );
        assert_eq!(
            join_home_relative_remote_path("/home/alice", "../project"),
            "/home/alice/../project"
        );
        assert_eq!(
            join_home_relative_remote_path("/", "workspace"),
            "/workspace"
        );
    }

    #[test]
    fn ensure_mount_local_path_handles_creation_rules() {
        let app = super::super::AppState::new(Config::default());
        let base = std::env::temp_dir().join(format!("pty-mcp-mount-{}", uuid::Uuid::new_v4()));

        let missing = app.context.ensure_mount_local_path(&base, false);
        assert!(missing.is_err());

        let created = app.context.ensure_mount_local_path(&base, true).unwrap();
        assert!(created);
        assert!(base.is_dir());

        std::fs::remove_dir_all(&base).unwrap();
    }

    #[test]
    fn cleanup_mount_local_path_only_for_managed_created_paths() {
        let app = super::super::AppState::new(Config::default());
        let base = std::env::temp_dir().join(format!("pty-mcp-mount-{}", uuid::Uuid::new_v4()));
        std::fs::create_dir_all(&base).unwrap();

        let mount = crate::ssh::SshMountSummary {
            mount_id: crate::ssh::SshMountId::new(),
            title: None,
            description: None,
            connection_id: crate::ssh::SshConnectionId::new(),
            target_summary: "example.com".into(),
            status: crate::ssh::SshMountStatus::Mounted,
            backend: crate::ssh::SshMountBackend::Sshfs,
            local_path: base.display().to_string(),
            remote_path: "/remote".into(),
            read_only: false,
            mounted_at: Utc::now(),
            last_error: None,
        };

        let skipped = app
            .context
            .cleanup_mount_local_path_if_allowed(
                &mount,
                &SshMountRuntimeContext {
                    managed_path: false,
                    created_local_path: true,
                },
            )
            .unwrap();
        assert!(!skipped);
        assert!(base.exists());

        let removed = app
            .context
            .cleanup_mount_local_path_if_allowed(
                &mount,
                &SshMountRuntimeContext {
                    managed_path: true,
                    created_local_path: true,
                },
            )
            .unwrap();
        assert!(removed);
        assert!(!base.exists());
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn unmount_marks_mount_failed_when_cleanup_fails() {
        use std::os::unix::fs::PermissionsExt;

        let sandbox = std::env::temp_dir().join(format!("pty-mcp-mount-{}", uuid::Uuid::new_v4()));
        let local_path = sandbox.join("mount");
        std::fs::create_dir_all(&local_path).unwrap();

        let umount_bin = sandbox.join("umount");
        std::fs::write(&umount_bin, "#!/bin/sh\nexit 0\n").unwrap();
        let mut permissions = std::fs::metadata(&umount_bin).unwrap().permissions();
        permissions.set_mode(0o755);
        std::fs::set_permissions(&umount_bin, permissions).unwrap();

        let mut config = Config::default();
        config.ssh.umount_bin_path = Some(umount_bin);
        let app = super::super::AppState::new(config);

        let mount = crate::ssh::SshMountSummary {
            mount_id: crate::ssh::SshMountId::new(),
            title: None,
            description: None,
            connection_id: crate::ssh::SshConnectionId::new(),
            target_summary: "example.com".into(),
            status: crate::ssh::SshMountStatus::Mounted,
            backend: crate::ssh::SshMountBackend::Sshfs,
            local_path: local_path.display().to_string(),
            remote_path: "/remote".into(),
            read_only: false,
            mounted_at: Utc::now(),
            last_error: None,
        };
        app.context.ssh_registry.upsert_mount(mount.clone());
        app.context.remember_mount_runtime_context(
            &mount.mount_id,
            SshMountRuntimeContext {
                managed_path: true,
                created_local_path: true,
            },
        );

        let parent_permissions = std::fs::metadata(&sandbox).unwrap().permissions();
        let mut readonly_permissions = parent_permissions.clone();
        readonly_permissions.set_mode(0o500);
        std::fs::set_permissions(&sandbox, readonly_permissions).unwrap();

        let result = app
            .ssh()
            .unmount(SshUnmountRequest {
                mount_id: mount.mount_id.clone(),
                force: false,
                cleanup_target: true,
            })
            .await;

        let mut restored_permissions = parent_permissions;
        restored_permissions.set_mode(0o700);
        std::fs::set_permissions(&sandbox, restored_permissions).unwrap();

        assert!(result.is_err());
        let updated = app.context.ssh_registry.get_mount(&mount.mount_id).unwrap();
        assert_eq!(updated.status, crate::ssh::SshMountStatus::Failed);
        assert!(
            updated
                .last_error
                .as_deref()
                .unwrap_or_default()
                .contains("failed to remove")
        );

        std::fs::remove_dir_all(&sandbox).unwrap();
    }
}
