use std::{
    fs,
    io::Write,
    path::{Path, PathBuf},
};

use toml::{Value, value::Table};

use crate::contract::sc_config::ContractVariantProfile;

use super::DependencyRawValue;

pub const CARGO_TOML_DEPENDENCIES: &str = "dependencies";
pub const CARGO_TOML_DEV_DEPENDENCIES: &str = "dev-dependencies";
pub const PACKAGE: &str = "package";
pub const AUTHORS: &str = "authors";
const DEFAULT_WORKSPACE_RESOLVER: &str = "3";
const AUTO_GENERATED: &str = "# Code generated by the multiversx-sc build system. DO NOT EDIT.

# ##########################################
# ############## AUTO-GENERATED #############
# ##########################################

";

/// Contains an in-memory representation of a Cargo.toml file.
///
/// Implementation notes:
///
/// - Currently contains a raw toml tree, but in principle it could also work with a cargo_toml::Manifest.
/// - It keeps an ordered representation, thanks to the `toml` `preserve_order` feature.
#[derive(Clone, Debug, Default)]
pub struct CargoTomlContents {
    pub path: PathBuf,
    pub toml_value: toml::Table,
    pub prepend_auto_generated_comment: bool,
}

impl CargoTomlContents {
    pub fn parse_string(raw_str: &str, path: &Path) -> Self {
        let toml_value = toml::from_str::<toml::Table>(raw_str).unwrap_or_else(|e| {
            panic!(
                "failed to parse Cargo.toml toml format, path:{}, error: {e}",
                path.display()
            )
        });
        CargoTomlContents {
            path: path.to_owned(),
            toml_value,
            prepend_auto_generated_comment: false,
        }
    }

    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Self {
        let path_ref = path.as_ref();
        let cargo_toml_content = fs::read(path_ref).expect("failed to open Cargo.toml file");
        let cargo_toml_content_str =
            String::from_utf8(cargo_toml_content).expect("error decoding Cargo.toml utf-8");
        Self::parse_string(&cargo_toml_content_str, path_ref)
    }

