use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::path::Path;

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

use crate::diff::DiffItem;
use crate::error::Error;
use crate::format::{make_diff, path_from_dep, path_from_target, value_from_dep, value_from_target};
use crate::krate::Crate;
use crate::metadata::Metadata;
use crate::utils::file_mode;

use super::common::dependency_partial_eq;
use super::version_req::compare_version_req;

pub struct CrateComparator<'a> {
    old: &'a Crate,
    new: &'a Crate,
}

impl<'a> CrateComparator<'a> {
    pub const fn new(old: &'a Crate, new: &'a Crate) -> Self {
        Self { old, new }
    }

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

        let old_metadata = &self.old.metadata;
        let new_metadata = &self.new.metadata;

        let old_contents = self.old.file_contents()?;
        let new_contents = self.new.file_contents()?;

        debug!("Comparing crate contents");

        items.extend(compare_metadata(old_metadata, new_metadata));

        for file in &new_contents.files {
            if !old_contents.files.contains(file) {
                items.push(DiffItem::FileAdded {
                    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)?);
        }

        for file in &old_contents.files {
            if !new_contents.files.contains(file) {
                items.push(DiffItem::FileRemoved {
                    path: file.to_string_lossy().to_string(),
                });
            }
        }

        Ok(items)
    }

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

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

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

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

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

        let old_file_content = self.old.read_entry_to_bytes(&old_path)?;
        let new_file_content = self.new.read_entry_to_bytes(&new_path)?;

        if old_file_content != new_file_content {
            let old_fc_lf: Vec<_> = old_file_content.iter().filter(|c| **c != b'\r').collect();
            let new_fc_lf: Vec<_> = new_file_content.iter().filter(|c| **c != b'\r').collect();

            if old_fc_lf == new_fc_lf {
                // file contents differ only because of line endings
                items.push(DiffItem::LineEndingsChange {
                    path: old_path.as_ref().to_string_lossy().to_string(),
                });
            } else {
                let diff = if let (Ok(old_file_utf8), Ok(new_file_utf8)) = (
                    self.old.read_entry_to_string(&old_path),
                    self.new.read_entry_to_string(&new_path),
                ) {
                    Some(make_diff(
                        &old_file_utf8,
                        &new_file_utf8,
                        Some((
                            &self.path_in_crate_old(&old_path.as_ref().to_string_lossy()),
                            &self.path_in_crate_new(&new_path.as_ref().to_string_lossy()),
                        )),
                    ))
                } else {
                    // file is probably not a text file
                    None
                };

                items.push(DiffItem::FileChanged {
                    path: old_path.as_ref().to_string_lossy().to_string(),
                    diff,
                });
            }
        }

        Ok(items)
    }

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

        let old_file_mode = file_mode(self.old.root.join(&old_path))?;
        let new_file_mode = file_mode(self.new.root.join(&new_path))?;

        if old_file_mode != new_file_mode {
            items.push(DiffItem::PermissionChange {
                path: old_path.as_ref().to_string_lossy().to_string(),
                old: old_file_mode,
                new: new_file_mode,
            });
        }

        Ok(items)
    }
}

