use std::collections::HashMap;

use nu_protocol::{IntoSpanned, Record, ShellError, Span, Spanned, Value};

use crate::commands::{
    Perms, confirmation_prompt, dry_run_command, run_command, run_command_for_stdout,
};
use crate::parser::Engine;
use crate::{CleanCacheCommand, CleanCommand, SyncCommand, UpdateCommand};
use crate::{mod_err, nest_errors};

use super::{Backend, Error, Result};

const TOOLCHAIN_LIST_KEY: &str = "toolchains";
const COMPONENT_LIST_KEY: &str = "components";
const TARGET_LIST_KEY: &str = "targets";
const ARCH_KEY: &str = "arch";
const VENDOR_KEY: &str = "vendor";
const OS_KEY: &str = "os";

const DEFAULT_COMPONENTS: [&str; 7] = [
    "cargo",
    "clippy",
    "rust-docs",
    "rust-std",
    "rust-src",
    "rustc",
    "rustfmt",
];

#[derive(Debug, Clone)]
pub struct Rustup {
    toolchains: HashMap<String, ToolchainSpec>,
}

#[derive(Debug, Clone)]
struct ToolchainSpec {
    targets: Box<[String]>,
    components: Box<[String]>,
}

impl Backend for Rustup {
    fn new(value: Spanned<&Record>, _config: Spanned<&Record>) -> Result<Self> {
        let toolchains = value
            .item
            .get(TOOLCHAIN_LIST_KEY)
            .ok_or_else(|| mod_err!(value.span, "Failed to get toolchains for Rustup"))?;

        let span = toolchains.span();
        let toolchains = toolchains.as_record().map_err(|e: ShellError| {
            nest_errors!(
                (value.span, span),
                "The toolchain spec in Rustup is not a record",
                e
            )
        })?;

        let toolchains = values_to_pkgspec(toolchains.into_spanned(span))?;

        log::info!("Successfully parsed rustup packages");
        Ok(Rustup { toolchains })
    }

    fn install(&self, _engine: &mut Engine, opts: &SyncCommand) -> Result<()> {
        let installed_toolchains = get_installed_toolchains()?;

        self.install_toolchains(installed_toolchains.as_ref(), opts)?;
        self.install_missing(installed_toolchains.as_ref(), opts)?;

        Ok(())
    }

    fn remove(&self, opts: &CleanCommand) -> Result<()> {
        let installed_toolchains = get_installed_toolchains()?;

        self.remove_toolchains(installed_toolchains.as_ref(), opts)?;
        self.remove_extra(installed_toolchains.as_ref(), opts)?;

        Ok(())
    }

    fn clean_cache(&self, _opts: &CleanCacheCommand) -> Result<()> {
        // Nothing to do here
        Ok(())
    }

    fn update(&self, opts: &UpdateCommand) -> Result<()> {
        if !opts.dry_run
            && !opts.no_confirm
            && !confirmation_prompt(
                "Do you want to update rustup?: ",
                &["Using <rustup update>"],
            )?
        {
            return Ok(());
        }

        let command_action = if opts.dry_run {
            dry_run_command
        } else {
            run_command
        };

        command_action(["rustup", "update"], Perms::User)
            .inspect(|_| log::info!("Successfully updated rustup backend"))
            .map_err(|e| nest_errors!("Failed to update rustup", e))
    }
}

impl Rustup {
    fn install_toolchains(
        &self,
        installed_toolchains: &[String],
        opts: &SyncCommand,
    ) -> Result<()> {
        let configured_toolchains = self.toolchains.keys();

        let missing_toolchains: Box<[_]> = configured_toolchains
            .filter(|toolchain| {
                !installed_toolchains
                    .iter()
                    .any(|installed| installed.starts_with(*toolchain))
            })
            .collect();

        if missing_toolchains.is_empty() {
            log::info!("No missing toolchains to install");
            return Ok(());
        }

        if !opts.dry_run
            && !opts.no_confirm
            && !confirmation_prompt(
                "Do you want to install the following toolchains for rustup?: ",
                &missing_toolchains,
            )?
        {
            return Ok(());
        }

        missing_toolchains
            .into_iter()
            .map(|toolchain| (toolchain, self.toolchains.get(toolchain).unwrap()))
            .try_for_each(|(toolchain, spec)| install_missing_toolchain(toolchain, spec, opts))
            .inspect(|_| log::info!("Successfully installed all the missing toolchains"))
    }

