use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};

use cargo_metadata::camino::Utf8PathBuf;
use cargo_metadata::{Dependency, Target};
use tracing::{debug, trace};

use crate::error::Error;
use crate::format::{format_list, make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target};
use crate::krate::Crate;
use crate::metadata::Metadata;
use crate::report::ReportItem;
use crate::upstream::Repository;
use crate::utils::file_mode;

use super::common::dependency_partial_eq;

pub struct RepoComparator<'a> {
    krate: &'a Crate,
    urepo: &'a Repository<'a>,
}

impl<'a> RepoComparator<'a> {
    pub const fn new(krate: &'a Crate, urepo: &'a Repository) -> Self {
        Self { krate, urepo }
    }

    fn path_in_krate(&self, path: &str) -> String {
        let name = self.krate.metadata.inner.name.as_str();
        let version = self.krate.metadata.inner.version.to_string();

        format!("{name}-{version}/{path}")
    }

    fn path_in_urepo(&self, path: &str) -> String {
        let shortref = &self.urepo.id[0..7];
        let path_in_vcs = &self.urepo.path_in_vcs;

        format!("git#{shortref}/{path_in_vcs}/{path}")
    }

    pub fn compare(&self) -> Result<Vec<ReportItem>, Error> {
        let mut items = Vec::new();

        let krate_cargo_toml = std::fs::read_to_string(self.krate.cargo_toml_orig())?;
        let urepo_cargo_toml = std::fs::read_to_string(self.urepo.cargo_toml())?;

        let krate_metadata = &self.krate.metadata;
        let urepo_metadata = &self.urepo.metadata;

        let krate_contents = self.krate.file_contents()?;
        let urepo_contents = self.urepo.file_contents()?;

        debug!("Comparing crate contents with repository contents");

        items.extend(compare_metadata(krate_metadata, urepo_metadata, &krate_contents.files));

        if krate_cargo_toml != urepo_cargo_toml {
            let kct_lf: Vec<_> = krate_cargo_toml.bytes().filter(|c| *c != b'\r').collect();
            let rct_lf: Vec<_> = urepo_cargo_toml.bytes().filter(|c| *c != b'\r').collect();

            if kct_lf == rct_lf {
                // file contents differ only because of line endings
                items.push(ReportItem::LineEndings {
                    path: String::from("Cargo.toml.orig"),
                });
            } else {
                let diff = Some(make_diff(
                    &krate_cargo_toml,
                    &urepo_cargo_toml,
                    Some((
                        &self.path_in_krate("Cargo.toml.orig"),
                        &self.path_in_urepo("Cargo.toml"),
                    )),
                ));
                items.push(ReportItem::ContentMismatch {
                    path: String::from("Cargo.toml.orig"),
                    diff,
                });
            }
        }

        for file in &krate_contents.broken_links {
            items.push(ReportItem::BrokenSymlinkInCrate {
                path: file.to_string_lossy().to_string(),
            });
        }
        for file in &urepo_contents.broken_links {
            items.push(ReportItem::BrokenSymlinkInRepo {
                path: file.to_string_lossy().to_string(),
            });
        }
        for file in &krate_contents.outside_base {
            items.push(ReportItem::BrokenSymlinkInCrate {
                path: file.to_string_lossy().to_string(),
            });
        }
        for file in &urepo_contents.outside_base {
            items.push(ReportItem::BrokenSymlinkInRepo {
                path: file.to_string_lossy().to_string(),
            });
        }

        for file in &krate_contents.files {
            if !urepo_contents.files.contains(file) {
                items.push(ReportItem::MissingFile {
                    path: file.to_string_lossy().to_string(),
                });
                continue;
            }

            trace!("Comparing files at path: {}", file.to_string_lossy());
            items.extend(self.compare_contents(&file, &file)?);
            items.extend(self.compare_modes(&file, &file)?);
        }

        Ok(items)
    }

    fn compare_contents<P: AsRef<Path>>(&self, krate_path: P, urepo_path: P) -> Result<Vec<ReportItem>, Error> {
        let mut items = Vec::new();

        let krate_file_content = self.krate.read_entry_to_bytes(&krate_path)?;
        let urepo_file_content = self.urepo.read_entry_to_bytes(&urepo_path)?;

        if krate_file_content != urepo_file_content {
            let kfc_lf: Vec<_> = krate_file_content.iter().filter(|c| **c != b'\r').collect();
            let rfc_lf: Vec<_> = urepo_file_content.iter().filter(|c| **c != b'\r').collect();

            if kfc_lf == rfc_lf {
                // file contents differ only because of line endings
                items.push(ReportItem::LineEndings {
                    path: krate_path.as_ref().to_string_lossy().to_string(),
                });
            } else {
                let diff = if let (Ok(ktc), Ok(utc)) = (
                    self.krate.read_entry_to_string(&krate_path),
                    self.urepo.read_entry_to_string(&urepo_path),
                ) {
                    Some(make_diff(
                        &ktc,
                        &utc,
                        Some((
                            &self.path_in_krate(&krate_path.as_ref().to_string_lossy()),
                            &self.path_in_urepo(&urepo_path.as_ref().to_string_lossy()),
                        )),
                    ))
                } else {
                    // file is probably not a text file
                    None
                };

                items.push(ReportItem::ContentMismatch {
                    path: krate_path.as_ref().to_string_lossy().to_string(),
                    diff,
                });
            }
        }

        Ok(items)
    }