#[expect(clippy::too_many_lines)]
fn compare_metadata(old_metadata: &Metadata, new_metadata: &Metadata) -> Vec<DiffItem> {
    let mut items = Vec::new();

    let old_md = &old_metadata.inner;
    let new_md = &new_metadata.inner;

    // package.id: not stable
    // package.source: always None
    // package.manifest_path: only present in "cargo metadata" output, not in Cargo.toml
    // package.publish: not relevant for crates from crates.io

    if old_md.name != new_md.name {
        // this should never happen?
        items.push(DiffItem::NameChange {
            old: old_md.name.to_string(),
            new: new_md.name.to_string(),
        });
    }
    if old_md.version != new_md.version {
        items.push(DiffItem::VersionChange {
            old: old_md.version.to_string(),
            new: new_md.version.to_string(),
        });
    }
    if old_md.edition != new_md.edition {
        items.push(DiffItem::EditionChange {
            old: old_md.edition.to_string(),
            new: new_md.edition.to_string(),
        });
    }
    if old_md.rust_version != new_md.rust_version {
        items.push(DiffItem::RustVersionChange {
            old: old_md.rust_version.as_ref().map(ToString::to_string),
            new: new_md.rust_version.as_ref().map(ToString::to_string),
        });
    }
    if old_md.authors != new_md.authors {
        let (added, removed) = compare_str_list(&old_md.authors, &new_md.authors);
        items.push(DiffItem::AuthorsChange { added, removed });
    }
    if old_md.description != new_md.description {
        items.push(DiffItem::DescriptionChange {
            old: old_md.description.clone(),
            new: new_md.description.clone(),
        });
    }
    if old_md.license != new_md.license {
        items.push(DiffItem::LicenseChange {
            old: old_md.license.clone(),
            new: new_md.license.clone(),
        });
    }
    if old_md.license_file != new_md.license_file {
        items.push(DiffItem::LicenseFileChange {
            old: old_md.license_file.as_ref().map(ToString::to_string),
            new: new_md.license_file.as_ref().map(ToString::to_string),
        });
    }
    if old_md.readme != new_md.readme {
        items.push(DiffItem::ReadmeChange {
            old: old_md.readme.as_ref().map(ToString::to_string),
            new: new_md.readme.as_ref().map(ToString::to_string),
        });
    }
    if old_md.categories != new_md.categories {
        let (added, removed) = compare_str_list(&old_md.categories, &new_md.categories);
        items.push(DiffItem::CategoriesChange { added, removed });
    }
    if old_md.keywords != new_md.keywords {
        let (added, removed) = compare_str_list(&old_md.keywords, &new_md.keywords);
        items.push(DiffItem::KeywordsChange { added, removed });
    }
    if old_md.repository != new_md.repository {
        items.push(DiffItem::RepositoryChange {
            old: old_md.repository.clone(),
            new: new_md.repository.clone(),
        });
    }
    if old_md.homepage != new_md.homepage {
        items.push(DiffItem::HomepageChange {
            old: old_md.homepage.clone(),
            new: new_md.homepage.clone(),
        });
    }
    if old_md.documentation != new_md.documentation {
        items.push(DiffItem::DocumentationChange {
            old: old_md.documentation.clone(),
            new: new_md.documentation.clone(),
        });
    }
    if old_md.links != new_md.links {
        items.push(DiffItem::LinksChange {
            old: old_md.links.clone(),
            new: new_md.links.clone(),
        });
    }
    if old_md.default_run != new_md.default_run {
        items.push(DiffItem::DefaultRunChange {
            old: old_md.default_run.clone(),
            new: new_md.default_run.clone(),
        });
    }
    if old_md.metadata != new_md.metadata {
        items.push(DiffItem::MetadataChange {
            old: old_md.metadata.clone(),
            new: new_md.metadata.clone(),
        });
    }

    items.extend(compare_dependencies(&old_md.dependencies, &new_md.dependencies));
    items.extend(compare_targets(&old_md.targets, &new_md.targets));
    items.extend(compare_features(&old_md.features, &new_md.features));

    items
}

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

    let mut items = Vec::new();

    // path_from_dep is specific to dependency kind and target
    let old_deps_map: BTreeMap<String, &Dependency> = old_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();
    let new_deps_map: BTreeMap<String, &Dependency> = new_deps.iter().map(|dep| (path_from_dep(dep), dep)).collect();

    for (path, old_dep) in &old_deps_map {
        if let Some(new_dep) = new_deps_map.get(path) {
            let (lower_bound, upper_bound) = compare_version_req(&old_dep.req, &new_dep.req);

            if lower_bound == Ordering::Greater || upper_bound == Ordering::Greater {
                items.push(DiffItem::DependencyUpgraded {
                    path: path_from_dep(new_dep),
                    old: old_dep.req.to_string(),
                    new: new_dep.req.to_string(),
                });
            }
            if lower_bound == Ordering::Less || upper_bound == Ordering::Less {
                items.push(DiffItem::DependencyDowngraded {
                    path: path_from_dep(new_dep),
                    old: old_dep.req.to_string(),
                    new: new_dep.req.to_string(),
                });
            }

            let (added_features, removed_features) = compare_str_list(&old_dep.features, &new_dep.features);
            if !added_features.is_empty() || !removed_features.is_empty() {
                items.push(DiffItem::DependencyFeatures {
                    path: path_from_dep(new_dep),
                    added: added_features,
                    removed: removed_features,
                });
            }

            if old_dep.optional != new_dep.optional {
                items.push(DiffItem::DependencyOptionality {
                    path: path_from_dep(new_dep),
                    old: old_dep.optional,
                    new: new_dep.optional,
                });
            }
        } else {
            items.push(DiffItem::DependencyRemoved {
                path: path_from_dep(old_dep),
                value: value_from_dep(old_dep),
            });
        }
    }

    for (path, new_dep) in &new_deps_map {
        if !old_deps_map.contains_key(path) {
            items.push(DiffItem::DependencyAdded {
                path: path_from_dep(new_dep),
                value: value_from_dep(new_dep),
            });
        }
    }

    items
}

