use handlebars::Handlebars;
use regex::Regex;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::ser::PrettyFormatter;
use std::{collections::HashMap, env, process::Stdio};
use tokio::{fs, io::AsyncWriteExt, process::Command};
use toml_edit::DocumentMut;

const MANIFEST: &str = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
const DOWNLOADS: &str = "https://net-secondary.web.minecraft-services.net/api/v1.0/download/links";
const BEDROCK_REGEX: &str = r"https:\/\/www\.minecraft\.net\/bedrockdedicatedserver\/bin-linux\/bedrock-server-([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\.zip";
const PROJECT_ID: &str = "54750221";
const PROJECT_SLUG: &str = "minecraft-server-rs";

#[derive(Deserialize)]
struct Manifest {
    latest: Latest,
}
#[derive(Deserialize)]
struct Latest {
    release: String,
}
#[derive(Serialize, Deserialize)]
struct Config {
    #[serde(rename = "minecraft-server-rs")]
    minecraft_server_rs: String,
}

#[tokio::main]
async fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 2 {
        eprintln!("Usage: <program> <gitlab_token>");
        std::process::exit(1);
    }

    let gitlab_token = &args[1]; // GitLab API token

    let manifest: Manifest = reqwest::get(MANIFEST).await.unwrap().json().await.unwrap();

    let html = reqwest::get(DOWNLOADS).await.unwrap().text().await.unwrap();
    let regex = Regex::new(BEDROCK_REGEX).unwrap();

    let new_bedrock_version = regex.captures(&html).unwrap().get(1).unwrap().as_str();

    let versions_file_path = "deploy/versions.json";
    let versions_file_content = fs::read_to_string(versions_file_path).await.unwrap();

    let java_version_path = "minecraft-server/src/java-version";
    let old_java_version = fs::read_to_string(java_version_path).await.unwrap();

    let bedrock_version_path = "minecraft-server/src/bedrock-version";
    let old_bedrock_version = fs::read_to_string(bedrock_version_path).await.unwrap();

    let mut config: Config = serde_json::from_str(&versions_file_content).unwrap();

    let exists_new_bedrock_version = old_bedrock_version != new_bedrock_version;
    let exists_new_java_version = old_java_version != manifest.latest.release;

    if !exists_new_java_version && !exists_new_bedrock_version {
        println!("No new versions found");
        return;
    }

    let mut version = semver::Version::parse(&config.minecraft_server_rs).unwrap();
    version.patch += 1;
    let tag_name = version.to_string();
    config.minecraft_server_rs = tag_name.clone();

    let mut output = Vec::new();
    let formatter = PrettyFormatter::with_indent(b"    ");
    let mut serializer = serde_json::Serializer::with_formatter(&mut output, formatter);
    config.serialize(&mut serializer).unwrap();

    let mut file = fs::File::create(versions_file_path).await.unwrap();
    file.write_all(&output).await.unwrap();

    let mut file2 = fs::File::create(java_version_path).await.unwrap();
    file2
        .write_all(manifest.latest.release.as_bytes())
        .await
        .unwrap();

    let mut file3 = fs::File::create(bedrock_version_path).await.unwrap();
    file3
        .write_all(new_bedrock_version.to_string().as_bytes())
        .await
        .unwrap();

    // Create a Handlebars registry
    let mut handlebars = Handlebars::new();

    let sub_tag = match (exists_new_java_version, exists_new_bedrock_version) {
        (true, true) => "Bedrock and Java",
        (true, false) => "Java",
        (false, true) => "Bedrock",
        (false, false) => panic!("unreachable"),
    };

    // it's a little sus to use raw unescaped strings here because we load these version from an external source
    // the risk of Mojang injecting something dangerous is low and likely GitLab will escape things anyway
    let tagline = format!("Mojang version bump for {sub_tag}");
    let bedrock_changes = match exists_new_bedrock_version {
        true => format!("Bump version from `{old_bedrock_version}` to `{new_bedrock_version}`"),
        false => "None".to_string(),
    };
    let java_changes = match exists_new_java_version {
        true => format!(
            "Bump version from `{old_java_version}` to `{}`",
            manifest.latest.release
        ),
        false => "None".to_string(),
    };

    // Generate release text
    let template_content = fs::read_to_string("deploy/release.md.hbs").await.unwrap();
    handlebars
        .register_template_string("release", template_content)
        .unwrap();
    let mut data: HashMap<&str, &str> = HashMap::new();
    data.insert("version", &tag_name);
    data.insert("tagline", &tagline);
    data.insert("bedrock_version", new_bedrock_version);
    data.insert("bedrock_changes", &bedrock_changes);
    data.insert("java_version", &manifest.latest.release);
    data.insert("java_changes", &java_changes);
    let release_text = handlebars.render("release", &data).unwrap();

    // Update spec file
    let template_content = fs::read_to_string("deploy/minecraft-server.spec.hbs")
        .await
        .unwrap();
    handlebars
        .register_template_string("spec", template_content)
        .unwrap();
    let mut data = HashMap::new();
    data.insert("version", &tag_name);
    let result = handlebars.render("spec", &data).unwrap();
    fs::write("minecraft-server.spec", result).await.unwrap();

    // Update tito version
    let template_content = fs::read_to_string("deploy/tito/minecraft-server.hbs")
        .await
        .unwrap();
    handlebars
        .register_template_string("tito", template_content)
        .unwrap();
    let mut data: HashMap<&str, &String> = HashMap::new();
    data.insert("version", &tag_name);
    let result = handlebars.render("tito", &data).unwrap();
    fs::write(".tito/packages/minecraft-server", result)
        .await
        .unwrap();

    // Update Cargo.toml
    let path = "minecraft-server/Cargo.toml";
    let contents = fs::read_to_string(path)
        .await
        .expect("Unable to read Cargo.toml");
    let mut doc = contents
        .parse::<DocumentMut>()
        .expect("Invalid TOML format");
    doc["package"]["version"] = toml_edit::value(&tag_name);
    fs::write(path, doc.to_string())
        .await
        .expect("Unable to write to Cargo.toml");

    // run `cargo update` to update the Cargo.lock file
    let cargo_update_output = Command::new("cargo")
        .args(["update"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to run cargo update");

    if !cargo_update_output.status.success() {
        eprintln!("Failed to run cargo update: {:?}", cargo_update_output);
        return;
    }

    // Set git configuration
    let git_config_email_output = Command::new("git")
        .args(["config", "user.email", "git@seaoflaurels.com"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to set git user.email");

    if !git_config_email_output.status.success() {
        eprintln!(
            "Failed to set git user.email: {:?}",
            git_config_email_output
        );
        return;
    }

    let git_config_name_output = Command::new("git")
        .args(["config", "user.name", "Kai DeLorenzo"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to set git user.name");

    if !git_config_name_output.status.success() {
        eprintln!("Failed to set git user.name: {:?}", git_config_name_output);
        return;
    }

    // Commit the changes
    let git_checkout_output = Command::new("git")
        .args(["checkout", "main"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to execute git checkout command");

    if !git_checkout_output.status.success() {
        eprintln!("Failed to checkout main: {:?}", git_checkout_output);
        return;
    }

    let git_add_output = Command::new("git")
        .args(["add", "."])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to execute git add command");

    if !git_add_output.status.success() {
        eprintln!("Failed to add changes: {:?}", git_add_output);
        return;
    }

    let git_commit_output = Command::new("git")
        .args([
            "commit",
            "-m",
            &format!("Update version files for release {tag_name} (pipeline job)"),
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to execute git commit command");

    if !git_commit_output.status.success() {
        eprintln!("Failed to commit changes: {:?}", git_commit_output);
        return;
    }

    let remote_url =
        format!("https://oauth2:{gitlab_token}@gitlab.com/kaidelorenzo/{PROJECT_SLUG}.git");

    let git_push_output = Command::new("git")
        .args(["push", &remote_url, "main"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .expect("Failed to execute git push command");

    if !git_push_output.status.success() {
        eprintln!("Failed to push changes: {:?}", git_push_output);
        return;
    }

    // Create a new release on GitLab
    let release_name = format!("minecraft-server {tag_name}");

    let client = Client::new();
    let release_url = format!("https://gitlab.com/api/v4/projects/{PROJECT_ID}/releases");

    let release_data = serde_json::json!({
        "name": release_name,
        "tag_name": tag_name,
        "ref": "main",
        "description": release_text,
        "assets": {
            "links": [
                {
                    "name": "Fedora and EPEL Copr",
                    "url": "https://copr.fedorainfracloud.org/coprs/kaidelorenzo/minecraft-server"
                }
            ]
        }
    });

    let response = client
        .post(&release_url)
        .bearer_auth(gitlab_token)
        .json(&release_data)
        .send()
        .await
        .unwrap();

    if response.status() != StatusCode::OK {
        let body = response.text().await.unwrap();
        println!("{body}");
    }
}
