use anyhow::{Context, Result, bail};
use base64::Engine;
use minisign::{PublicKeyBox, SignatureBox};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

/// Verifies updater JSON before publishing the website.
#[derive(clap::Parser)]
pub struct Command {
    #[arg(long)]
    installer: PathBuf,
    #[arg(long, default_value = "website/api/gui/tauri-updater.json")]
    json: PathBuf,
    #[arg(long, default_value = "vrc-get-gui/src/updater.rs")]
    updater_rs: PathBuf,
}

impl crate::Command for Command {
    fn run(self) -> Result<i32> {
        verify_alcom_updater_json(&self.installer, &self.json, &self.updater_rs)?;
        println!("Updater JSON verification passed: {}", self.json.display());
        Ok(0)
    }
}

#[derive(Deserialize)]
struct UpdaterJson {
    version: String,
    platforms: HashMap<String, Platform>,
}

#[derive(Deserialize)]
struct Platform {
    signature: String,
    url: String,
}

fn verify_alcom_updater_json(
    installer_path: &PathBuf,
    json_path: &PathBuf,
    updater_rs_path: &PathBuf,
) -> Result<()> {
    let json = fs::read_to_string(json_path)
        .with_context(|| format!("failed to read updater JSON: {}", json_path.display()))?;
    let updater: UpdaterJson = serde_json::from_str(&json)
        .with_context(|| format!("failed to parse updater JSON: {}", json_path.display()))?;
    let platform = updater
        .platforms
        .get("windows-x86_64")
        .context("missing platforms.windows-x86_64")?;

    if platform.signature.contains("REPLACE_WITH") || platform.signature.trim().is_empty() {
        bail!("updater JSON still has a placeholder or empty signature");
    }

    let installer_name = installer_path
        .file_name()
        .context("installer path has no file name")?
        .to_string_lossy();

    if !installer_name.contains(&updater.version) {
        bail!(
            "installer file name does not contain JSON version: installer={installer_name}, version={}",
            updater.version
        );
    }

    if !platform.url.ends_with(installer_name.as_ref()) {
        bail!(
            "updater URL does not end with installer file name: url={}, installer={installer_name}",
            platform.url
        );
    }

    let public_key = read_embedded_public_key(updater_rs_path)?;
    let signature = decode_signature(&platform.signature)?;
    let mut installer = fs::File::open(installer_path)
        .with_context(|| format!("failed to open installer: {}", installer_path.display()))?;

    minisign::verify(&public_key, &signature, &mut installer, true, false, true)
        .context("signature does not verify with the embedded updater public key")?;

    Ok(())
}

fn read_embedded_public_key(updater_rs_path: &PathBuf) -> Result<minisign::PublicKey> {
    let updater_rs = fs::read_to_string(updater_rs_path).with_context(|| {
        format!(
            "failed to read updater source: {}",
            updater_rs_path.display()
        )
    })?;
    let line = updater_rs
        .lines()
        .find(|line| line.contains("static PUBLIC_KEY"))
        .context("failed to find static PUBLIC_KEY in updater.rs")?;
    let public_key_base64 = line
        .split('"')
        .nth(1)
        .context("failed to parse PUBLIC_KEY string literal")?;
    let decoded = base64::engine::general_purpose::STANDARD
        .decode(public_key_base64)
        .context("failed to base64-decode PUBLIC_KEY")?;
    let decoded = String::from_utf8(decoded).context("PUBLIC_KEY is not valid UTF-8")?;
    PublicKeyBox::from_string(&decoded)
        .context("failed to parse PUBLIC_KEY as minisign public key")?
        .into_public_key()
        .context("failed to convert PUBLIC_KEY to minisign public key")
}

fn decode_signature(signature_base64: &str) -> Result<SignatureBox> {
    let decoded = base64::engine::general_purpose::STANDARD
        .decode(signature_base64)
        .context("failed to base64-decode updater signature")?;
    let decoded = String::from_utf8(decoded).context("updater signature is not valid UTF-8")?;
    SignatureBox::from_string(&decoded).context("failed to parse updater signature")
}
