use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{bail, Context, Result};
use serde::Serialize;
use sha2::{Digest, Sha256};

use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile::{self, ProfileName};
use crate::repo::marker::{self as repo_marker, RepoMarker};
use crate::repo::registry::{self as repo_registry, RepoAliases, RepoRegistryEntry};

#[derive(Serialize)]
pub struct LinkReport {
    command: &'static str,
    ok: bool,
    profile: String,
    locality_id: String,
    action: &'static str,
    resolution: &'static str,
    marker_path: String,
    registry_path: String,
    overlay_path: String,
    warnings: Vec<String>,
}

impl CommandReport for LinkReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        println!(
            "Linked profile {} to project ID {} ({}, {}).",
            self.profile, self.locality_id, self.action, self.resolution
        );
        println!("Marker: {}", self.marker_path);
        println!("Registry: {}", self.registry_path);
        println!("Project overlay: {}", self.overlay_path);

        for warning in &self.warnings {
            println!("Warning: {warning}");
        }
    }
}

pub fn run(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    explicit_locality_id: Option<&str>,
    display_name: Option<&str>,
) -> Result<LinkReport> {
    let request = LinkRequest {
        explicit_profile,
        explicit_locality_id,
        display_name,
    };
    let result = link_repo(repo_root, request)?;

    Ok(LinkReport {
        command: "link",
        ok: true,
        profile: result.profile.to_string(),
        locality_id: result.locality_id,
        action: result.action,
        resolution: result.resolution,
        marker_path: result.marker_path.display().to_string(),
        registry_path: result.registry_path.display().to_string(),
        overlay_path: result.overlay_path.display().to_string(),
        warnings: result.warnings,
    })
}

pub(crate) struct LinkRequest<'a> {
    pub(crate) explicit_profile: Option<&'a str>,
    pub(crate) explicit_locality_id: Option<&'a str>,
    pub(crate) display_name: Option<&'a str>,
}

pub(crate) struct LinkResult {
    pub(crate) profile: ProfileName,
    pub(crate) locality_id: String,
    pub(crate) action: &'static str,
    pub(crate) resolution: &'static str,
    pub(crate) marker_path: PathBuf,
    pub(crate) registry_path: PathBuf,
    pub(crate) overlay_path: PathBuf,
    pub(crate) warnings: Vec<String>,
}

pub(crate) struct PreparedLink {
    pub(crate) layout: StateLayout,
    pub(crate) profile: ProfileName,
    repo_root: PathBuf,
    marker: RepoMarker,
    registry_entry: RepoRegistryEntry,
    locality_id: String,
    previous_locality_id: Option<String>,
    current_clone_root: String,
    action: &'static str,
    resolution: &'static str,
    warnings: Vec<String>,
}