fn compare_targets(old_targets: &[Target], new_targets: &[Target]) -> Vec<DiffItem> {
    if old_targets == new_targets {
        return Vec::new();
    }

    let mut items = Vec::new();

    let old_target_map: BTreeMap<String, &Target> = old_targets
        .iter()
        .map(|target| (path_from_target(target), target))
        .collect();
    let new_target_map: BTreeMap<String, &Target> = new_targets
        .iter()
        .map(|target| (path_from_target(target), target))
        .collect();

    for (path, old_target) in &old_target_map {
        if let Some(new_target) = new_target_map.get(path) {
            if old_target != new_target {
                items.push(DiffItem::TargetChanged {
                    path: path.clone(),
                    old: value_from_target(old_target),
                    new: value_from_target(new_target),
                });
            }
        } else {
            items.push(DiffItem::TargetRemoved {
                path: path.clone(),
                target: value_from_target(old_target),
            });
        }
    }

    for (path, new_target) in &new_target_map {
        if !old_target_map.contains_key(path) {
            items.push(DiffItem::TargetAdded {
                path: path.clone(),
                target: value_from_target(new_target),
            });
        }
    }

    items
}

type Features = BTreeMap<String, Vec<String>>;
fn compare_features(old_features: &Features, new_features: &Features) -> Vec<DiffItem> {
    if old_features == new_features {
        return Vec::new();
    }

    let mut items = Vec::new();

    for (old_key, old_values) in old_features {
        match new_features.get(old_key) {
            Some(new_values) => {
                if old_values == new_values {
                    continue;
                }
                let (added, removed) = compare_str_list(old_values, new_values);
                items.push(DiffItem::FeatureChanged {
                    name: String::from(old_key),
                    added,
                    removed,
                });
            },
            None => {
                items.push(DiffItem::FeatureRemoved {
                    name: String::from(old_key),
                });
            },
        }
    }

    for new_key in new_features.keys() {
        if old_features.get(new_key).is_none() {
            items.push(DiffItem::FeatureAdded {
                name: String::from(new_key),
            });
        }
    }

    items
}

fn compare_str_list(old: &[String], new: &[String]) -> (Vec<String>, Vec<String>) {
    let mut added = Vec::new();
    let mut removed = Vec::new();

    for new_str in new {
        if !old.contains(new_str) {
            added.push(new_str.clone());
        }
    }

    for old_str in old {
        if !new.contains(old_str) {
            removed.push(old_str.clone());
        }
    }

    (added, removed)
}