    fn install_missing(&self, installed_toolchains: &[String], opts: &SyncCommand) -> Result<()> {
        let mut configured_toolchains = installed_toolchains
            .iter()
            .filter_map(|toolchain| {
                self.toolchains
                    .keys()
                    .find(|configured| toolchain.starts_with(*configured))
            })
            .peekable();

        if configured_toolchains.peek().is_none() {
            log::info!("No missing components or targets to install");
            return Ok(());
        }

        for toolchain in configured_toolchains {
            let toolchain_spec = self.toolchains.get(toolchain).unwrap();

            install_missing_targets(toolchain, toolchain_spec.targets.as_ref(), opts)?;
            install_missing_components(toolchain, toolchain_spec.components.as_ref(), opts)?;
        }

        log::info!("Installed missing components and targets");
        Ok(())
    }

    fn remove_toolchains(
        &self,
        installed_toolchains: &[String],
        opts: &CleanCommand,
    ) -> Result<()> {
        let configured_toolchains = &self.toolchains;

        let extra_toolchains: Box<[_]> = installed_toolchains
            .iter()
            .filter(|toolchain| {
                !configured_toolchains
                    .keys()
                    .into_iter()
                    .any(|configured| toolchain.starts_with(configured))
            })
            .map(String::as_str)
            .collect();

        if extra_toolchains.is_empty() {
            log::info!("No extra toolchains to remove!");
            return Ok(());
        }

        let command_action = if opts.dry_run {
            dry_run_command
        } else {
            run_command
        };

        if opts.dry_run
            || opts.no_confirm
            || confirmation_prompt(
                "Do you want to remove the following toolchains for rustup?: ",
                &extra_toolchains,
            )?
        {
            command_action(
                ["rustup", "toolchain", "remove"]
                    .into_iter()
                    .chain(extra_toolchains),
                Perms::User,
            )
            .inspect(|_| log::info!("Successfully removed unused toolchains"))
            .map_err(|e| nest_errors!("Failed to remove toolchains", e))
        } else {
            Ok(())
        }
    }

    fn remove_extra(&self, installed_toolchains: &[String], opts: &CleanCommand) -> Result<()> {
        let configured_toolchains = &self.toolchains;

        let mut present_toolchains = installed_toolchains
            .iter()
            .flat_map(|toolchain| {
                configured_toolchains
                    .keys()
                    .find(|configured| toolchain.starts_with(*configured))
            })
            .peekable();

        if present_toolchains.peek().is_none() {
            log::info!("No extra components or targets to remove");
            return Ok(());
        }

        for toolchain in present_toolchains {
            let toolchain_spec = self.toolchains.get(toolchain).unwrap();

            remove_extra_targets(toolchain, &toolchain_spec.targets, opts)?;
            remove_extra_components(toolchain, &toolchain_spec.components, opts)?;
        }

        log::info!("Removed extra components and targets");
        Ok(())
    }
}

fn values_to_pkgspec(record: Spanned<&Record>) -> Result<HashMap<String, ToolchainSpec>> {
    record
        .item
        .iter()
        .map(|(toolchain, value)| -> Result<_> {
            Ok((
                toolchain.to_owned(),
                value_to_toolchainspec(toolchain, value, record.span)?,
            ))
        })
        .collect()
}

