use std::collections::HashMap;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};

use super::super::config::{RoleType, TeamConfig};
use super::super::hierarchy::MemberInstance;
use super::{CheckLevel, CheckLine, DoctorDaemonState, LaunchIdentityRecord};

pub(super) fn check_line(level: CheckLevel, message: impl Into<String>) -> CheckLine {
    CheckLine {
        level,
        message: message.into(),
    }
}

pub(super) fn resolve_task_worktree(project_root: &Path, worktree_path: &str) -> PathBuf {
    let path = PathBuf::from(worktree_path);
    if path.is_absolute() {
        path
    } else {
        project_root.join(path)
    }
}

pub(super) fn is_task_branch(branch: &str) -> bool {
    branch.starts_with("eng-")
        && branch
            .split_once('/')
            .is_some_and(|(_, suffix)| suffix.starts_with("task-") || suffix.parse::<u32>().is_ok())
}

pub(super) fn is_engineer_name(name: &str) -> bool {
    name.starts_with("eng-")
}

pub(super) fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
    print!("{msg}");
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Ok(default_yes);
    }
    Ok(trimmed.starts_with('y') || trimmed.starts_with('Y'))
}

pub(super) fn current_prompt(member: &MemberInstance, config_dir: &Path) -> String {
    let prompt_file = member.prompt.as_deref().unwrap_or(match member.role_type {
        RoleType::Architect => "architect.md",
        RoleType::Manager => "manager.md",
        RoleType::Engineer => "engineer.md",
        RoleType::User => "architect.md",
    });

    let path = config_dir.join(prompt_file);
    let content = fs::read_to_string(&path).unwrap_or_else(|_| {
        format!(
            "You are {} (role: {:?}). Work on assigned tasks.",
            member.name, member.role_type
        )
    });

    strip_nudge_section(
        &content
            .replace("{{member_name}}", &member.name)
            .replace("{{role_name}}", &member.role_name)
            .replace(
                "{{reports_to}}",
                member.reports_to.as_deref().unwrap_or("none"),
            ),
    )
}

pub(super) fn strip_nudge_section(prompt: &str) -> String {
    let mut lines = Vec::new();
    let mut in_nudge = false;

    for line in prompt.lines() {
        if line.starts_with("## Nudge") {
            in_nudge = true;
            continue;
        }
        if in_nudge && line.starts_with("## ") {
            in_nudge = false;
        }
        if !in_nudge {
            lines.push(line);
        }
    }

    lines.join("\n").trim_end().to_string()
}

pub(super) fn short_prompt_hash(prompt: &str) -> String {
    let digest = Sha256::digest(prompt.as_bytes());
    format!(
        "{:02x}{:02x}{:02x}{:02x}",
        digest[0], digest[1], digest[2], digest[3]
    )
}

pub(super) fn canonical_agent_name(agent_name: &str) -> String {
    match agent_name {
        "claude" | "claude-code" => "claude-code".to_string(),
        "codex" | "codex-cli" => "codex-cli".to_string(),
        "kiro" | "kiro-cli" => "kiro-cli".to_string(),
        _ => agent_name.to_string(),
    }
}

pub(super) fn git_output(path: &Path, args: &[&str]) -> Option<String> {
    let output = Command::new("git")
        .args(args)
        .current_dir(path)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
    (!text.is_empty()).then_some(text)
}

pub(super) fn load_team_config(project_root: &Path) -> Result<Option<TeamConfig>> {
    let path = super::super::team_config_path(project_root);
    if !path.exists() {
        return Ok(None);
    }
    Ok(Some(TeamConfig::load(&path)?))
}

pub(super) fn load_launch_state(
    path: &Path,
) -> Result<Option<HashMap<String, LaunchIdentityRecord>>> {
    load_json_file(path)
}

pub(super) fn load_daemon_state(path: &Path) -> Result<Option<DoctorDaemonState>> {
    load_json_file(path)
}

pub(super) fn load_json_file<T: DeserializeOwned>(path: &Path) -> Result<Option<T>> {
    if !path.exists() {
        return Ok(None);
    }
    let content =
        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
    let parsed = serde_json::from_str(&content)
        .with_context(|| format!("failed to parse {}", path.display()))?;
    Ok(Some(parsed))
}

pub(super) fn launch_state_path(project_root: &Path) -> PathBuf {
    project_root.join(".batty").join("launch-state.json")
}

pub(super) fn file_size(path: &Path) -> Option<u64> {
    fs::metadata(path).ok().map(|metadata| metadata.len())
}

pub(super) fn default_claude_projects_root() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/"))
        .join(".claude")
        .join("projects")
}

pub(super) fn claude_session_id_exists(session_id: &str) -> bool {
    let session_file = format!("{session_id}.jsonl");
    let Ok(entries) = fs::read_dir(default_claude_projects_root()) else {
        return false;
    };

    entries.flatten().any(|entry| {
        let path = entry.path();
        path.is_dir() && path.join(&session_file).exists()
    })
}

pub(super) fn display_cleanup_path(project_root: &Path, path: &Path) -> String {
    path.strip_prefix(project_root)
        .map(|relative| relative.display().to_string())
        .unwrap_or_else(|_| path.display().to_string())
}

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

    #[test]
    fn load_json_file_returns_none_for_missing_path() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("missing.json");

        let parsed: Option<DoctorDaemonState> = load_json_file(&path).unwrap();

        assert_eq!(parsed, None);
    }

    #[test]
    fn load_json_file_reports_invalid_json() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("broken.json");
        fs::write(&path, "{not json").unwrap();

        let error = load_json_file::<DoctorDaemonState>(&path).unwrap_err();

        assert!(error.to_string().contains("failed to parse"));
    }

    #[test]
    fn resolve_task_worktree_handles_relative_and_absolute_paths() {
        let tmp = tempfile::tempdir().unwrap();
        let relative = resolve_task_worktree(tmp.path(), ".batty/worktrees/eng-1");
        let absolute = resolve_task_worktree(tmp.path(), "/tmp/eng-1");

        assert_eq!(
            relative,
            tmp.path().join(".batty").join("worktrees").join("eng-1")
        );
        assert_eq!(absolute, PathBuf::from("/tmp/eng-1"));
    }

    #[test]
    fn branch_and_engineer_name_helpers_match_expected_patterns() {
        assert!(is_task_branch("eng-1/12"));
        assert!(is_task_branch("eng-1/task-12"));
        assert!(!is_task_branch("main"));
        assert!(!is_task_branch("eng-1/feature"));
        assert!(is_engineer_name("eng-1"));
        assert!(!is_engineer_name("manager"));
    }

    #[test]
    fn display_cleanup_path_prefers_relative_path() {
        let tmp = tempfile::tempdir().unwrap();
        let nested = tmp.path().join(".batty").join("worktrees").join("eng-1");

        let display = display_cleanup_path(tmp.path(), &nested);

        assert_eq!(display, ".batty/worktrees/eng-1");
    }
}
