use super::util::current_dir;
use koala_wiki::{
    gen, refresh_open_prs, DraftSummary, GitOps, PrCommand, RefreshError, REFRESH_LABEL,
};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};

pub fn gen_cmd(root: Option<PathBuf>) -> ExitCode {
    let root = root.unwrap_or_else(current_dir);
    match gen(&root) {
        Ok(report) => {
            println!("wiki gen: wrote {} file(s):", report.written.len());
            for p in &report.written {
                println!("  {}", p.display());
            }
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::from(1)
        }
    }
}

/// `koala-core wiki refresh-pr` — walk `.wiki-draft/` and for each draft
/// branch off main, copy the draft into `wiki/`, commit, push, and open a
/// `gh pr create --label wiki-refresh` PR. `--dry-run` plans without
/// touching git or gh.
pub fn refresh_pr_cmd(root: Option<PathBuf>, dry_run: bool) -> ExitCode {
    let root = root.unwrap_or_else(current_dir);

    let date_tag = std::env::var("KOALA_TODAY").ok().unwrap_or_else(today_tag);

    let mut git: Box<dyn GitOps> = if dry_run {
        Box::new(DryRunGit::default())
    } else {
        Box::new(RealGit::new(root.clone()))
    };
    let mut pr: Box<dyn PrCommand> = if dry_run {
        Box::new(DryRunGh)
    } else {
        Box::new(GhBin)
    };

    match refresh_open_prs(&root, &date_tag, git.as_mut(), pr.as_mut()) {
        Ok(opened) if opened.is_empty() => {
            println!("no drafts in .wiki-draft/ — nothing to open.");
            ExitCode::SUCCESS
        }
        Ok(opened) => {
            println!("opened {} PR(s):", opened.len());
            for p in &opened {
                println!(
                    "  {} → branch {} → {}",
                    p.draft_path.display(),
                    p.branch,
                    p.url
                );
            }
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::from(1)
        }
    }
}

/// `YYYYMMDD` from the system clock. Tests / CI override via `KOALA_TODAY`.
fn today_tag() -> String {
    let bin = std::env::var("KOALA_DATE_BIN").unwrap_or_else(|_| "date".into());
    let out = Command::new(&bin).args(["-u", "+%Y%m%d"]).output();
    match out {
        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
        _ => "00000000".to_string(),
    }
}

struct RealGit {
    repo_root: PathBuf,
}

impl RealGit {
    fn new(repo_root: PathBuf) -> Self {
        Self { repo_root }
    }

    fn run(&self, args: &[&str]) -> Result<String, RefreshError> {
        let bin = std::env::var("KOALA_GIT_BIN").unwrap_or_else(|_| "git".into());
        let out = Command::new(&bin)
            .arg("-C")
            .arg(&self.repo_root)
            .args(args)
            .output()
            .map_err(|e| RefreshError::Git(format!("spawn `{bin}`: {e}")))?;
        if !out.status.success() {
            return Err(RefreshError::Git(format!(
                "git {} exit {}: {}",
                args.join(" "),
                out.status,
                String::from_utf8_lossy(&out.stderr).trim()
            )));
        }
        Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
    }
}

impl GitOps for RealGit {
    fn require_clean_on_main(&mut self) -> Result<(), RefreshError> {
        let branch = self.run(&["rev-parse", "--abbrev-ref", "HEAD"])?;
        if branch != "main" {
            return Err(RefreshError::Preflight(format!(
                "must be on `main` to refresh-pr (currently on `{branch}`)"
            )));
        }
        let status = self.run(&["status", "--porcelain"])?;
        // Allow only `.wiki-draft/` entries to be present — those are the
        // drafts we're about to publish, not user wip.
        for line in status.lines() {
            let path = line.get(3..).unwrap_or("");
            if !path.starts_with(".wiki-draft/") && !path.is_empty() {
                return Err(RefreshError::Preflight(format!(
                    "working tree is dirty (untracked / modified outside `.wiki-draft/`): {path}"
                )));
            }
        }
        Ok(())
    }

    fn create_branch_from_main(&mut self, name: &str) -> Result<(), RefreshError> {
        self.run(&["checkout", "-b", name, "main"])?;
        Ok(())
    }

    fn add(&mut self, rel_path: &Path) -> Result<(), RefreshError> {
        self.run(&["add", &rel_path.to_string_lossy()])?;
        Ok(())
    }

    fn commit(&mut self, message: &str) -> Result<(), RefreshError> {
        self.run(&["commit", "-m", message])?;
        Ok(())
    }

    fn push_current(&mut self) -> Result<(), RefreshError> {
        self.run(&["push", "-u", "origin", "HEAD"])?;
        Ok(())
    }

    fn checkout_main(&mut self) -> Result<(), RefreshError> {
        self.run(&["checkout", "main"])?;
        Ok(())
    }
}

#[derive(Default)]
struct DryRunGit {
    branches: Vec<String>,
}

impl GitOps for DryRunGit {
    fn require_clean_on_main(&mut self) -> Result<(), RefreshError> {
        println!("(dry-run preflight: skipping git status / branch check)");
        Ok(())
    }
    fn create_branch_from_main(&mut self, name: &str) -> Result<(), RefreshError> {
        println!("(dry-run: would `git checkout -b {name} main`)");
        self.branches.push(name.to_string());
        Ok(())
    }
    fn add(&mut self, rel: &Path) -> Result<(), RefreshError> {
        println!("(dry-run: would `git add {}`)", rel.display());
        Ok(())
    }
    fn commit(&mut self, message: &str) -> Result<(), RefreshError> {
        println!("(dry-run: would `git commit -m \"{message}\"`)");
        Ok(())
    }
    fn push_current(&mut self) -> Result<(), RefreshError> {
        println!("(dry-run: would `git push -u origin HEAD`)");
        Ok(())
    }
    fn checkout_main(&mut self) -> Result<(), RefreshError> {
        println!("(dry-run: would `git checkout main`)");
        Ok(())
    }
}

struct GhBin;

impl PrCommand for GhBin {
    fn open_pr(&mut self, summary: &DraftSummary, branch: &str) -> Result<String, RefreshError> {
        let bin = std::env::var("KOALA_GH_BIN").unwrap_or_else(|_| "gh".into());
        let out = Command::new(&bin)
            .args(["pr", "create", "--label", REFRESH_LABEL, "--head"])
            .arg(branch)
            .args(["--base", "main", "--title"])
            .arg(&summary.title)
            .args(["--body"])
            .arg(&summary.body)
            .output()
            .map_err(|e| RefreshError::GhSpawn(format!("spawn `{bin}`: {e}")))?;
        if !out.status.success() {
            return Err(RefreshError::GhSpawn(format!(
                "gh exit {}: {}",
                out.status,
                String::from_utf8_lossy(&out.stderr).trim()
            )));
        }
        Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
    }
}

struct DryRunGh;

impl PrCommand for DryRunGh {
    fn open_pr(&mut self, summary: &DraftSummary, branch: &str) -> Result<String, RefreshError> {
        Ok(format!(
            "(dry-run: would `gh pr create --label {REFRESH_LABEL} --head {branch} --base main --title {:?}` for {})",
            summary.title,
            summary.draft_path.display()
        ))
    }
}