    pub fn new() -> Self {
        CargoTomlContents::default()
    }

    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) {
        let cargo_toml_content_str = &self.to_string_pretty();
        let mut file = std::fs::File::create(path).expect("failed to create Cargo.toml file");
        file.write_all(cargo_toml_content_str.as_bytes())
            .expect("failed to write Cargo.toml contents to file");
    }

    pub fn package_name(&self) -> String {
        self.toml_value
            .get(PACKAGE)
            .unwrap_or_else(|| {
                panic!(
                    "missing [package] section in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .get("name")
            .unwrap_or_else(|| {
                panic!(
                    "missing package name in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .as_str()
            .unwrap_or_else(|| {
                panic!(
                    "package name is not a string in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .to_string()
    }

    pub fn package_version(&self) -> String {
        self.toml_value
            .get(PACKAGE)
            .unwrap_or_else(|| {
                panic!(
                    "missing [package] section in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .get("version")
            .unwrap_or_else(|| {
                panic!(
                    "missing package version in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .as_str()
            .unwrap_or_else(|| {
                panic!(
                    "package version is not a string in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .to_string()
    }

    pub fn package_edition(&self) -> String {
        self.toml_value
            .get(PACKAGE)
            .unwrap_or_else(|| {
                panic!(
                    "missing [package] section in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .get("edition")
            .unwrap_or_else(|| {
                panic!(
                    "missing package edition in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .as_str()
            .unwrap_or_else(|| {
                panic!(
                    "package edition is not a string in Cargo.toml: {}",
                    self.path.display()
                )
            })
            .to_string()
    }

    /// Interprets the dependency value and organizes values in a struct.
    pub fn dependency_raw_value(&self, crate_name: &str) -> Option<DependencyRawValue> {
        self.dependency(crate_name)
            .map(DependencyRawValue::parse_toml_value)
    }

    pub fn insert_dependency_raw_value(&mut self, crate_name: &str, raw_value: DependencyRawValue) {
        self.dependencies_mut()
            .insert(crate_name.to_owned(), raw_value.into_toml_value());
    }

    /// Assumes that a package section already exists.
    pub fn change_package_name(&mut self, new_package_name: String) {
        let package = self
            .toml_value
            .get_mut("package")
            .expect("missing package in Cargo.toml");
        package
            .as_table_mut()
            .expect("malformed package in Cargo.toml")
            .insert("name".to_string(), toml::Value::String(new_package_name));
    }

    pub fn dependencies_table(&self) -> Option<&Table> {
        if let Some(deps) = self.toml_value.get(CARGO_TOML_DEPENDENCIES) {
            deps.as_table()
        } else if let Some(deps) = self.toml_value.get(CARGO_TOML_DEV_DEPENDENCIES) {
            deps.as_table()
        } else {
            None
        }
    }

    pub fn dependency(&self, dep_name: &str) -> Option<&Value> {
        if let Some(deps_map) = self.dependencies_table() {
            deps_map.get(dep_name)
        } else {
            None
        }
    }

    /// Returns `true` if this manifest contains a `[package]` section.
    ///
    /// Manifests without `[package]` are called *virtual manifests* — they only define
    /// a `[workspace]` and contain no crate of their own.
    pub fn has_package(&self) -> bool {
        self.toml_value.contains_key(PACKAGE)
    }

    pub fn has_dependencies(&self) -> bool {
        self.toml_value.get(CARGO_TOML_DEPENDENCIES).is_some()
    }

    pub fn dependencies_mut(&mut self) -> &mut Table {
        self.toml_value
            .entry(CARGO_TOML_DEPENDENCIES)
            .or_insert(toml::Value::Table(toml::map::Map::new()))
            .as_table_mut()
            .expect("malformed crate Cargo.toml")
    }

    pub fn has_dev_dependencies(&self) -> bool {
        self.toml_value.get(CARGO_TOML_DEV_DEPENDENCIES).is_some()
    }

    pub fn change_author(&mut self, authors: String) -> bool {
        let package = self
            .toml_value
            .get_mut(PACKAGE)
            .unwrap_or_else(|| panic!("no dependencies found in crate {}", self.path.display()))
            .as_table_mut()
            .expect("missing package in Cargo.toml");

        package.remove(AUTHORS);

        package.insert(
            AUTHORS.to_owned(),
            toml::Value::Array(vec![toml::Value::String(authors)]),
        );

        true
    }

    pub fn dev_dependencies_mut(&mut self) -> &mut Table {
        self.toml_value
            .get_mut(CARGO_TOML_DEV_DEPENDENCIES)
            .unwrap_or_else(|| panic!("no dependencies found in crate {}", self.path.display()))
            .as_table_mut()
            .expect("malformed crate Cargo.toml")
    }

    pub fn add_crate_type(&mut self, crate_type: &str) {
        let mut value = toml::map::Map::new();
        let array = vec![toml::Value::String(crate_type.to_string())];
        let members = toml::Value::Array(array);
        value.insert("crate-type".to_string(), members);

        self.toml_value
            .insert("lib".to_string(), toml::Value::Table(value));
    }

    pub fn add_package_info(
        &mut self,
        name: &String,
        version: String,
        current_edition: String,
        publish: bool,
    ) {
        let mut value = toml::map::Map::new();
        value.insert("name".to_string(), Value::String(name.to_string()));
        value.insert("version".to_string(), Value::String(version));
        value.insert("edition".to_string(), Value::String(current_edition));
        value.insert("publish".to_string(), Value::Boolean(publish));

        self.toml_value
            .insert("package".to_string(), toml::Value::Table(value));
    }

    pub fn add_contract_variant_profile(&mut self, contract_profile: &ContractVariantProfile) {
        let mut profile_props = toml::map::Map::new();
        profile_props.insert(
            "codegen-units".to_string(),
            Value::Integer(contract_profile.codegen_units.into()),
        );
        profile_props.insert(
            "opt-level".to_string(),
            Value::String(contract_profile.opt_level.to_owned()),
        );
        profile_props.insert("lto".to_string(), Value::Boolean(contract_profile.lto));
        profile_props.insert("debug".to_string(), Value::Boolean(contract_profile.debug));
        profile_props.insert(
            "panic".to_string(),
            Value::String(contract_profile.panic.to_owned()),
        );
        profile_props.insert(
            "overflow-checks".to_string(),
            Value::Boolean(contract_profile.overflow_checks),
        );

        // add contract variant profile
        let mut toml_table = toml::map::Map::new();
        toml_table.insert("release".to_string(), toml::Value::Table(profile_props));

        // add profile dev
        let mut dev_value = toml::map::Map::new();
        dev_value.insert("panic".to_string(), Value::String("abort".to_string()));
        toml_table.insert("dev".to_string(), toml::Value::Table(dev_value));

        self.toml_value
            .insert("profile".to_string(), toml::Value::Table(toml_table));
    }

    pub fn add_workspace(&mut self, members: &[&str]) {
        let array: Vec<toml::Value> = members
            .iter()
            .map(|s| toml::Value::String(s.to_string()))
            .collect();
        let members_toml = toml::Value::Array(array);

        let mut workspace_table = toml::map::Map::new();
        // The resolver version is only needed in virtual workspaces (no [package] section).
        // When [package] is present, Cargo infers the resolver from the package's edition,
        // so omitting it here avoids a redundant field and Cargo does not warn.
        if !self.has_package() {
            workspace_table.insert(
                "resolver".to_string(),
                toml::Value::String(DEFAULT_WORKSPACE_RESOLVER.to_string()),
            );
        }
        workspace_table.insert("members".to_string(), members_toml);

        self.toml_value
            .insert("workspace".to_string(), toml::Value::Table(workspace_table));
    }

    pub fn local_dependency_paths(&self, ignore_deps: &[&str]) -> Vec<String> {
        let mut result = Vec::new();
        if let Some(deps_map) = self.dependencies_table() {
            for (key, value) in deps_map {
                if ignore_deps.contains(&key.as_str()) {
                    continue;
                }

                if let Some(path) = value.get("path") {
                    result.push(path.as_str().expect("path is not a string").to_string());
                }
            }
        }
        result
    }

    pub fn change_features_for_parent_crate_dep(
        &mut self,
        features: &[String],
        default_features: Option<bool>,
    ) {
        let deps_mut = self.dependencies_mut();
        for (_, dep) in deps_mut {
            if is_dep_path_above(dep) {
                let feature_values = features
                    .iter()
                    .map(|feature| Value::String(feature.clone()))
                    .collect();
                let deps_table = dep.as_table_mut().expect("malformed crate Cargo.toml");
                deps_table.insert("features".to_string(), Value::Array(feature_values));
                if let Some(default_features_value) = default_features {
                    deps_table.insert(
                        "default-features".to_string(),
                        Value::Boolean(default_features_value),
                    );
                }
            }
        }
    }

    pub fn to_string_pretty(&self) -> String {
        let toml_string =
            toml::to_string_pretty(&self.toml_value).expect("failed to format Cargo.toml contents");
        if self.prepend_auto_generated_comment {
            return format!("{}{}", AUTO_GENERATED, toml_string);
        }

        toml_string
    }

    /// Removes `path = "..."` from the given named dependencies in both
    /// `[dependencies]` and `[dev-dependencies]`.
    /// Returns `true` if any change was made.
    pub fn strip_dependency_paths(&mut self, crate_names: &[&str]) -> bool {
        let mut changed = false;
        for section in [CARGO_TOML_DEPENDENCIES, CARGO_TOML_DEV_DEPENDENCIES] {
            if let Some(deps) = self
                .toml_value
                .get_mut(section)
                .and_then(|v| v.as_table_mut())
            {
                for &crate_name in crate_names {
                    if let Some(dep) = deps.get_mut(crate_name) {
                        if let Some(table) = dep.as_table_mut() {
                            if table.remove("path").is_some() {
                                changed = true;
                            }
                        }
                    }
                }
            }
        }
        changed
    }
}

/// Checks that path == ".." in a dependency.
fn is_dep_path_above(dep: &Value) -> bool {
    if let Some(path) = dep.get("path") {
        if let Some(s) = path.as_str() {
            return s == "..";
        }
    }

    false
}

pub fn change_from_base_to_adapter_path(base_path: &Path) -> PathBuf {
    let path = base_path.to_string_lossy().replace("base", "wasm-adapter");

    Path::new("..").join(path)
}

/// TODO: still useful?
#[allow(unused)]
fn remove_quotes(var: &Value) -> String {
    var.to_string().replace('\"', "")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cargo_toml::{DependencyReference, GitCommitReference, VersionReq};

    #[test]
    fn test_change_from_base_to_adapter_path() {
        let base_path = Path::new("..")
            .join("..")
            .join("..")
            .join("framework")
            .join("base");
        let adapter_path = Path::new("..")
            .join("..")
            .join("..")
            .join("..")
            .join("framework")
            .join("wasm-adapter");

        assert_eq!(
            super::change_from_base_to_adapter_path(&base_path),
            adapter_path
        );
    }

    const CARGO_TOML_RAW: &str = r#"
[dependencies.by-version-1]
version = "0.54.0"

[dependencies.by-version-1-strict]
version = "=0.54.1"

[dependencies.by-git-commit-1]
git = "https://github.com/multiversx/repo1"
rev = "85c31b9ce730bd5ffe41589c353d935a14baaa96"

[dependencies.by-path-1]
path = "a/b/c"

[dependencies]
by-version-2 = "0.54.2"
by-version-2-strict = "=0.54.3"
by-path-2 = { path = "d/e/f" }
by-git-commit-2 = { git = "https://github.com/multiversx/repo2", rev = "e990be823f26d1e7f59c71536d337b7240dc3fa2" }
    "#;

    #[test]
    fn test_dependency_value() {
        let cargo_toml = CargoTomlContents::parse_string(CARGO_TOML_RAW, Path::new("test"));

        // version
        let raw_value = cargo_toml.dependency_raw_value("by-version-1").unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                version: Some("0.54.0".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::Version(VersionReq::from_version_str("0.54.0").unwrap()),
        );

        // version, strict
        let raw_value = cargo_toml
            .dependency_raw_value("by-version-1-strict")
            .unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                version: Some("=0.54.1".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::Version(VersionReq::from_version_str("0.54.1").unwrap().strict()),
        );

        // version, compact
        let raw_value = cargo_toml.dependency_raw_value("by-version-2").unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                version: Some("0.54.2".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::Version(VersionReq::from_version_str("0.54.2").unwrap()),
        );

        // version, compact, strict
        let raw_value = cargo_toml
            .dependency_raw_value("by-version-2-strict")
            .unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                version: Some("=0.54.3".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::Version(VersionReq::from_version_str("0.54.3").unwrap().strict()),
        );

        // git
        let raw_value = cargo_toml.dependency_raw_value("by-git-commit-1").unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                git: Some("https://github.com/multiversx/repo1".to_owned()),
                rev: Some("85c31b9ce730bd5ffe41589c353d935a14baaa96".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::GitCommit(GitCommitReference {
                git: "https://github.com/multiversx/repo1".to_owned(),
                rev: "85c31b9ce730bd5ffe41589c353d935a14baaa96".to_owned(),
            })
        );

        // git, compact
        let raw_value = cargo_toml.dependency_raw_value("by-git-commit-2").unwrap();
        assert_eq!(
            raw_value,
            DependencyRawValue {
                git: Some("https://github.com/multiversx/repo2".to_owned()),
                rev: Some("e990be823f26d1e7f59c71536d337b7240dc3fa2".to_owned()),
                ..Default::default()
            },
        );
        assert_eq!(
            raw_value.interpret(),
            DependencyReference::GitCommit(GitCommitReference {
                git: "https://github.com/multiversx/repo2".to_owned(),
                rev: "e990be823f26d1e7f59c71536d337b7240dc3fa2".to_owned(),
            })
        );

        // path
        let raw_value = cargo_toml.dependency_raw_value("by-path-1").unwrap();
        let path = Path::new("a").join("b").join("c");
        assert_eq!(
            raw_value,
            DependencyRawValue {
                path: Some(path.clone()),
                ..Default::default()
            },
        );
        assert_eq!(raw_value.interpret(), DependencyReference::Path(path),);

        // path, compact
        let raw_value = cargo_toml.dependency_raw_value("by-path-2").unwrap();
        let path = Path::new("d").join("e").join("f");
        assert_eq!(
            raw_value,
            DependencyRawValue {
                path: Some(path.clone()),
                ..Default::default()
            },
        );
        assert_eq!(raw_value.interpret(), DependencyReference::Path(path),);
    }
}