    fn compare_modes<P: AsRef<Path>>(&self, krate_path: P, urepo_path: P) -> Result<Vec<ReportItem>, Error> {
        let mut items = Vec::new();

        let krate_file_mode = file_mode(self.krate.root.join(&krate_path))?;
        let urepo_file_mode = file_mode(self.urepo.root.join(&self.urepo.path_in_vcs).join(&urepo_path))?;

        if krate_file_mode != urepo_file_mode {
            items.push(ReportItem::Permissions {
                path: krate_path.as_ref().to_string_lossy().to_string(),
                krate: krate_file_mode,
                urepo: urepo_file_mode,
            });
        }

        Ok(items)
    }
}

fn compare_metadata(krate_metadata: &Metadata, urepo_metadata: &Metadata, krate_files: &[PathBuf]) -> Vec<ReportItem> {
    let mut items = Vec::new();

    let kmd = &krate_metadata.inner;
    let umd = &urepo_metadata.inner;

    items.extend(compare_displayable("package.name", &kmd.name, &umd.name));
    items.extend(compare_displayable("package.version", &kmd.version, &umd.version));
    items.extend(compare_displayable_list("package.authors", &kmd.authors, &umd.authors));

    // package.id: not stable
    // package.source: always None

    items.extend(compare_displayable_option(
        "package.description",
        kmd.description.as_ref(),
        umd.description.as_ref(),
    ));
    items.extend(compare_dependencies(&kmd.dependencies, &umd.dependencies));
    items.extend(compare_displayable_option(
        "package.license",
        kmd.license.as_ref(),
        umd.license.as_ref(),
    ));

    items.extend(compare_path_option(
        "package.license-file",
        kmd.license_file.as_ref(),
        umd.license_file.as_ref(),
    ));

    items.extend(compare_targets(&kmd.targets, &umd.targets, krate_files));
    items.extend(compare_features(&kmd.features, &umd.features));

    // manifest_path: only present in "cargo metadata" output, not in Cargo.toml

    items.extend(compare_displayable_list(
        "package.categories",
        &kmd.categories,
        &umd.categories,
    ));
    items.extend(compare_displayable_list(
        "package.keywords",
        &kmd.keywords,
        &umd.keywords,
    ));

    items.extend(compare_path_option(
        "package.readme",
        kmd.readme.as_ref(),
        umd.readme.as_ref(),
    ));

    items.extend(compare_displayable_option(
        "package.repository",
        kmd.repository.as_ref(),
        umd.repository.as_ref(),
    ));
    items.extend(compare_displayable_option(
        "package.homepage",
        kmd.homepage.as_ref(),
        umd.homepage.as_ref(),
    ));
    items.extend(compare_displayable_option(
        "package.documentation",
        kmd.documentation.as_ref(),
        umd.documentation.as_ref(),
    ));

    items.extend(compare_displayable("package.edition", &kmd.edition, &umd.edition));
    items.extend(compare_displayable("package.metadata", &kmd.metadata, &umd.metadata));

    items.extend(compare_displayable_option(
        "package.links",
        kmd.links.as_ref(),
        umd.links.as_ref(),
    ));

    items.extend(compare_displayable_list_option(
        "package.publish",
        kmd.publish.as_deref(),
        umd.publish.as_deref(),
    ));

    items.extend(compare_displayable_option(
        "package.default-run",
        kmd.default_run.as_ref(),
        umd.default_run.as_ref(),
    ));
    items.extend(compare_displayable_option(
        "package.rust-version",
        kmd.rust_version.as_ref(),
        umd.rust_version.as_ref(),
    ));

    items
}