pub(crate) fn prepare_link(repo_root: &Path, request: LinkRequest<'_>) -> Result<PreparedLink> {
    let profile = profile::resolve(request.explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;

    let aliases = collect_aliases(repo_root)?;
    let existing_marker = repo_marker::load(repo_root)?;
    let resolution = resolve_locality_id(
        repo_root,
        &layout,
        request.explicit_locality_id,
        existing_marker.as_ref(),
        &aliases,
    )?;

    let chosen_display_name = choose_display_name(
        request.display_name,
        existing_marker
            .as_ref()
            .and_then(|marker| marker.display_name.as_deref()),
        repo_root.file_name().and_then(|name| name.to_str()),
    );
    let locality_id = resolution.locality_id.clone();
    let current_clone_root = repo_registry::normalize_clone_root(repo_root)?;
    let marker = match existing_marker.as_ref() {
        Some(existing_marker) => existing_marker.rewrite_with_locality_id(
            locality_id.clone(),
            chosen_display_name.as_ref().map(|value| value.to_owned()),
        )?,
        None => RepoMarker::new(
            locality_id.clone(),
            chosen_display_name.as_ref().map(|value| value.to_owned()),
        )?,
    };

    let registry_path = layout.repo_metadata_path(&locality_id)?;
    let existing_entry = repo_registry::load(&registry_path)?;
    let mut known_clone_roots = existing_entry
        .as_ref()
        .map(|entry| entry.known_clone_roots.clone())
        .unwrap_or_default();
    if !known_clone_roots
        .iter()
        .any(|known| known == &current_clone_root)
    {
        known_clone_roots.push(current_clone_root.clone());
        known_clone_roots.sort();
    }
    let registry_entry = RepoRegistryEntry::new_with_known_clone_roots(
        choose_display_name(
            request.display_name,
            existing_entry
                .as_ref()
                .and_then(|entry| entry.display_name.as_deref())
                .or(chosen_display_name.as_deref()),
            repo_root.file_name().and_then(|name| name.to_str()),
        )
        .map(|value| value.to_owned()),
        aliases,
        known_clone_roots,
    )?;

    Ok(PreparedLink {
        layout,
        profile,
        repo_root: repo_root.to_path_buf(),
        marker,
        registry_entry,
        locality_id,
        previous_locality_id: existing_marker
            .as_ref()
            .map(|marker| marker.locality_id.clone())
            .filter(|existing_locality_id| existing_locality_id != &resolution.locality_id),
        current_clone_root,
        action: resolution.action,
        resolution: resolution.resolution,
        warnings: resolution.warnings,
    })
}

pub(crate) fn finalize_link(prepared: PreparedLink) -> Result<LinkResult> {
    let PreparedLink {
        layout,
        profile,
        repo_root,
        marker,
        registry_entry,
        locality_id,
        previous_locality_id,
        current_clone_root,
        action,
        resolution,
        mut warnings,
    } = prepared;

    let marker_path = repo_marker::write(&repo_root, &marker)?;

    let registry_path = layout.repo_metadata_path(&locality_id)?;
    let registry_path = repo_registry::write(&registry_path, &registry_entry)?;

    if let Some(previous_locality_id) = previous_locality_id.as_deref() {
        if let Err(error) =
            remove_known_clone_root(&layout, previous_locality_id, &current_clone_root)
        {
            warnings.push(format!(
                "failed to prune clone root {current_clone_root} from previous locality_id `{previous_locality_id}`: {error:#}",
            ));
        }
    }

    let overlay_path = layout.repo_overlay_root(&locality_id)?;
    fs::create_dir_all(&overlay_path)
        .with_context(|| format!("failed to create directory {}", overlay_path.display()))?;

    Ok(LinkResult {
        profile,
        locality_id,
        action,
        resolution,
        marker_path,
        registry_path,
        overlay_path,
        warnings,
    })
}

pub(crate) fn link_repo(repo_root: &Path, request: LinkRequest<'_>) -> Result<LinkResult> {
    let prepared = prepare_link(repo_root, request)?;
    ensure_profile_exists(&prepared.layout)?;
    finalize_link(prepared)
}

struct RepoIdResolution {
    locality_id: String,
    action: &'static str,
    resolution: &'static str,
    warnings: Vec<String>,
}

fn resolve_locality_id(
    repo_root: &Path,
    layout: &StateLayout,
    explicit_locality_id: Option<&str>,
    existing_marker: Option<&RepoMarker>,
    aliases: &RepoAliases,
) -> Result<RepoIdResolution> {
    let alias_matches = find_alias_matches(layout, aliases)?;

    if let Some(locality_id) = explicit_locality_id {
        let mut warnings = Vec::new();
        if let Some(marker) = existing_marker {
            if marker.locality_id != locality_id {
                warnings.push(format!(
                    "relinked clone from locality_id `{}` to explicit locality_id `{locality_id}`",
                    marker.locality_id
                ));
            }
        }

        return Ok(RepoIdResolution {
            locality_id: locality_id.to_owned(),
            action: "attached",
            resolution: "explicit_locality_id",
            warnings,
        });
    }

    if let Some(marker) = existing_marker {
        let mut warnings = Vec::new();
        let conflicting_matches: Vec<String> = alias_matches
            .iter()
            .filter(|candidate| candidate.locality_id != marker.locality_id)
            .map(|candidate| candidate.locality_id.clone())
            .collect();
        if !conflicting_matches.is_empty() {
            warnings.push(format!(
                "local marker locality_id `{}` wins over alias matches: {}",
                marker.locality_id,
                conflicting_matches.join(", ")
            ));
        }

        return Ok(RepoIdResolution {
            locality_id: marker.locality_id.clone(),
            action: "refreshed",
            resolution: "local_marker",
            warnings,
        });
    }

    match alias_matches.as_slice() {
        [] => Ok(RepoIdResolution {
            locality_id: generate_locality_id(repo_root, layout)?,
            action: "created",
            resolution: "new_assignment",
            warnings: Vec::new(),
        }),
        [single] => Ok(RepoIdResolution {
            locality_id: single.locality_id.clone(),
            action: "attached",
            resolution: "alias_match",
            warnings: Vec::new(),
        }),
        many => bail!(
            "ambiguous repo link: multiple existing locality IDs match this clone's aliases: {}. Re-run with --project-id to attach explicitly",
            many.iter()
                .map(|candidate| candidate.locality_id.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ),
    }
}

pub(crate) fn choose_display_name(
    explicit: Option<&str>,
    existing: Option<&str>,
    fallback: Option<&str>,
) -> Option<String> {
    explicit
        .or(existing)
        .or(fallback)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn ensure_profile_exists(layout: &StateLayout) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it before linking repos",
        layout.profile(),
        profile_root.display()
    )
}

#[derive(Clone)]
struct AliasMatch {
    locality_id: String,
}

fn find_alias_matches(layout: &StateLayout, aliases: &RepoAliases) -> Result<Vec<AliasMatch>> {
    let registry_root = layout.repo_registry_root();
    if !registry_root.is_dir() {
        return Ok(Vec::new());
    }

    let mut matches = Vec::new();
    for entry in fs::read_dir(&registry_root)
        .with_context(|| format!("failed to read directory {}", registry_root.display()))?
    {
        let entry = entry?;
        if !entry.file_type()?.is_dir() {
            continue;
        }

        let locality_id = entry.file_name().to_string_lossy().to_string();
        let metadata_path = entry.path().join(repo_registry::REPO_METADATA_FILE);
        let Some(candidate) = repo_registry::load(&metadata_path)? else {
            continue;
        };

        if is_strong_match(aliases, &candidate.aliases) {
            matches.push(AliasMatch { locality_id });
        }
    }

    matches.sort_by(|left, right| left.locality_id.cmp(&right.locality_id));
    Ok(matches)
}

fn is_strong_match(current: &RepoAliases, candidate: &RepoAliases) -> bool {
    has_overlap(&current.remote_urls, &candidate.remote_urls)
        || has_overlap(&current.root_commits, &candidate.root_commits)
}

fn has_overlap(left: &[String], right: &[String]) -> bool {
    let (smaller, larger) = if left.len() <= right.len() {
        (left, right)
    } else {
        (right, left)
    };

    smaller
        .iter()
        .any(|value| larger.iter().any(|candidate| candidate == value))
}

pub(crate) fn find_alias_match_locality_ids(
    layout: &StateLayout,
    aliases: &RepoAliases,
) -> Result<Vec<String>> {
    find_alias_matches(layout, aliases).map(|matches| {
        matches
            .into_iter()
            .map(|candidate| candidate.locality_id)
            .collect()
    })
}

pub(crate) fn collect_aliases(repo_root: &Path) -> Result<RepoAliases> {
    let remote_urls = collect_remote_urls(repo_root);
    let root_commits = collect_root_commits(repo_root);
    let repo_basenames = repo_root
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| vec![name.to_owned()])
        .unwrap_or_default();

    Ok(RepoAliases {
        remote_urls,
        root_commits,
        repo_basenames,
    })
}

