use nu_engine::command_prelude::*;
use serde::Deserialize;
use update_informer::{
    http_client::{GenericHttpClient, HttpClient},
    registry, Check, Package, Registry, Result as UpdateResult,
};

#[derive(Clone)]
pub struct VersionCheck;

impl Command for VersionCheck {
    fn name(&self) -> &str {
        "version check"
    }

    fn description(&self) -> &str {
        "Checks to see if you have the latest version of nushell."
    }

    fn extra_description(&self) -> &str {
        "If you're running nushell nightly, `version check` will check to see if you are running the latest nightly version. If you are running the nushell release, `version check` will check to see if you're running the latest release version."
    }

    fn signature(&self) -> Signature {
        Signature::build("version check")
            .category(Category::Platform)
            .input_output_types(vec![(Type::Nothing, Type::String)])
    }

    fn examples(&self) -> Vec<Example> {
        vec![Example {
            description: "Check if you have the latest version of nushell",
            example: "version check",
            result: None,
        }]
    }

    fn run(
        &self,
        _engine_state: &EngineState,
        _stack: &mut Stack,
        _call: &Call,
        _input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let version_check = check_for_latest_nushell_version();
        Ok(version_check.into_pipeline_data())
    }
}

pub struct NuShellNightly;

impl Registry for NuShellNightly {
    const NAME: &'static str = "nushell/nightly";

    fn get_latest_version<T: HttpClient>(
        http_client: GenericHttpClient<T>,
        pkg: &Package,
    ) -> UpdateResult<Option<String>> {
        #[derive(Deserialize, Debug)]
        struct Response {
            tag_name: String,
        }

        let url = format!("https://api.github.com/repos/{}/releases", pkg);
        let versions = http_client
            .add_header("Accept", "application/vnd.github.v3+json")
            .add_header("User-Agent", "update-informer")
            .get::<Vec<Response>>(&url)?;

        if let Some(v) = versions.first() {
            // The nightly repo tags look like "0.102.0-nightly.4+23dc1b6"
            // We want to return the "0.102.0-nightly.4" part because hustcer
            // is changing the cargo.toml package.version to be that syntax
            let up_through_plus = match v.tag_name.split('+').next() {
                Some(v) => v,
                None => &v.tag_name,
            };
            return Ok(Some(up_through_plus.to_string()));
        }

        Ok(None)
    }
}

struct NativeTlsHttpClient;

impl HttpClient for NativeTlsHttpClient {
    fn get<T: serde::de::DeserializeOwned>(
        url: &str,
        timeout: std::time::Duration,
        headers: update_informer::http_client::HeaderMap,
    ) -> update_informer::Result<T> {
        let agent = ureq::AgentBuilder::new()
            .tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new()?))
            .build();

        let mut req = agent.get(url).timeout(timeout);

        for (header, value) in headers {
            req = req.set(header, value);
        }

        let json = req.call()?.into_json()?;

        Ok(json)
    }
}

pub fn check_for_latest_nushell_version() -> Value {
    let current_version = env!("CARGO_PKG_VERSION").to_string();

    let mut rec = Record::new();

    if current_version.contains("nightly") {
        rec.push("channel", Value::test_string("nightly"));

        let nightly_pkg_name = "nushell/nightly";
        // The .interval() determines how long the cached check lives. Setting it to std::time::Duration::ZERO
        // means that there is essentially no cache and it will check for a new version each time you run nushell.
        // Since this is run on demand, there isn't really a need to cache the check.
        let informer =
            update_informer::new(NuShellNightly, nightly_pkg_name, current_version.clone())
                .http_client(NativeTlsHttpClient)
                .interval(std::time::Duration::ZERO);

        if let Ok(Some(new_version)) = informer.check_version() {
            rec.push("current", Value::test_bool(false));
            rec.push("latest", Value::test_string(format!("{}", new_version)));
            Value::test_record(rec)
        } else {
            rec.push("current", Value::test_bool(true));
            rec.push("latest", Value::test_string(current_version.clone()));
            Value::test_record(rec)
        }
    } else {
        rec.push("channel", Value::test_string("release"));

        let normal_pkg_name = "nushell/nushell";
        // By default, this update request is cached for 24 hours so it won't check for a new version
        // each time you run nushell. Since this is run on demand, there isn't really a need to cache the check which
        // is why we set the interval to std::time::Duration::ZERO.
        let informer =
            update_informer::new(registry::GitHub, normal_pkg_name, current_version.clone())
                .interval(std::time::Duration::ZERO);

        if let Ok(Some(new_version)) = informer.check_version() {
            rec.push("current", Value::test_bool(false));
            rec.push("latest", Value::test_string(format!("{}", new_version)));
            Value::test_record(rec)
        } else {
            rec.push("current", Value::test_bool(true));
            rec.push("latest", Value::test_string(current_version.clone()));
            Value::test_record(rec)
        }
    }
}