fn get_installed_toolchains() -> Result<Box<[String]>> {
    let toolchains = run_command_for_stdout(["rustup", "toolchain", "list"], Perms::User, true)
        .map_err(|e| nest_errors!("Failed to get toolchains", e))?;

    let toolchains = toolchains
        .lines()
        .map(|toolchain| toolchain.split_once(' ').map_or(toolchain, |split| split.0))
        .map(ToOwned::to_owned)
        .collect();

    Ok(toolchains)
}

fn install_missing_toolchain(
    toolchain: &str,
    toolchain_spec: &ToolchainSpec,
    opts: &SyncCommand,
) -> Result<()> {
    let components = Some(
        ["--component"]
            .into_iter()
            .chain(toolchain_spec.components.iter().map(String::as_str)),
    )
    .into_iter()
    .filter(|_| !toolchain_spec.components.is_empty())
    .flatten();

    let targets = Some(
        ["--target"]
            .into_iter()
            .chain(toolchain_spec.targets.iter().map(String::as_str)),
    )
    .into_iter()
    .filter(|_| !toolchain_spec.targets.is_empty())
    .flatten();

    let command_action = if opts.dry_run {
        dry_run_command
    } else {
        run_command
    };

    command_action(
        ["rustup", "toolchain", "install", toolchain]
            .into_iter()
            .chain(components)
            .chain(targets),
        Perms::User,
    )
    .inspect(|_| log::debug!("Successfully installed missing toolchain {toolchain}"))
    .map_err(|e| nest_errors!(format!("Failed to install toolchain {toolchain}"), e))
}

fn install_missing_targets(
    toolchain: &String,
    configured_targets: &[String],
    opts: &SyncCommand,
) -> Result<()> {
    let installed_targets = get_installed_targets(toolchain)?;

    let missing_targets: Box<[_]> = configured_targets
        .iter()
        .filter(|target| !installed_targets.contains(target))
        .map(String::as_str)
        .collect();

    if missing_targets.is_empty() {
        log::debug!("No missing targets to install for {toolchain}");
        return Ok(());
    }

    let command_action = if opts.dry_run {
        dry_run_command
    } else {
        run_command
    };

    if opts.dry_run
        || opts.no_confirm
        || confirmation_prompt(
            "Do you want to install the following targets for ".to_string()
                + toolchain
                + "? (Rustup): ",
            &missing_targets,
        )?
    {
        command_action(
            ["rustup", "target", "add", "--toolchain", toolchain]
                .into_iter()
                .chain(missing_targets),
            Perms::User,
        )
        .map_err(|e| nest_errors!(format!("Failed to add targets for {toolchain}"), e))
    } else {
        Ok(())
    }
}

fn install_missing_components(
    toolchain: &String,
    configured_components: &[String],
    opts: &SyncCommand,
) -> Result<()> {
    let installed_components = get_installed_components(toolchain)?;

    let missing_components: Box<[_]> = configured_components
        .iter()
        .map(String::as_str)
        .chain(DEFAULT_COMPONENTS)
        .filter(|component| {
            !installed_components
                .iter()
                .any(|comp| comp.starts_with(*component))
        })
        .collect();

    if missing_components.is_empty() {
        log::debug!("No components left to install for {toolchain}");
        return Ok(());
    }

    let command_action = if opts.dry_run {
        dry_run_command
    } else {
        run_command
    };

    if opts.dry_run
        || opts.no_confirm
        || confirmation_prompt(
            "Do you want to install the following components for ".to_string()
                + toolchain
                + "? (Rustup): ",
            &missing_components,
        )?
    {
        command_action(
            ["rustup", "component", "add", "--toolchain", toolchain]
                .into_iter()
                .chain(missing_components),
            Perms::User,
        )
        .map_err(|e| nest_errors!(format!("Failed to add components for {toolchain}"), e))
    } else {
        Ok(())
    }
}