pub(crate) fn remove_known_clone_root(
    layout: &StateLayout,
    locality_id: &str,
    clone_root: &str,
) -> Result<()> {
    let registry_path = layout.repo_metadata_path(locality_id)?;
    let Some(mut entry) = repo_registry::load(&registry_path)? else {
        return Ok(());
    };

    if entry.remove_known_clone_root(clone_root) {
        repo_registry::write(&registry_path, &entry)?;
    }

    Ok(())
}

fn collect_remote_urls(repo_root: &Path) -> Vec<String> {
    let Ok(output) = git_output(repo_root, &["remote"]) else {
        return Vec::new();
    };

    dedup(
        output
            .lines()
            .map(str::trim)
            .filter(|remote| !remote.is_empty())
            .filter_map(|remote| git_output(repo_root, &["remote", "get-url", remote]).ok())
            .map(|url| url.trim().to_owned())
            .filter(|url| !url.is_empty())
            .collect(),
    )
}

fn collect_root_commits(repo_root: &Path) -> Vec<String> {
    let Ok(output) = git_output(repo_root, &["rev-list", "--max-parents=0", "HEAD"]) else {
        return Vec::new();
    };

    dedup(
        output
            .lines()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .collect(),
    )
}

fn dedup(values: Vec<String>) -> Vec<String> {
    let mut seen = std::collections::BTreeSet::new();
    let mut deduped = Vec::new();
    for value in values {
        if seen.insert(value.clone()) {
            deduped.push(value);
        }
    }
    deduped
}

