use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use async_trait::async_trait;
use chrono::Utc;
use tracing::{info, warn};

use crate::backend::{
    ConflictInfo, MergeStatus, VcsCapabilities, VcsMetadata, WorkspaceBackend, WorkspaceInfo,
};
use crate::error::{Result, WorktreeError};

/// Constraints for the git backend.
pub struct GitConstraints {
    /// Maximum workspaces that may exist simultaneously under `workspace_base`.
    pub max_concurrent: usize,
    pub merge_timeout: Duration,
    pub max_inactive: Duration,
    pub require_pre_check: bool,
    /// Reject any merge that touches `.ontology/` files.
    ///
    /// Default `false` — set `true` only when using git as a **degraded
    /// fallback** in a project that normally runs jj, to prevent ontology
    /// state drift through a non-jj code path.
    pub deny_ontology_writes: bool,
}

impl Default for GitConstraints {
    fn default() -> Self {
        Self {
            max_concurrent: 3,
            merge_timeout: Duration::from_secs(30),
            max_inactive: Duration::from_secs(3600),
            require_pre_check: true,
            deny_ontology_writes: false,
        }
    }
}

pub struct GitBackend {
    repo_path: PathBuf,
    workspace_base: PathBuf,
    constraints: GitConstraints,
}

impl GitBackend {
    pub fn new(
        repo_path: PathBuf,
        workspace_base: PathBuf,
        constraints: GitConstraints,
    ) -> Result<Self> {
        if !repo_path.exists() {
            return Err(WorktreeError::InvalidState(format!(
                "Repository path does not exist: {}",
                repo_path.display()
            )));
        }
        if !workspace_base.exists() {
            std::fs::create_dir_all(&workspace_base)?;
        }
        Ok(Self {
            repo_path,
            workspace_base,
            constraints,
        })
    }

    fn branch_name(agent_id: &str, workspace_id: &str) -> String {
        format!("agent/{agent_id}/{workspace_id}")
    }

    fn workspace_path(&self, workspace_id: &str) -> PathBuf {
        self.workspace_base.join(workspace_id)
    }

    /// Read the HEAD branch of a git worktree at `path`.
    fn head_branch(path: &Path) -> Result<String> {
        let out = Command::new("git")
            .current_dir(path)
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .output()
            .map_err(|e| WorktreeError::GitError(e.to_string()))?;
        if !out.status.success() {
            return Err(WorktreeError::NotFound(path.display().to_string()));
        }
        Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
    }

    /// Files under `.ontology/` that differ between `target` and `branch` in
    /// the repo.
    fn ontology_diff(&self, branch: &str, target: &str) -> Result<Vec<PathBuf>> {
        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["diff", "--name-only", target, branch])
            .output()
            .map_err(|e| WorktreeError::GitError(format!("diff failed: {e}")))?;
        let stdout = String::from_utf8_lossy(&out.stdout);
        Ok(stdout
            .lines()
            .filter(|f| f.starts_with(".ontology/"))
            .map(PathBuf::from)
            .collect())
    }

    /// Error if `deny_ontology_writes` is set and `.ontology/` is modified.
    fn enforce_ontology_guard(&self, branch: &str, target: &str) -> Result<()> {
        if !self.constraints.deny_ontology_writes {
            return Ok(());
        }
        let modified = self.ontology_diff(branch, target)?;
        if !modified.is_empty() {
            return Err(WorktreeError::OntologyWriteDenied(
                modified
                    .iter()
                    .map(|p| p.to_string_lossy().into_owned())
                    .collect(),
            ));
        }
        Ok(())
    }

    fn count_workspace_dirs(&self) -> Result<usize> {
        Ok(std::fs::read_dir(&self.workspace_base)?
            .flatten()
            .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
            .count())
    }

    fn head_sha(&self) -> String {
        Command::new("git")
            .current_dir(&self.repo_path)
            .args(["rev-parse", "HEAD"])
            .output()
            .ok()
            .and_then(|o| {
                if o.status.success() {
                    Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
                } else {
                    None
                }
            })
            .unwrap_or_default()
    }
}