fn remove_extra_targets(
    toolchain: &str,
    configured_targets: &[String],
    opts: &CleanCommand,
) -> Result<()> {
    let installed_targets = get_installed_targets(toolchain)?;

    let extra_targets: Box<[_]> = installed_targets
        .iter()
        .filter(|target| !configured_targets.contains(target))
        .map(String::as_str)
        .collect();

    if extra_targets.is_empty() {
        log::debug!("No extra targets to remove for {toolchain}!");
        return Ok(());
    }

    let command_action = if opts.dry_run {
        dry_run_command
    } else {
        run_command
    };

    if opts.dry_run
        || opts.no_confirm
        || confirmation_prompt(
            "Do you want to remove the following targets for ".to_string()
                + toolchain
                + "? (Rustup): ",
            &extra_targets,
        )?
    {
        command_action(
            ["rustup", "target", "remove", "--toolchain", toolchain]
                .into_iter()
                .chain(extra_targets),
            Perms::User,
        )
        .inspect(|_| log::debug!("Remove extra targets for {toolchain}"))
        .map_err(|e| {
            nest_errors!(
                format!("Failed to remove unused targets for {toolchain}"),
                e
            )
        })
    } else {
        Ok(())
    }
}

fn remove_extra_components(
    toolchain: &str,
    configured_components: &[String],
    opts: &CleanCommand,
) -> Result<()> {
    let installed_components = get_installed_components(toolchain)?;

    let extra_components: Box<[_]> = installed_components
        .iter()
        .filter(|component| {
            !configured_components
                .iter()
                .map(String::as_str)
                .chain(DEFAULT_COMPONENTS)
                .any(|comp| component.starts_with(comp))
        })
        .map(String::as_str)
        .collect();

    if extra_components.is_empty() {
        log::debug!("No extra components to remove for {toolchain}!");
        return Ok(());
    }

    let command_action = if opts.dry_run {
        dry_run_command
    } else {
        run_command
    };

    if opts.dry_run
        || opts.no_confirm
        || confirmation_prompt(
            "Do you want to install the following components for ".to_string()
                + toolchain
                + "? (Rustup): ",
            &extra_components,
        )?
    {
        command_action(
            ["rustup", "component", "remove", "--toolchain", toolchain]
                .into_iter()
                .chain(extra_components),
            Perms::User,
        )
        .inspect(|_| log::debug!("Removed extra components for {toolchain}"))
        .map_err(|e| {
            nest_errors!(
                format!("rustup command to remove components for {toolchain} failed!"),
                e
            )
        })
    } else {
        Ok(())
    }
}

fn value_to_toolchainspec(
    toolchain: &str,
    value: &Value,
    outer_span: Span,
) -> Result<ToolchainSpec> {
    let record_msg = format!("Parse error for value in {toolchain}, not a record");
    let record = value
        .as_record()
        .map_err(|e| nest_errors!((outer_span, value.span()), record_msg, e))?;

    let components = match record.get(COMPONENT_LIST_KEY) {
        Some(components) => {
            let span = components.span();
            let components = components.as_list().map_err(|e| {
                nest_errors!(
                    (value.span(), span),
                    "Failed to convert components to a list",
                    e
                )
            })?;

            parse_components(components.into_spanned(span))?
        }
        None => {
            log::debug!("No components specified in {toolchain}, using defaults");
            Box::new([])
        }
    };

    let targets: Box<[_]> = match record.get(TARGET_LIST_KEY) {
        Some(targets) => {
            let msg = format!("Parse error for targets in {toolchain}, not a list");
            let span = targets.span();
            let targets = targets
                .as_list()
                .map_err(|e| nest_errors!((value.span(), span), msg, e))?;
            targets
                .iter()
                .map(|target| parse_target(target, toolchain, span))
                .collect::<Result<_>>()?
        }
        None => {
            log::debug!("No targets specified in {toolchain}, using defaults");
            Box::new([])
        }
    };

    Ok(ToolchainSpec {
        targets,
        components,
    })
}

