use std::collections::HashSet;
use std::path::Path;
use std::process::Command;

use chrono::{DateTime, Local, NaiveDate, TimeZone, Utc};
use serde::Serialize;

use crate::engine::config::load_config;
use crate::engine::git::{
    current_branch, delete_remote_branch, get_default_branch, is_squash_merged,
};
use crate::engine::naming::wave_name_from_branch;
use crate::ops::error::{OpsError, OpsResult};
use crate::ops::progress::Progress;

#[derive(Debug, Clone)]
pub struct BranchListOptions {
    pub filters: BranchFilterOptions,
    pub default_user_if_empty: bool,
}

#[derive(Debug, Clone)]
pub struct BranchPruneOptions {
    pub filters: BranchFilterOptions,
    pub dry_run: bool,
    pub yes: bool,
}

#[derive(Debug, Clone, Default)]
pub struct BranchFilterOptions {
    pub user: Option<String>,
    pub wave: Option<String>,
    pub stale: Option<String>,
    pub created_before: Option<String>,
    pub merged: bool,
    pub include_open_prs: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BranchCandidate {
    pub branch: String,
    pub author: String,
    pub last_commit_date: String,
    pub age_days: i64,
    pub wave: Option<String>,
    pub merged: bool,
    pub open_pr: bool,
    pub protected: bool,
    pub protect_reason: Option<String>,
}

#[derive(Debug, Clone)]
struct BranchFilters {
    user: Option<String>,
    wave: Option<String>,
    stale_days: Option<i64>,
    created_before: Option<NaiveDate>,
    merged: bool,
    include_open_prs: bool,
}

#[derive(Debug, Clone)]
struct RemoteBranchRef {
    branch: String,
    author: String,
    committer_timestamp: i64,
}

pub fn list_branch_candidates(
    repo: &Path,
    options: &BranchListOptions,
) -> OpsResult<Vec<BranchCandidate>> {
    let filters =
        BranchFilters::from_options(repo, &options.filters, options.default_user_if_empty)?;
    collect_branch_candidates(repo, &filters)
}

pub fn prune_branches(
    repo: &Path,
    options: &BranchPruneOptions,
    progress: &impl Progress,
) -> OpsResult<Vec<BranchCandidate>> {
    if !options.has_filter() {
        return Err(OpsError::Message(
            "refusing to prune remote branches without at least one filter".to_string(),
        ));
    }

    let filters = BranchFilters::from_options(repo, &options.filters, false)?;
    let candidates = collect_branch_candidates(repo, &filters)?;
    let targets = candidates
        .into_iter()
        .filter(|branch| !branch.protected)
        .collect::<Vec<_>>();

    if targets.is_empty() {
        progress.status("No deletable remote branches match.");
        return Ok(Vec::new());
    }

    if options.dry_run {
        return Ok(targets);
    }

    let summary = targets
        .iter()
        .map(|candidate| format!("  {}", candidate.branch))
        .collect::<Vec<_>>()
        .join("\n");
    if !options.yes && !progress.confirm(&format!("Delete these remote branches?\n{summary}")) {
        return Err(OpsError::Message("aborted".to_string()));
    }

    for candidate in &targets {
        progress.status(&format!("Deleting origin/{}...", candidate.branch));
        delete_remote_branch(repo, "origin", &candidate.branch)?;
    }

    Ok(targets)
}

impl BranchPruneOptions {
    fn has_filter(&self) -> bool {
        self.filters.has_filter()
    }
}

impl BranchFilterOptions {
    fn has_filter(&self) -> bool {
        self.user.is_some()
            || self.wave.is_some()
            || self.stale.is_some()
            || self.created_before.is_some()
            || self.merged
    }
}

impl BranchFilters {
    fn from_options(
        repo: &Path,
        options: &BranchFilterOptions,
        default_user_if_empty: bool,
    ) -> OpsResult<Self> {
        let user = if !options.has_filter() && default_user_if_empty {
            Some(resolve_user(repo, "@me")?)
        } else {
            options
                .user
                .as_deref()
                .map(|user| resolve_user(repo, user))
                .transpose()?
        };
        Self::new(
            user,
            options.wave.clone(),
            options.stale.as_deref(),
            options.created_before.as_deref(),
            options.merged,
            options.include_open_prs,
        )
    }