fn compare_dependencies(kdeps: &[Dependency], udeps: &[Dependency]) -> Vec<ReportItem> {
    if kdeps.len() == udeps.len() && kdeps.iter().zip(udeps).all(|(k, u)| dependency_partial_eq(k, u)) {
        return Vec::new();
    }

    let mut items = Vec::new();

    for kdep in kdeps {
        if udeps.iter().any(|udep| dependency_partial_eq(kdep, udep)) {
            continue;
        }

        let path = path_from_dep(kdep);
        items.push(ReportItem::metadata_mismatch(path, Some(value_from_dep(kdep)), None));
    }

    for udep in udeps {
        if kdeps.iter().any(|kdep| dependency_partial_eq(kdep, udep)) {
            continue;
        }

        if let Some(source) = &udep.source
            && !source.is_crates_io()
        {
            // non-crates.io sources (like path- or git-based dependencies) are stripped during publishing
            continue;
        }

        let path = path_from_dep(udep);
        items.push(ReportItem::metadata_mismatch(path, None, Some(value_from_dep(udep))));
    }

    items
}

fn compare_targets(ktargets: &[Target], utargets: &[Target], krate_files: &[PathBuf]) -> Vec<ReportItem> {
    if ktargets == utargets {
        return Vec::new();
    }

    let mut items = Vec::new();

    for ktarget in ktargets {
        if utargets.contains(ktarget) {
            continue;
        }

        let path = path_from_target(ktarget);
        items.push(ReportItem::metadata_mismatch(
            path,
            Some(value_from_target(ktarget)),
            None,
        ));
    }

    for utarget in utargets {
        if ktargets.contains(utarget) {
            continue;
        }

        if !krate_files.contains(&PathBuf::from(&utarget.src_path)) {
            // targets for paths that are not included when publishing are stripped
            continue;
        }

        let path = path_from_target(utarget);
        items.push(ReportItem::metadata_mismatch(
            path,
            None,
            Some(value_from_target(utarget)),
        ));
    }

    items
}

type Features = BTreeMap<String, Vec<String>>;
fn compare_features(kfeatures: &Features, ufeatures: &Features) -> Vec<ReportItem> {
    if kfeatures == ufeatures {
        return Vec::new();
    }

    let mut items = Vec::new();

    for (kkey, kvalues) in kfeatures {
        match ufeatures.get(kkey) {
            Some(uvalues) => {
                if kvalues == uvalues {
                    continue;
                }
                items.push(ReportItem::metadata_mismatch(
                    format!("features.{kkey}"),
                    Some(format_list(kvalues)),
                    Some(format_list(uvalues)),
                ));
            },
            None => {
                items.push(ReportItem::metadata_mismatch(
                    format!("features.{kkey}"),
                    Some(format_list(kvalues)),
                    None,
                ));
            },
        }
    }

    for (ukey, uvalues) in ufeatures {
        match kfeatures.get(ukey) {
            Some(kvalues) => {
                if kvalues == uvalues {
                    continue;
                }
                items.push(ReportItem::metadata_mismatch(
                    format!("features.{ukey}"),
                    Some(format_list(kvalues)),
                    Some(format_list(uvalues)),
                ));
            },
            None => {
                items.push(ReportItem::metadata_mismatch(
                    format!("features.{ukey}"),
                    None,
                    Some(format_list(uvalues)),
                ));
            },
        }
    }

    items
}

fn compare_displayable<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
    field: S,
    kitem: &D,
    uitem: &D,
) -> Option<ReportItem> {
    if kitem == uitem {
        None
    } else {
        Some(ReportItem::metadata_mismatch(
            field,
            Some(kitem.to_string()),
            Some(uitem.to_string()),
        ))
    }
}

fn compare_path_option<S: Into<Cow<'static, str>>>(
    field: S,
    kitem: Option<&Utf8PathBuf>,
    uitem: Option<&Utf8PathBuf>,
) -> Option<ReportItem> {
    if kitem == uitem {
        None
    } else {
        Some(ReportItem::metadata_mismatch(
            field,
            kitem.map(ToString::to_string),
            uitem.map(ToString::to_string),
        ))
    }
}

fn compare_displayable_option<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
    field: S,
    kitem: Option<&D>,
    uitem: Option<&D>,
) -> Option<ReportItem> {
    if kitem == uitem {
        None
    } else {
        Some(ReportItem::metadata_mismatch(
            field,
            kitem.map(ToString::to_string),
            uitem.map(ToString::to_string),
        ))
    }
}

fn compare_displayable_list<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
    field: S,
    kitems: &[D],
    uitems: &[D],
) -> Option<ReportItem> {
    if kitems == uitems {
        None
    } else {
        Some(ReportItem::metadata_mismatch(
            field,
            Some(format_list(kitems)),
            Some(format_list(uitems)),
        ))
    }
}

fn compare_displayable_list_option<S: Into<Cow<'static, str>>, D: Display + PartialEq>(
    field: S,
    kitems: Option<&[D]>,
    uitems: Option<&[D]>,
) -> Option<ReportItem> {
    if kitems == uitems {
        None
    } else {
        Some(ReportItem::metadata_mismatch(
            field,
            kitems.map(format_list),
            uitems.map(format_list),
        ))
    }
}