#[async_trait]
impl WorkspaceBackend for GitBackend {
    fn name(&self) -> &'static str {
        "git"
    }

    fn capabilities(&self) -> VcsCapabilities {
        VcsCapabilities {
            conflicts_as_data: false,
            stable_change_ids: false,
            native_workspaces: false,
            hooks_supported: true,
        }
    }

    async fn create_workspace(&self, agent_id: &str, workspace_id: &str) -> Result<WorkspaceInfo> {
        let active = self.count_workspace_dirs()?;
        if active >= self.constraints.max_concurrent {
            return Err(WorktreeError::MaxConcurrentExceeded {
                limit: self.constraints.max_concurrent,
            });
        }

        let branch = Self::branch_name(agent_id, workspace_id);
        let path = self.workspace_path(workspace_id);
        let path_str = path
            .to_str()
            .ok_or_else(|| WorktreeError::InvalidState("Non-UTF8 workspace path".into()))?;

        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["worktree", "add", "-b", &branch, path_str])
            .output()
            .map_err(|e| WorktreeError::GitError(format!("worktree add: {e}")))?;

        if !out.status.success() {
            return Err(WorktreeError::CreationFailed(
                String::from_utf8_lossy(&out.stderr).into_owned(),
            ));
        }

        let commit_sha = self.head_sha();
        info!("Created git workspace {workspace_id} for agent {agent_id} on branch {branch}");

        Ok(WorkspaceInfo {
            id: workspace_id.to_string(),
            agent_id: agent_id.to_string(),
            path,
            vcs: VcsMetadata::Git { branch, commit_sha },
            created_at: Utc::now(),
        })
    }

    /// Destructive dry-run: `git merge --no-commit` then `git merge --abort`.
    ///
    /// The working tree is temporarily mutated. If the process dies between
    /// the two steps, the repo will be in merge state and require manual abort.
    async fn check_merge(&self, workspace_id: &str, target: &str) -> Result<MergeStatus> {
        let ws_path = self.workspace_path(workspace_id);
        let branch = Self::head_branch(&ws_path)?;
        let ontology_files = self.ontology_diff(&branch, target)?;

        self.enforce_ontology_guard(&branch, target)?;

        // Best-effort fetch to ensure target is up to date
        let _ = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["fetch", "origin", &format!("{target}:{target}")])
            .output();

        let merge_out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["merge", "--no-commit", "--no-ff", "--no-stat", &branch])
            .output()
            .map_err(|e| WorktreeError::GitError(format!("merge dry-run: {e}")))?;

        let has_conflict = !merge_out.status.success();

        // Always abort — even on success (we only wanted the preview)
        let abort_out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["merge", "--abort"])
            .output()
            .map_err(|e| WorktreeError::GitError(format!("merge abort: {e}")))?;

        if !abort_out.status.success() {
            warn!(
                "merge --abort returned non-zero for workspace {workspace_id}: {}",
                String::from_utf8_lossy(&abort_out.stderr)
            );
        }

        let conflicts = if has_conflict {
            vec![ConflictInfo {
                path: PathBuf::from(&branch),
                description: String::from_utf8_lossy(&merge_out.stderr).into_owned(),
            }]
        } else {
            vec![]
        };

        Ok(MergeStatus {
            can_merge: !has_conflict,
            conflicts,
            ontology_files_modified: ontology_files,
        })
    }

    async fn merge_workspace(&self, workspace_id: &str, target: &str) -> Result<()> {
        let ws_path = self.workspace_path(workspace_id);
        let branch = Self::head_branch(&ws_path)?;

        self.enforce_ontology_guard(&branch, target)?;

        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["merge", "--no-edit", &branch])
            .output()
            .map_err(|e| WorktreeError::GitError(format!("merge: {e}")))?;

        if !out.status.success() {
            return Err(WorktreeError::MergeConflict(
                String::from_utf8_lossy(&out.stderr).into_owned(),
            ));
        }

        info!("Merged git workspace {workspace_id} into {target}");
        Ok(())
    }

    async fn remove_workspace(&self, workspace_id: &str) -> Result<()> {
        let path = self.workspace_path(workspace_id);

        // Best-effort branch lookup before we remove the worktree
        let branch = Self::head_branch(&path).unwrap_or_default();

        let path_str = path.to_str().unwrap_or_default();
        let rm_out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["worktree", "remove", "-f", path_str])
            .output()
            .map_err(|e| WorktreeError::RemovalFailed(e.to_string()))?;

        if !rm_out.status.success() {
            warn!(
                "worktree remove -f had issues for {workspace_id}: {}",
                String::from_utf8_lossy(&rm_out.stderr)
            );
        }

        // Remove branch — best effort, workspace may be on a detached HEAD
        if !branch.is_empty() {
            let _ = Command::new("git")
                .current_dir(&self.repo_path)
                .args(["branch", "-D", &branch])
                .output();
        }

        info!("Removed git workspace {workspace_id}");
        Ok(())
    }

    async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>> {
        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["worktree", "list", "--porcelain"])
            .output()
            .map_err(|e| WorktreeError::GitError(e.to_string()))?;

        if !out.status.success() {
            return Err(WorktreeError::GitError(
                String::from_utf8_lossy(&out.stderr).into_owned(),
            ));
        }

        let stdout = String::from_utf8_lossy(&out.stdout);
        let mut workspaces = Vec::new();

        // Porcelain format: blocks separated by blank lines
        // Each block: "worktree <path>\nHEAD <sha>\nbranch refs/heads/<name>"
        for block in stdout.split("\n\n") {
            let mut wt_path: Option<String> = None;
            let mut branch: Option<String> = None;
            let mut sha: Option<String> = None;

            for line in block.lines() {
                if let Some(p) = line.strip_prefix("worktree ") {
                    wt_path = Some(p.to_string());
                } else if let Some(b) = line.strip_prefix("branch refs/heads/") {
                    branch = Some(b.to_string());
                } else if let Some(s) = line.strip_prefix("HEAD ") {
                    sha = Some(s.to_string());
                }
            }

            let (Some(p), Some(b), Some(s)) = (wt_path, branch, sha) else {
                continue;
            };

            let path = PathBuf::from(&p);
            if !path.starts_with(&self.workspace_base) {
                continue;
            }

            let workspace_id = path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or_default()
                .to_string();

            // agent_id is embedded in branch: agent/<agent_id>/<workspace_id>
            let agent_id = b
                .strip_prefix("agent/")
                .and_then(|rest| rest.split('/').next())
                .unwrap_or("unknown")
                .to_string();

            workspaces.push(WorkspaceInfo {
                id: workspace_id,
                agent_id,
                path,
                vcs: VcsMetadata::Git {
                    branch: b,
                    commit_sha: s,
                },
                created_at: Utc::now(),
            });
        }

        Ok(workspaces)
    }

    async fn push_to_remote(
        &self,
        workspace_id: &str,
        _agent_id: &str,
        remote: &str,
    ) -> Result<()> {
        let ws_path = self.workspace_path(workspace_id);
        let branch = Self::head_branch(&ws_path)?;

        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["push", remote, &branch])
            .output()
            .map_err(|e| WorktreeError::GitError(e.to_string()))?;

        if !out.status.success() {
            return Err(WorktreeError::GitError(
                String::from_utf8_lossy(&out.stderr).into_owned(),
            ));
        }
        Ok(())
    }

    async fn cleanup_orphaned(&self) -> Result<()> {
        let out = Command::new("git")
            .current_dir(&self.repo_path)
            .args(["worktree", "list"])
            .output()
            .map_err(|e| WorktreeError::GitError(e.to_string()))?;

        let stdout = String::from_utf8_lossy(&out.stdout);
        for line in stdout.lines() {
            if line.is_empty() {
                continue;
            }
            if let Some(path_str) = line.split_whitespace().next() {
                let path = PathBuf::from(path_str);
                if path.starts_with(&self.workspace_base) {
                    warn!("Removing orphaned git worktree: {}", path.display());
                    let _ = Command::new("git")
                        .current_dir(&self.repo_path)
                        .args(["worktree", "remove", "-f", path_str])
                        .output();
                }
            }
        }
        Ok(())
    }
}

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

    use tempfile::TempDir;

    use super::*;

    fn init_git_repo(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();
    }

    fn backend(repo: &Path, ws: &Path) -> GitBackend {
        GitBackend::new(
            repo.to_path_buf(),
            ws.to_path_buf(),
            GitConstraints::default(),
        )
        .unwrap()
    }

    #[test]
    fn capabilities_correct() {
        let repo = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        init_git_repo(repo.path());
        let caps = backend(repo.path(), ws.path()).capabilities();
        assert!(!caps.conflicts_as_data);
        assert!(!caps.stable_change_ids);
        assert!(!caps.native_workspaces);
        assert!(caps.hooks_supported);
    }

    #[tokio::test]
    async fn create_and_remove() {
        let repo = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        init_git_repo(repo.path());
        let b = backend(repo.path(), ws.path());

        let info = b.create_workspace("agent-001", "ws-0001").await.unwrap();
        assert_eq!(info.id, "ws-0001");
        assert_eq!(info.agent_id, "agent-001");
        assert!(info.path.exists());
        assert!(matches!(info.vcs, VcsMetadata::Git { .. }));

        b.remove_workspace("ws-0001").await.unwrap();
        assert!(!info.path.exists());
    }

    #[tokio::test]
    async fn max_concurrent_enforced() {
        let repo = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        init_git_repo(repo.path());
        let b = GitBackend::new(
            repo.path().to_path_buf(),
            ws.path().to_path_buf(),
            GitConstraints {
                max_concurrent: 2,
                ..Default::default()
            },
        )
        .unwrap();

        b.create_workspace("agent-001", "ws-0001").await.unwrap();
        b.create_workspace("agent-001", "ws-0002").await.unwrap();
        let err = b
            .create_workspace("agent-001", "ws-0003")
            .await
            .unwrap_err();
        assert!(matches!(
            err,
            WorktreeError::MaxConcurrentExceeded { limit: 2 }
        ));
    }

    #[tokio::test]
    async fn deny_ontology_writes_blocks_check_merge() {
        let repo = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        init_git_repo(repo.path());
        let b = GitBackend::new(
            repo.path().to_path_buf(),
            ws.path().to_path_buf(),
            GitConstraints {
                deny_ontology_writes: true,
                ..Default::default()
            },
        )
        .unwrap();

        let info = b.create_workspace("agent-001", "ws-0001").await.unwrap();

        // Commit a .ontology/ file in the workspace
        let onto_dir = info.path.join(".ontology");
        std::fs::create_dir_all(&onto_dir).unwrap();
        std::fs::write(onto_dir.join("core.ncl"), "let x = 1").unwrap();
        Command::new("git")
            .current_dir(&info.path)
            .args(["add", "."])
            .output()
            .unwrap();
        Command::new("git")
            .current_dir(&info.path)
            .args(["commit", "-m", "add ontology"])
            .output()
            .unwrap();

        let err = b.check_merge("ws-0001", "main").await.unwrap_err();
        assert!(matches!(err, WorktreeError::OntologyWriteDenied(_)));
    }

    #[tokio::test]
    async fn ontology_files_populated_when_not_denied() {
        let repo = TempDir::new().unwrap();
        let ws = TempDir::new().unwrap();
        init_git_repo(repo.path());
        let b = backend(repo.path(), ws.path()); // deny_ontology_writes: false

        let info = b.create_workspace("agent-001", "ws-0001").await.unwrap();
        let onto_dir = info.path.join(".ontology");
        std::fs::create_dir_all(&onto_dir).unwrap();
        std::fs::write(onto_dir.join("state.ncl"), "let s = true").unwrap();
        Command::new("git")
            .current_dir(&info.path)
            .args(["add", "."])
            .output()
            .unwrap();
        Command::new("git")
            .current_dir(&info.path)
            .args(["commit", "-m", "state"])
            .output()
            .unwrap();

        let status = b.check_merge("ws-0001", "main").await.unwrap();
        assert!(
            status
                .ontology_files_modified
                .iter()
                .any(|p| p.ends_with("state.ncl")),
            "expected state.ncl in {:?}",
            status.ontology_files_modified
        );
    }
}