    fn new(
        user: Option<String>,
        wave: Option<String>,
        stale: Option<&str>,
        created_before: Option<&str>,
        merged: bool,
        include_open_prs: bool,
    ) -> OpsResult<Self> {
        Ok(Self {
            user,
            wave,
            stale_days: stale.map(parse_duration_days).transpose()?,
            created_before: created_before.map(parse_date).transpose()?,
            merged,
            include_open_prs,
        })
    }
}

fn collect_branch_candidates(
    repo: &Path,
    filters: &BranchFilters,
) -> OpsResult<Vec<BranchCandidate>> {
    let default_branch = get_default_branch(repo)?;
    let _ = fetch_origin_prune(repo);
    let refs = remote_branch_refs(repo)?;
    let open_prs = open_pr_branches(repo);
    let current = current_branch(repo)?.unwrap_or_default();
    let branch_config = load_config(Some(repo))
        .ok()
        .flatten()
        .and_then(|config| config.branch_names);

    let mut candidates = Vec::new();
    for item in refs {
        if item.branch == "HEAD"
            || item.branch == default_branch
            || item.branch == "main"
            || item.branch == "master"
        {
            continue;
        }

        let parsed_wave = wave_name_from_branch(&item.branch, branch_config.as_ref());
        if let Some(user) = filters.user.as_deref() {
            if !matches_user(&item.author, user) {
                continue;
            }
        }
        if let Some(wave) = filters.wave.as_deref() {
            if !branch_matches_wave(&item.branch, parsed_wave.as_deref(), wave) {
                continue;
            }
        }
        if let Some(stale_days) = filters.stale_days {
            if age_days(item.committer_timestamp) < stale_days {
                continue;
            }
        }
        if let Some(cutoff) = filters.created_before {
            let created =
                first_unique_commit_date(repo, &item.branch)?.unwrap_or(item.committer_timestamp);
            let created_date = Utc
                .timestamp_opt(created, 0)
                .single()
                .ok_or_else(|| OpsError::Parse(format!("invalid commit timestamp: {created}")))?
                .date_naive();
            if created_date >= cutoff {
                continue;
            }
        }

        let merged = branch_is_merged(repo, &item.branch, &default_branch);
        if filters.merged && !merged {
            continue;
        }

        let open_pr = open_prs.contains(&item.branch);
        let (protected, protect_reason) =
            protected_reason(&item.branch, &current, open_pr, filters.include_open_prs);
        candidates.push(BranchCandidate {
            branch: item.branch,
            author: item.author,
            last_commit_date: format_date(item.committer_timestamp)?,
            age_days: age_days(item.committer_timestamp),
            wave: parsed_wave,
            merged,
            open_pr,
            protected,
            protect_reason,
        });
    }

    candidates.sort_by(|a, b| b.age_days.cmp(&a.age_days).then(a.branch.cmp(&b.branch)));
    Ok(candidates)
}

fn fetch_origin_prune(repo: &Path) -> OpsResult<()> {
    let output = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["fetch", "origin", "--prune"])
        .output()?;
    if output.status.success() {
        Ok(())
    } else {
        Err(OpsError::CommandFailed {
            command: "git fetch origin --prune".to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        })
    }
}

fn remote_branch_refs(repo: &Path) -> OpsResult<Vec<RemoteBranchRef>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args([
            "for-each-ref",
            "refs/remotes/origin/",
            "--format=%(refname:short)%00%(authorname)%00%(committerdate:unix)",
        ])
        .output()?;
    if !output.status.success() {
        return Err(OpsError::CommandFailed {
            command: "git for-each-ref refs/remotes/origin/".to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        });
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut refs = Vec::new();
    for line in stdout.lines() {
        let mut parts = line.split('\0');
        let Some(ref_name) = parts.next() else {
            continue;
        };
        let Some(author) = parts.next() else { continue };
        let Some(timestamp) = parts.next() else {
            continue;
        };
        let Some(branch) = ref_name.strip_prefix("origin/") else {
            continue;
        };
        let committer_timestamp = timestamp
            .parse::<i64>()
            .map_err(|_| OpsError::Parse(format!("invalid commit timestamp: {timestamp}")))?;
        refs.push(RemoteBranchRef {
            branch: branch.to_string(),
            author: author.to_string(),
            committer_timestamp,
        });
    }
    Ok(refs)
}

fn open_pr_branches(repo: &Path) -> HashSet<String> {
    let output = Command::new("gh")
        .current_dir(repo)
        .args([
            "pr",
            "list",
            "--state",
            "open",
            "--json",
            "headRefName",
            "--limit",
            "1000",
            "--jq",
            ".[] | .headRefName",
        ])
        .output();

    match output {
        Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
            .lines()
            .filter(|line| !line.trim().is_empty())
            .map(|line| line.trim().to_string())
            .collect(),
        _ => HashSet::new(),
    }
}

