use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use tokio::sync::RwLock;
use tracing::{debug, info};
use uuid::Uuid;

use crate::backend::{MergeStatus, VcsCapabilities, WorkspaceBackend};
use crate::detect::{detect_vcs, VcsDetection};
use crate::error::{Result, WorktreeError};
#[cfg(feature = "git")]
use crate::git::{GitBackend, GitConstraints};
use crate::handle::WorktreeHandle;
#[cfg(feature = "jj")]
use crate::jj::JjBackend;

/// Manages workspace lifecycle for code-modifying agents.
///
/// Holds a `Box<dyn WorkspaceBackend>` for VCS operations and an in-memory
/// `active` map for tracking handles across the process lifetime. VCS is
/// selected at construction via `auto_detect`, `new_jj`, or `new_git`.
///
/// **No external callers exist at this time** — `vapora-worktree` is consumed
/// only internally. External callers will appear during WS-5 (agent coordinator
/// integration), at which point the public API here should be stabilised.
pub struct WorktreeManager {
    backend: Box<dyn WorkspaceBackend>,
    active: Arc<RwLock<HashMap<String, WorktreeHandle>>>,
}

impl WorktreeManager {
    pub fn new(backend: Box<dyn WorkspaceBackend>) -> Self {
        Self {
            backend,
            active: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    #[cfg(feature = "jj")]
    pub fn new_jj(repo_path: PathBuf, workspace_base: PathBuf) -> Result<Self> {
        Ok(Self::new(Box::new(JjBackend::new(
            repo_path,
            workspace_base,
        )?)))
    }

    #[cfg(feature = "git")]
    pub fn new_git(
        repo_path: PathBuf,
        workspace_base: PathBuf,
        constraints: GitConstraints,
    ) -> Result<Self> {
        Ok(Self::new(Box::new(GitBackend::new(
            repo_path,
            workspace_base,
            constraints,
        )?)))
    }

    /// Detect VCS from filesystem and construct the appropriate backend.
    ///
    /// git via `auto_detect` sets `deny_ontology_writes: false` because git is
    /// detected as the **primary** VCS — ontology writes through git are the
    /// normal path. Construct `new_git` with explicit constraints to override.
    pub fn auto_detect(repo_path: PathBuf, workspace_base: PathBuf) -> Result<Self> {
        match detect_vcs(&repo_path) {
            #[cfg(feature = "jj")]
            VcsDetection::JjColocated | VcsDetection::Jj => Self::new_jj(repo_path, workspace_base),
            #[cfg(feature = "git")]
            VcsDetection::Git => Self::new_git(
                repo_path,
                workspace_base,
                GitConstraints {
                    deny_ontology_writes: false,
                    ..Default::default()
                },
            ),
            _ => Err(WorktreeError::VcsDetectionFailed(
                repo_path.display().to_string(),
            )),
        }
    }

    pub fn backend_name(&self) -> &'static str {
        self.backend.name()
    }

    pub fn capabilities(&self) -> VcsCapabilities {
        self.backend.capabilities()
    }

    /// Create a new workspace for `agent_id`. The workspace ID is a fresh UUID.
    pub async fn create_for_agent(&self, agent_id: &str) -> Result<WorktreeHandle> {
        let workspace_id = Uuid::new_v4().to_string();
        debug!("Creating workspace {workspace_id} for agent {agent_id}");

        let info = self
            .backend
            .create_workspace(agent_id, &workspace_id)
            .await?;
        let handle = WorktreeHandle::new(info);
        let id = handle.id().to_string();

        self.active.write().await.insert(id.clone(), handle.clone());
        info!("Tracking workspace {id} for agent {agent_id}");

        Ok(handle)
    }

    /// Preview merge readiness. See [`WorkspaceBackend::check_merge`] for
    /// per-backend semantics (destructive dry-run for git, read-only for jj).
    pub async fn check_merge(&self, handle: &WorktreeHandle) -> Result<MergeStatus> {
        handle.can_modify()?;
        self.backend.check_merge(handle.id(), "main").await
    }

    /// Merge the workspace into `main`, then remove the workspace.
    pub async fn merge_workspace(&self, handle: &WorktreeHandle) -> Result<()> {
        handle.can_modify()?;
        self.backend.merge_workspace(handle.id(), "main").await?;
        self.backend.remove_workspace(handle.id()).await?;
        self.retire(handle.id()).await;
        info!("Merged and removed workspace {}", handle.id());
        Ok(())
    }

    /// Force-remove workspace without merging (failed task cleanup).
    pub async fn remove_workspace(&self, handle: &WorktreeHandle) -> Result<()> {
        self.backend.remove_workspace(handle.id()).await?;
        self.retire(handle.id()).await;
        info!("Force-removed workspace {}", handle.id());
        Ok(())
    }

    pub async fn push_to_remote(&self, handle: &WorktreeHandle, remote: &str) -> Result<()> {
        handle.can_modify()?;
        self.backend
            .push_to_remote(handle.id(), handle.agent_id(), remote)
            .await
    }

    pub async fn get_worktree(&self, id: &str) -> Option<WorktreeHandle> {
        self.active.read().await.get(id).cloned()
    }

    pub async fn list_active(&self) -> Vec<WorktreeHandle> {
        self.active.read().await.values().cloned().collect()
    }

    pub async fn count_for_agent(&self, agent_id: &str) -> usize {
        self.active
            .read()
            .await
            .values()
            .filter(|h| h.agent_id() == agent_id)
            .count()
    }

    pub async fn cleanup_orphaned(&self) -> Result<()> {
        self.backend.cleanup_orphaned().await
    }

    async fn retire(&self, id: &str) {
        let mut active = self.active.write().await;
        if let Some(h) = active.get_mut(id) {
            h.deactivate();
        }
        active.remove(id);
    }
}

#[cfg(test)]
mod tests {
    use std::process::Command;