fn get_installed_targets(toolchain: &str) -> Result<Box<[String]>> {
    let targets = run_command_for_stdout(
        [
            "rustup",
            "target",
            "list",
            "--toolchain",
            toolchain,
            "--installed",
            "--quiet",
        ],
        Perms::User,
        false,
    )
    .map_err(|e| {
        nest_errors!(
            format!("rustup command to find targets for {toolchain} failed"),
            e
        )
    })?;

    let targets = targets
        .lines()
        .map(str::trim)
        .map(ToOwned::to_owned)
        .collect();

    Ok(targets)
}

fn get_installed_components(toolchain: &str) -> Result<Box<[String]>> {
    let components = run_command_for_stdout(
        [
            "rustup",
            "component",
            "list",
            "--toolchain",
            toolchain,
            "--installed",
            "--quiet",
        ],
        Perms::User,
        false,
    )
    .map_err(|e| {
        nest_errors!(
            format!("rustup command to find components for {toolchain} failed"),
            e
        )
    })?;

    let components = components
        .lines()
        .map(str::trim)
        .map(ToOwned::to_owned)
        .collect();

    Ok(components)
}

fn parse_components(components: Spanned<&[Value]>) -> Result<Box<[String]>> {
    components
        .item
        .iter()
        .map(|comp| {
            comp.as_str().map(ToOwned::to_owned).map_err(|e| {
                nest_errors!(
                    (components.span, comp.span()),
                    "Expected a string for component",
                    e
                )
            })
        })
        .collect()
}

fn parse_target(target: &Value, toolchain: &str, outer_span: Span) -> Result<String> {
    let span = target.span();

    let target = target.as_record().map_err(|e| {
        nest_errors!(
            (outer_span, span),
            format!("Specified target for {toolchain} not a record"),
            e
        )
    })?;

    let arch = target.get(ARCH_KEY).ok_or_else(|| {
        mod_err!(
            (outer_span, span),
            format!("Failed to get architecture from target for {toolchain}")
        )
    })?;

    let arch = arch.as_str().map_err(|e| {
        nest_errors!(
            (span, arch.span()),
            "Architecture specified is not a string",
            e
        )
    })?;

    let vendor = target
        .get(VENDOR_KEY)
        .map(|vendor| {
            let msg = format!("Vendor specified is not a string for {arch} in {toolchain}");

            vendor
                .as_str()
                .map_err(|e| nest_errors!((span, vendor.span()), msg, e))
        })
        .unwrap_or_else(|| {
            log::debug!("Using default value for vendor in {toolchain}'s {arch}");
            Ok("unknown")
        })?;

    let os = target
        .get(OS_KEY)
        .map(|os| {
            let msg = "OS specified is not a string for {arch} in {toolchain}";
            os.as_str()
                .map_err(|e| nest_errors!((span, os.span()), msg, e))
        })
        .unwrap_or_else(|| {
            log::debug!("Using default value for vendor in {toolchain}'s {arch}");
            Ok("none")
        })?;

    Ok([arch, vendor, os].join("-"))
}

#[cfg(test)]
mod test {
    use nu_protocol::Span;

    use super::*;