fn git_output(repo_root: &Path, args: &[&str]) -> Result<String> {
    let output = Command::new("git")
        .args(args)
        .current_dir(repo_root)
        .output()
        .with_context(|| format!("failed to run `git {}`", args.join(" ")))?;

    if !output.status.success() {
        bail!(
            "`git {}` failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&output.stderr).trim()
        );
    }

    Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}

pub(crate) fn generate_locality_id(repo_root: &Path, layout: &StateLayout) -> Result<String> {
    for attempt in 0u32..1024 {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .context("system clock is before UNIX_EPOCH")?
            .as_nanos();
        let seed = format!(
            "{}:{}:{}:{}",
            repo_root.display(),
            layout.profile(),
            std::process::id(),
            now + u128::from(attempt)
        );
        let digest = Sha256::digest(seed.as_bytes());
        let locality_id = format!("ccdrepo_{digest:x}");
        let locality_id = locality_id[..32].to_owned();
        if !layout.repo_registry_dir(&locality_id)?.exists() {
            return Ok(locality_id);
        }
    }

    bail!("failed to generate a unique locality_id after 1024 attempts")
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn basename_only_matches_are_not_strong() {
        let current = RepoAliases {
            remote_urls: Vec::new(),
            root_commits: Vec::new(),
            repo_basenames: vec!["ccd-guide".to_owned()],
        };
        let candidate = RepoAliases {
            remote_urls: Vec::new(),
            root_commits: Vec::new(),
            repo_basenames: vec!["ccd-guide".to_owned()],
        };

        assert!(!is_strong_match(&current, &candidate));
    }

    #[test]
    fn shared_root_commit_is_a_strong_match() {
        let current = RepoAliases {
            remote_urls: Vec::new(),
            root_commits: vec!["abc123".to_owned()],
            repo_basenames: vec!["ccd-guide".to_owned()],
        };
        let candidate = RepoAliases {
            remote_urls: Vec::new(),
            root_commits: vec!["def456".to_owned(), "abc123".to_owned()],
            repo_basenames: vec!["other".to_owned()],
        };

        assert!(is_strong_match(&current, &candidate));
    }

    #[test]
    fn generate_project_id_creates_prefixed_identifier() {
        let temp = tempdir().expect("tempdir");
        let layout = StateLayout::new(
            temp.path().join(".ccd"),
            temp.path().join(".git/ccd"),
            ProfileName::new("main").expect("profile"),
        );

        let locality_id = generate_locality_id(temp.path(), &layout).expect("project id");

        assert!(locality_id.starts_with("ccdrepo_"));
        assert_eq!(locality_id.len(), 32);
    }
}