    use tempfile::TempDir;

    use super::*;
    use crate::error::WorktreeError;

    #[cfg(feature = "git")]
    fn init_git_repo(path: &std::path::Path) {
        Command::new("git")
            .current_dir(path)
            .args(["init"])
            .output()
            .unwrap();
        Command::new("git")
            .current_dir(path)
            .args(["config", "user.email", "test@test.com"])
            .output()
            .unwrap();
        Command::new("git")
            .current_dir(path)
            .args(["config", "user.name", "Test"])
            .output()
            .unwrap();
        std::fs::write(path.join("README"), "init").unwrap();
        Command::new("git")
            .current_dir(path)
            .args(["add", "."])
            .output()
            .unwrap();
        Command::new("git")
            .current_dir(path)
            .args(["commit", "-m", "init"])
            .output()
            .unwrap();
    }

    #[cfg(feature = "git")]
    #[tokio::test]
    async fn manager_creation_empty() -> Result<()> {
        let repo = TempDir::new().map_err(WorktreeError::IoError)?;
        let ws = TempDir::new().map_err(WorktreeError::IoError)?;
        init_git_repo(repo.path());

        let manager = WorktreeManager::new_git(
            repo.path().to_path_buf(),
            ws.path().to_path_buf(),
            GitConstraints::default(),
        )?;

        assert!(manager.list_active().await.is_empty());
        assert_eq!(manager.backend_name(), "git");
        Ok(())
    }

    #[cfg(feature = "git")]
    #[tokio::test]
    async fn workspace_creation_tracked() -> Result<()> {
        let repo = TempDir::new().map_err(WorktreeError::IoError)?;
        let ws = TempDir::new().map_err(WorktreeError::IoError)?;
        init_git_repo(repo.path());

        let manager = WorktreeManager::new_git(
            repo.path().to_path_buf(),
            ws.path().to_path_buf(),
            GitConstraints::default(),
        )?;

        let handle = manager.create_for_agent("agent-001").await?;

        assert_eq!(handle.agent_id(), "agent-001");
        assert!(handle.is_active);
        assert_eq!(manager.list_active().await.len(), 1);
        assert_eq!(manager.count_for_agent("agent-001").await, 1);
        assert_eq!(manager.count_for_agent("agent-002").await, 0);
        assert!(manager.get_worktree(handle.id()).await.is_some());

        Ok(())
    }

    #[cfg(feature = "git")]
    #[tokio::test]
    async fn auto_detect_picks_git() -> Result<()> {
        let repo = TempDir::new().map_err(WorktreeError::IoError)?;
        let ws = TempDir::new().map_err(WorktreeError::IoError)?;
        init_git_repo(repo.path());

        let manager =
            WorktreeManager::auto_detect(repo.path().to_path_buf(), ws.path().to_path_buf())?;

        assert_eq!(manager.backend_name(), "git");
        assert!(!manager.capabilities().conflicts_as_data);
        Ok(())
    }

    #[test]
    fn auto_detect_fails_on_no_vcs() {
        let dir = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        let result =
            WorktreeManager::auto_detect(dir.path().to_path_buf(), ws.path().to_path_buf());
        assert!(matches!(result, Err(WorktreeError::VcsDetectionFailed(_))));
    }
}