fn branch_is_merged(repo: &Path, branch: &str, default_branch: &str) -> bool {
    let remote_branch = format!("origin/{branch}");
    let target = format!("origin/{default_branch}");
    let ancestor = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["merge-base", "--is-ancestor", &remote_branch, &target])
        .status()
        .map(|status| status.success())
        .unwrap_or(false);
    ancestor || is_squash_merged(repo, &remote_branch, &target).unwrap_or(false)
}

fn first_unique_commit_date(repo: &Path, branch: &str) -> OpsResult<Option<i64>> {
    let default_branch = get_default_branch(repo)?;
    let range = format!("origin/{default_branch}..origin/{branch}");
    let output = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["log", "--reverse", "--format=%ct", &range])
        .output()?;
    if !output.status.success() {
        return Ok(None);
    }
    let first = String::from_utf8_lossy(&output.stdout)
        .lines()
        .find(|line| !line.trim().is_empty())
        .map(str::to_string);
    first
        .map(|value| {
            value
                .parse::<i64>()
                .map_err(|_| OpsError::Parse(format!("invalid commit timestamp: {value}")))
        })
        .transpose()
}

fn protected_reason(
    branch: &str,
    current: &str,
    open_pr: bool,
    include_open_prs: bool,
) -> (bool, Option<String>) {
    let reason = if branch == "main" || branch == "master" {
        Some("base branch".to_string())
    } else if !current.is_empty() && branch == current {
        Some("current branch".to_string())
    } else if open_pr && !include_open_prs {
        Some("open PR".to_string())
    } else {
        None
    };
    (reason.is_some(), reason)
}

fn parse_duration_days(value: &str) -> OpsResult<i64> {
    let trimmed = value.trim();
    let split = trimmed
        .find(|ch: char| !ch.is_ascii_digit())
        .unwrap_or(trimmed.len());
    let (digits, unit) = trimmed.split_at(split);
    if digits.is_empty() {
        return Err(OpsError::Parse(format!("invalid duration: {value}")));
    }
    let amount = digits
        .parse::<i64>()
        .map_err(|_| OpsError::Parse(format!("invalid duration: {value}")))?;
    let multiplier = match unit.trim() {
        "" | "d" | "day" | "days" => 1,
        "w" | "week" | "weeks" => 7,
        "m" | "mo" | "month" | "months" => 30,
        _ => return Err(OpsError::Parse(format!("invalid duration unit: {value}"))),
    };
    Ok(amount * multiplier)
}

fn parse_date(value: &str) -> OpsResult<NaiveDate> {
    NaiveDate::parse_from_str(value, "%Y-%m-%d")
        .map_err(|_| OpsError::Parse(format!("invalid date: {value}")))
}

fn age_days(timestamp: i64) -> i64 {
    let now = Utc::now().timestamp();
    ((now - timestamp).max(0)) / 86_400
}

fn format_date(timestamp: i64) -> OpsResult<String> {
    let utc = Utc
        .timestamp_opt(timestamp, 0)
        .single()
        .ok_or_else(|| OpsError::Parse(format!("invalid commit timestamp: {timestamp}")))?;
    let local: DateTime<Local> = DateTime::from(utc);
    Ok(local.format("%Y-%m-%d").to_string())
}

fn resolve_user(repo: &Path, user: &str) -> OpsResult<String> {
    if user != "@me" {
        return Ok(user.to_string());
    }

    let output = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(["config", "user.name"])
        .output()?;
    if output.status.success() {
        let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
        if !value.is_empty() {
            return Ok(value);
        }
    }

    Ok(std::env::var("USER").unwrap_or_else(|_| "user".to_string()))
}

fn matches_user(author: &str, user: &str) -> bool {
    author.eq_ignore_ascii_case(user)
}

fn branch_matches_wave(branch: &str, parsed_wave: Option<&str>, wave: &str) -> bool {
    if parsed_wave.is_some_and(|name| name == wave) {
        return true;
    }
    branch.split(['.', '/', '_', '-']).any(|part| part == wave)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_duration_accepts_days_weeks_months() {
        assert_eq!(parse_duration_days("30d").unwrap(), 30);
        assert_eq!(parse_duration_days("2w").unwrap(), 14);
        assert_eq!(parse_duration_days("1month").unwrap(), 30);
    }

    #[test]
    fn branch_wave_match_uses_schema_or_segments() {
        assert!(branch_matches_wave(
            "jack.redesign.20260401_1200",
            Some("redesign"),
            "redesign"
        ));
        assert!(branch_matches_wave(
            "feature/redesign-cleanup",
            None,
            "redesign"
        ));
        assert!(!branch_matches_wave("feature/predesign", None, "redesign"));
    }
}
