use anyhow::Error;
use curl::easy::Easy;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::convert::TryInto;

const DIST_SERVER: &str = "https://static.rust-lang.org";
const COMPILER_COMPONENTS: &[&str] = &["rustc", "rust-std", "cargo"];
const RUSTFMT_COMPONENTS: &[&str] = &["rustfmt-preview"];

struct Tool {
    channel: Channel,
    version: [u16; 3],
    checksums: IndexMap<String, String>,
}

impl Tool {
    fn new() -> Result<Self, Error> {
        let channel = match std::fs::read_to_string("src/ci/channel")?.trim() {
            "stable" => Channel::Stable,
            "beta" => Channel::Beta,
            "nightly" => Channel::Nightly,
            other => anyhow::bail!("unsupported channel: {}", other),
        };

        // Split "1.42.0" into [1, 42, 0]
        let version = std::fs::read_to_string("src/version")?
            .trim()
            .split('.')
            .map(|val| val.parse())
            .collect::<Result<Vec<_>, _>>()?
            .try_into()
            .map_err(|_| anyhow::anyhow!("failed to parse version"))?;

        Ok(Self { channel, version, checksums: IndexMap::new() })
    }

    fn update_json(mut self) -> Result<(), Error> {
        std::fs::write(
            "src/stage0.json",
            format!(
                "{}\n",
                serde_json::to_string_pretty(&Stage0 {
                    comment: "Generated by `./x.py run src/tools/bump-stage0`. \
                              Run that command again to update the bootstrap compiler.",
                    dist_server: DIST_SERVER.into(),
                    compiler: self.detect_compiler()?,
                    rustfmt: self.detect_rustfmt()?,
                    checksums_sha256: {
                        // Keys are sorted here instead of beforehand because values in this map
                        // are added while filling the other struct fields just above this block.
                        self.checksums.sort_keys();
                        self.checksums
                    }
                })?
            ),
        )?;
        Ok(())
    }

    // Currently Rust always bootstraps from the previous stable release, and in our train model
    // this means that the master branch bootstraps from beta, beta bootstraps from current stable,
    // and stable bootstraps from the previous stable release.
    //
    // On the master branch the compiler version is configured to `beta` whereas if you're looking
    // at the beta or stable channel you'll likely see `1.x.0` as the version, with the previous
    // release's version number.
    fn detect_compiler(&mut self) -> Result<Stage0Toolchain, Error> {
        let channel = match self.channel {
            Channel::Stable | Channel::Beta => {
                // The 1.XX manifest points to the latest point release of that minor release.
                format!("{}.{}", self.version[0], self.version[1] - 1)
            }
            Channel::Nightly => "beta".to_string(),
        };

        let manifest = fetch_manifest(&channel)?;
        self.collect_checksums(&manifest, COMPILER_COMPONENTS)?;
        Ok(Stage0Toolchain {
            date: manifest.date,
            version: if self.channel == Channel::Nightly {
                "beta".to_string()
            } else {
                // The version field is like "1.42.0 (abcdef1234 1970-01-01)"
                manifest.pkg["rust"]
                    .version
                    .split_once(' ')
                    .expect("invalid version field")
                    .0
                    .to_string()
            },
        })
    }

    /// We use a nightly rustfmt to format the source because it solves some bootstrapping issues
    /// with use of new syntax in this repo. For the beta/stable channels rustfmt is not provided,
    /// as we don't want to depend on rustfmt from nightly there.
    fn detect_rustfmt(&mut self) -> Result<Option<Stage0Toolchain>, Error> {
        if self.channel != Channel::Nightly {
            return Ok(None);
        }

        let manifest = fetch_manifest("nightly")?;
        self.collect_checksums(&manifest, RUSTFMT_COMPONENTS)?;
        Ok(Some(Stage0Toolchain { date: manifest.date, version: "nightly".into() }))
    }

    fn collect_checksums(&mut self, manifest: &Manifest, components: &[&str]) -> Result<(), Error> {
        let prefix = format!("{}/", DIST_SERVER);
        for component in components {
            let pkg = manifest
                .pkg
                .get(*component)
                .ok_or_else(|| anyhow::anyhow!("missing component from manifest: {}", component))?;
            for target in pkg.target.values() {
                for pair in &[(&target.url, &target.hash), (&target.xz_url, &target.xz_hash)] {
                    if let (Some(url), Some(sha256)) = pair {
                        let url = url
                            .strip_prefix(&prefix)
                            .ok_or_else(|| {
                                anyhow::anyhow!("url doesn't start with dist server base: {}", url)
                            })?
                            .to_string();
                        self.checksums.insert(url, sha256.clone());
                    }
                }
            }
        }
        Ok(())
    }
}

fn main() -> Result<(), Error> {
    let tool = Tool::new()?;
    tool.update_json()?;
    Ok(())
}

fn fetch_manifest(channel: &str) -> Result<Manifest, Error> {
    Ok(toml::from_slice(&http_get(&format!(
        "{}/dist/channel-rust-{}.toml",
        DIST_SERVER, channel
    ))?)?)
}

fn http_get(url: &str) -> Result<Vec<u8>, Error> {
    let mut data = Vec::new();
    let mut handle = Easy::new();
    handle.fail_on_error(true)?;
    handle.url(url)?;
    {
        let mut transfer = handle.transfer();
        transfer.write_function(|new_data| {
            data.extend_from_slice(new_data);
            Ok(new_data.len())
        })?;
        transfer.perform()?;
    }
    Ok(data)
}

#[derive(Debug, PartialEq, Eq)]
enum Channel {
    Stable,
    Beta,
    Nightly,
}

#[derive(Debug, serde::Serialize)]
struct Stage0 {
    #[serde(rename = "__comment")]
    comment: &'static str,
    dist_server: String,
    compiler: Stage0Toolchain,
    rustfmt: Option<Stage0Toolchain>,
    checksums_sha256: IndexMap<String, String>,
}

#[derive(Debug, serde::Serialize)]
struct Stage0Toolchain {
    date: String,
    version: String,
}

#[derive(Debug, serde::Deserialize)]
struct Manifest {
    date: String,
    pkg: HashMap<String, ManifestPackage>,
}

#[derive(Debug, serde::Deserialize)]
struct ManifestPackage {
    version: String,
    target: HashMap<String, ManifestTargetPackage>,
}

#[derive(Debug, serde::Deserialize)]
struct ManifestTargetPackage {
    url: Option<String>,
    hash: Option<String>,
    xz_url: Option<String>,
    xz_hash: Option<String>,
}