    #[test]
    fn rustup_backend_ok() {
        let component_list = vec![Value::string("foo", Span::test_data())];

        let target = Record::from_raw_cols_vals(
            ["arch", "vendor", "os"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            ["AMD64", "unknown-elf", "linux"]
                .into_iter()
                .map(|string| Value::string(string, Span::test_data()))
                .collect(),
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();
        let target_list = vec![Value::record(target, Span::test_data())];

        let inner_record = Record::from_raw_cols_vals(
            ["components", "targets"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::list(component_list, Span::test_data()),
                Value::list(target_list, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let toolchain_record = Record::from_raw_cols_vals(
            vec!["toolchain1".to_owned()],
            vec![Value::record(inner_record, Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let outer_record = Record::from_raw_cols_vals(
            vec!["toolchains".to_owned()],
            vec![Value::record(toolchain_record, Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let result = Rustup::new(
            (&outer_record).into_spanned(Span::unknown()),
            (&Record::new()).into_spanned(Span::unknown()),
        );
        assert!(result.is_ok());
    }

    #[test]
    fn rustup_backend_not_record() {
        let outer_record = Record::from_raw_cols_vals(
            vec!["toolchains".to_owned()],
            vec![Value::string("foo", Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let result = Rustup::new(
            (&outer_record).into_spanned(Span::unknown()),
            (&Record::new()).into_spanned(Span::unknown()),
        );
        assert!(result.is_err());
    }

    #[test]
    fn values_to_fields_ok() {
        let target = Record::from_raw_cols_vals(
            ["arch", "vendor", "os"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            ["AMD64", "unknown-elf", "linux"]
                .into_iter()
                .map(|string| Value::string(string, Span::test_data()))
                .collect(),
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();
        let target_list = vec![Value::record(target, Span::test_data())];
        let component_list = vec![Value::string("foo", Span::test_data())];

        let inner_record = Record::from_raw_cols_vals(
            vec!["components".to_owned(), "targets".to_owned()],
            vec![
                Value::list(component_list, Span::test_data()),
                Value::list(target_list, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let result = value_to_toolchainspec(
            "_",
            &Value::record(inner_record, Span::test_data()),
            Span::test_data(),
        );
        assert!(result.is_ok());
        let result = result.unwrap();
        assert_eq!(*result.targets, ["AMD64-unknown-elf-linux"]);
        assert_eq!(*result.components, ["foo"]);
    }

    #[test]
    fn values_to_fields_components_missing() {
        let target = Record::from_raw_cols_vals(
            ["arch", "vendor", "os"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            ["AMD64", "unknown-elf", "linux"]
                .into_iter()
                .map(|string| Value::string(string, Span::test_data()))
                .collect(),
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();
        let target_list = vec![Value::record(target, Span::test_data())];

        let inner_record = Record::from_raw_cols_vals(
            vec!["targets".to_owned()],
            vec![Value::list(target_list, Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let result = value_to_toolchainspec(
            "_",
            &Value::record(inner_record, Span::test_data()),
            Span::test_data(),
        );
        assert!(result.is_ok());
        let result = result.unwrap();
        let res: [String; 0] = [];
        assert_eq!(*result.components, res);
    }

    #[test]
    fn values_to_fields_targets_missing() {
        let component_list = vec![Value::string("foo", Span::test_data())];

        let inner_record = Record::from_raw_cols_vals(
            vec!["components".to_owned()],
            vec![Value::list(component_list, Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let result = value_to_toolchainspec(
            "_",
            &Value::record(inner_record, Span::test_data()),
            Span::test_data(),
        );

        assert!(result.is_ok());
        let res: [String; 0] = [];
        let result = result.unwrap();
        assert_eq!(*result.targets, res);
    }

    #[test]
    fn values_to_fields_not_record() {
        let result = value_to_toolchainspec(
            "_",
            &Value::string("foo", Span::test_data()),
            Span::test_data(),
        );
        assert!(result.is_err());
        // let res: [String; 0] = [];
        // assert_eq!(*result.0.0, res);
        // assert_eq!(*result.1.0, res);
    }

    #[test]
    fn parse_components_ok() {
        let components: Vec<_> = ["foo", "bar", "aaaa"]
            .into_iter()
            .map(|comp| Value::string(comp, Span::test_data()))
            .collect();

        let result = parse_components(components.as_slice().into_spanned(Span::test_data()));
        assert!(result.is_ok());
        let result = result.unwrap();
        assert_eq!(result.len(), 3);
        assert_eq!(result[0], "foo");
        assert_eq!(result[1], "bar");
        assert_eq!(result[2], "aaaa");
    }

    #[test]
    fn parse_components_bad() {
        let mut components: Vec<_> = ["foo", "bar", "aaaa"]
            .into_iter()
            .map(|comp| Value::string(comp, Span::test_data()))
            .collect();

        components.push(Value::bool(true, Span::test_data()));

        let result = parse_components(components.as_slice().into_spanned(Span::test_data()));
        assert!(result.is_err());
    }
}
