use crate::utils::command::{CommandExt, create_command};
use anyhow::{Context, Result, bail};
use clap::ValueEnum;
use semver::Version;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use toml_edit::{DocumentMut, value};

const DEFAULT_REPO: &str = "CQMHV/ALCOMD3";
const DEFAULT_SITE_BASE_URL: &str = "https://alcomd3.cqmhv.com";
const DEFAULT_TARGET: &str = "x86_64-pc-windows-msvc";

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum ReleaseChannel {
    Stable,
    Beta,
}

impl ReleaseChannel {
    pub fn is_prerelease(self) -> bool {
        matches!(self, Self::Beta)
    }

    pub fn updater_json_relative(self) -> &'static str {
        match self {
            Self::Stable => "website/public/api/gui/tauri-updater.json",
            Self::Beta => "website/public/api/gui/tauri-updater-beta.json",
        }
    }

    pub fn updater_endpoint(self, site_base_url: &str) -> String {
        let suffix = match self {
            Self::Stable => "/api/gui/tauri-updater.json",
            Self::Beta => "/api/gui/tauri-updater-beta.json",
        };
        format!("{}{}", site_base_url.trim_end_matches('/'), suffix)
    }
}

impl fmt::Display for ReleaseChannel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Stable => f.write_str("stable"),
            Self::Beta => f.write_str("beta"),
        }
    }
}

#[derive(Clone, Debug)]
pub struct ReleaseContext {
    pub version: String,
    pub channel: ReleaseChannel,
    pub repo: String,
    pub site_base_url: String,
    pub target: String,
    pub workspace_root: PathBuf,
    pub setup_dir: PathBuf,
    pub installer_name: String,
    pub signature_name: String,
    pub zip_name: String,
    pub installer: PathBuf,
    pub signature: PathBuf,
    pub zip: PathBuf,
    pub tag: String,
    pub release_notes: PathBuf,
    pub updater_json: PathBuf,
    pub updater_endpoint: String,
}

impl ReleaseContext {
    pub fn new(
        version: impl Into<String>,
        channel: ReleaseChannel,
        repo: Option<String>,
        site_base_url: Option<String>,
        target: Option<String>,
    ) -> Result<Self> {
        let version = version.into();
        validate_version_for_channel(&version, channel)?;

        let metadata = crate::utils::cargo::cargo_metadata();
        let workspace_root = metadata.workspace_root.as_std_path().to_path_buf();

        let repo = repo.unwrap_or_else(|| DEFAULT_REPO.to_string());
        let site_base_url = site_base_url.unwrap_or_else(|| DEFAULT_SITE_BASE_URL.to_string());
        let target = target.unwrap_or_else(|| DEFAULT_TARGET.to_string());

        let setup_dir = workspace_root
            .join("target")
            .join(&target)
            .join("release")
            .join("bundle")
            .join("setup");
        let installer_name = format!("alcomd3-{version}-setup.exe");
        let signature_name = format!("{installer_name}.sig");
        let zip_name = format!("{installer_name}.zip");
        let installer = setup_dir.join(&installer_name);
        let signature = setup_dir.join(&signature_name);
        let zip = setup_dir.join(&zip_name);
        let tag = format!("v{version}");
        let release_notes = workspace_root
            .join("release-notes")
            .join(format!("ALCOMD3_{version}.md"));
        let updater_json = workspace_root.join(channel.updater_json_relative());
        let updater_endpoint = channel.updater_endpoint(&site_base_url);

        Ok(Self {
            version,
            channel,
            repo,
            site_base_url,
            target,
            workspace_root,
            setup_dir,
            installer_name,
            signature_name,
            zip_name,
            installer,
            signature,
            zip,
            tag,
            release_notes,
            updater_json,
            updater_endpoint,
        })
    }

    pub fn artifact_dir(&self) -> PathBuf {
        self.workspace_root
            .join("artifacts")
            .join("release")
            .join(&self.tag)
    }

    pub fn release_check_dir(&self) -> PathBuf {
        self.workspace_root
            .join("artifacts")
            .join("release-check")
            .join(&self.tag)
    }

    pub fn artifact_installer(&self) -> PathBuf {
        self.artifact_dir().join(&self.installer_name)
    }

    pub fn artifact_signature(&self) -> PathBuf {
        self.artifact_dir().join(&self.signature_name)
    }

    pub fn artifact_zip(&self) -> PathBuf {
        self.artifact_dir().join(&self.zip_name)
    }
}

pub struct CmdRunner {
    dry_run: bool,
}

impl CmdRunner {
    pub fn new(dry_run: bool) -> Self {
        Self { dry_run }
    }

    pub fn dry_run(&self) -> bool {
        self.dry_run
    }

    pub fn run(&self, mut cmd: ProcessCommand, what: &str) -> Result<()> {
        println!("$ {}", cmd.display_command());
        if self.dry_run {
            return Ok(());
        }
        cmd.run_checked(what)
    }

    pub fn capture(&self, mut cmd: ProcessCommand, what: &str) -> Result<String> {
        println!("$ {}", cmd.display_command());
        if self.dry_run {
            return Ok(String::new());
        }
        cmd.run_capture_checked(what)
    }
}

pub fn default_repo() -> String {
    DEFAULT_REPO.to_string()
}

pub fn default_site_base_url() -> String {
    DEFAULT_SITE_BASE_URL.to_string()
}

pub fn default_target() -> String {
    DEFAULT_TARGET.to_string()
}

pub fn validate_version_for_channel(version: &str, channel: ReleaseChannel) -> Result<()> {
    let parsed = Version::parse(version).with_context(|| format!("invalid SemVer: {version}"))?;

    match channel {
        ReleaseChannel::Stable if !parsed.pre.is_empty() => {
            bail!("stable release version must not contain prerelease metadata: {version}")
        }
        ReleaseChannel::Beta if parsed.pre.is_empty() => {
            bail!("beta release version must contain prerelease metadata: {version}")
        }
        _ => Ok(()),
    }
}

pub fn cargo_xtask() -> ProcessCommand {
    ProcessCommand::new(std::env::current_exe().expect("failed to locate current xtask executable"))
}

pub fn cargo() -> ProcessCommand {
    create_command("cargo")
}

pub fn npm() -> ProcessCommand {
    create_command("npm")
}

pub fn git() -> ProcessCommand {
    create_command("git")
}

pub fn gh() -> ProcessCommand {
    create_command("gh")
}

pub fn check_worktree_clean(ctx: &ReleaseContext) -> Result<()> {
    let mut cmd = git();
    cmd.arg("status")
        .arg("--short")
        .current_dir(&ctx.workspace_root);
    let output = cmd.run_capture_checked("checking git status")?;
    if !output.trim().is_empty() {
        bail!("worktree is not clean:\n{output}");
    }
    Ok(())
}

pub fn update_workspace_version(ctx: &ReleaseContext, dry_run: bool) -> Result<()> {
    let cargo_toml = ctx.workspace_root.join("Cargo.toml");
    let source = fs::read_to_string(&cargo_toml)
        .with_context(|| format!("reading {}", cargo_toml.display()))?;
    let mut doc = source
        .parse::<DocumentMut>()
        .with_context(|| format!("parsing {}", cargo_toml.display()))?;
    doc["workspace"]["package"]["version"] = value(&ctx.version);
    let rendered = doc.to_string();

    if source == rendered {
        println!("version unchanged: {}", cargo_toml.display());
        return Ok(());
    }

    println!(
        "update workspace package version: {} -> {}",
        cargo_toml.display(),
        ctx.version
    );
    if !dry_run {
        fs::write(&cargo_toml, rendered)
            .with_context(|| format!("writing {}", cargo_toml.display()))?;
    }
    Ok(())
}

pub fn create_release_notes_if_missing(ctx: &ReleaseContext, dry_run: bool) -> Result<()> {
    if ctx.release_notes.exists() {
        println!(
            "release notes already exist: {}",
            ctx.release_notes.display()
        );
        return Ok(());
    }

    println!("create release notes: {}", ctx.release_notes.display());
    if dry_run {
        return Ok(());
    }

    let parent = ctx
        .release_notes
        .parent()
        .context("release notes path has no parent")?;
    fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
    fs::write(
        &ctx.release_notes,
        format!(
            "# ALCOMD3 v{}\n\n## Changes\n\n- Replace this line with the user-facing release summary.\n\n## Notes\n\n- Add migration, compatibility, or known-issue notes here. Remove this section if there are none.\n",
            ctx.version
        ),
    )
    .with_context(|| format!("writing {}", ctx.release_notes.display()))?;
    Ok(())
}

pub fn ensure_release_notes_ready(ctx: &ReleaseContext) -> Result<()> {
    let notes = fs::read_to_string(&ctx.release_notes)
        .with_context(|| format!("reading release notes: {}", ctx.release_notes.display()))?;
    for placeholder in [
        "Replace this line with the user-facing release summary.",
        "Add migration, compatibility, or known-issue notes here.",
    ] {
        if notes.contains(placeholder) {
            bail!("release notes still contain placeholder text: {placeholder}");
        }
    }
    Ok(())
}

pub fn ensure_path_exists(path: &Path, what: &str) -> Result<()> {
    if !path.exists() {
        bail!("{what} does not exist: {}", path.display());
    }
    Ok(())
}

pub fn copy_release_artifacts(ctx: &ReleaseContext, dry_run: bool) -> Result<()> {
    let artifact_dir = ctx.artifact_dir();
    println!("copy release artifacts to {}", artifact_dir.display());
    if dry_run {
        return Ok(());
    }

    fs::create_dir_all(&artifact_dir)
        .with_context(|| format!("creating {}", artifact_dir.display()))?;
    fs::copy(&ctx.installer, ctx.artifact_installer())
        .with_context(|| format!("copying {}", ctx.installer.display()))?;
    fs::copy(&ctx.signature, ctx.artifact_signature())
        .with_context(|| format!("copying {}", ctx.signature.display()))?;
    fs::copy(&ctx.zip, ctx.artifact_zip())
        .with_context(|| format!("copying {}", ctx.zip.display()))?;
    Ok(())
}

pub fn run_sign_installer(
    ctx: &ReleaseContext,
    runner: &CmdRunner,
    key_loader: &Path,
) -> Result<()> {
    if runner.dry_run() {
        let mut cmd = cargo_xtask();
        cmd.arg("sign-alcom-updater").arg(&ctx.installer);
        return runner.run(cmd, "signing updater installer");
    }

    let has_key =
        std::env::var_os("ALCOMD3_UPDATER_PRIVATE_KEY").is_some_and(|value| !value.is_empty());
    let has_password = std::env::var_os("ALCOMD3_UPDATER_PRIVATE_KEY_PASSWORD")
        .is_some_and(|value| !value.is_empty());

    if has_key && has_password {
        let mut cmd = cargo_xtask();
        cmd.arg("sign-alcom-updater").arg(&ctx.installer);
        return runner.run(cmd, "signing updater installer");
    }

    let key_loader = resolve_key_loader(key_loader)?;
    match key_loader
        .extension()
        .and_then(|extension| extension.to_str())
    {
        Some("ps1") => run_sign_installer_with_ps1_loader(ctx, runner, &key_loader),
        Some("env") => run_sign_installer_with_env_loader(ctx, runner, &key_loader),
        _ => bail!("unsupported updater key loader: {}", key_loader.display()),
    }
}

fn run_sign_installer_with_ps1_loader(
    ctx: &ReleaseContext,
    runner: &CmdRunner,
    key_script: &Path,
) -> Result<()> {
    let command = format!(
        ". '{}'; cargo xtask sign-alcom-updater '{}'",
        ps_quote(key_script),
        ps_quote(&ctx.installer),
    );

    let mut cmd = if cfg!(windows) {
        create_command("powershell")
    } else {
        create_command("pwsh")
    };
    cmd.arg("-NoProfile")
        .arg("-ExecutionPolicy")
        .arg("Bypass")
        .arg("-Command")
        .arg(command)
        .current_dir(&ctx.workspace_root);
    runner.run(cmd, "signing updater installer with key loader")
}

fn run_sign_installer_with_env_loader(
    ctx: &ReleaseContext,
    runner: &CmdRunner,
    key_env: &Path,
) -> Result<()> {
    let env = read_key_env_loader(key_env)?;
    let private_key = env
        .get("ALCOMD3_UPDATER_PRIVATE_KEY")
        .context("ALCOMD3_UPDATER_PRIVATE_KEY is missing from updater key loader")?;
    let password = env
        .get("ALCOMD3_UPDATER_PRIVATE_KEY_PASSWORD")
        .context("ALCOMD3_UPDATER_PRIVATE_KEY_PASSWORD is missing from updater key loader")?;

    let mut cmd = cargo_xtask();
    cmd.arg("sign-alcom-updater")
        .arg(&ctx.installer)
        .env("ALCOMD3_UPDATER_PRIVATE_KEY", private_key)
        .env("ALCOMD3_UPDATER_PRIVATE_KEY_PASSWORD", password)
        .current_dir(&ctx.workspace_root);
    runner.run(cmd, "signing updater installer with env key loader")
}

fn resolve_key_loader(key_loader: &Path) -> Result<PathBuf> {
    if key_loader.is_file() {
        return Ok(key_loader.to_path_buf());
    }

    if key_loader.is_dir() {
        for file_name in ["private-key.ps1", "private-key.env"] {
            let candidate = key_loader.join(file_name);
            if candidate.is_file() {
                return Ok(candidate);
            }
        }
    }

    bail!(
        "updater signing variables are not set and key loader does not exist: {}",
        key_loader.display()
    )
}

fn read_key_env_loader(key_env: &Path) -> Result<HashMap<String, String>> {
    let source =
        fs::read_to_string(key_env).with_context(|| format!("reading {}", key_env.display()))?;
    let mut env = HashMap::new();

    for (index, line) in source.lines().enumerate() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        let (name, value) = line.split_once('=').with_context(|| {
            format!(
                "invalid updater key loader line {} in {}",
                index + 1,
                key_env.display()
            )
        })?;
        env.insert(name.trim().to_string(), value.trim().to_string());
    }

    Ok(env)
}

pub fn verify_release_assets(ctx: &ReleaseContext) -> Result<()> {
    ensure_path_exists(&ctx.installer, "installer")?;
    ensure_path_exists(&ctx.signature, "updater signature")?;
    ensure_path_exists(&ctx.zip, "installer zip")?;
    Ok(())
}

pub fn verify_copied_release_assets(ctx: &ReleaseContext) -> Result<()> {
    ensure_path_exists(&ctx.artifact_installer(), "copied installer")?;
    ensure_path_exists(&ctx.artifact_signature(), "copied updater signature")?;
    ensure_path_exists(&ctx.artifact_zip(), "copied installer zip")?;
    Ok(())
}

pub fn verify_github_release(ctx: &ReleaseContext, runner: &CmdRunner) -> Result<()> {
    let mut cmd = gh();
    cmd.arg("release")
        .arg("view")
        .arg(&ctx.tag)
        .arg("--repo")
        .arg(&ctx.repo)
        .arg("--json")
        .arg("tagName,isDraft,isPrerelease,targetCommitish,publishedAt,assets");

    let output = runner.capture(cmd, "viewing GitHub Release")?;
    if runner.dry_run() {
        return Ok(());
    }

    let state: ReleaseState =
        serde_json::from_str(&output).context("parsing GitHub Release JSON")?;
    let expected_assets = [&ctx.installer_name, &ctx.signature_name, &ctx.zip_name];

    for expected in expected_assets {
        if !state.assets.iter().any(|asset| asset.name == *expected) {
            bail!("GitHub Release asset is missing: {expected}");
        }
    }

    if state.is_prerelease != ctx.channel.is_prerelease() {
        bail!(
            "GitHub Release prerelease flag does not match channel {}",
            ctx.channel
        );
    }

    println!("{output}");
    Ok(())
}

pub fn check_public_updater_endpoint(ctx: &ReleaseContext) -> Result<()> {
    let mut response = crate::utils::ureq()
        .get(&ctx.updater_endpoint)
        .call()
        .with_context(|| format!("requesting {}", ctx.updater_endpoint))?;
    let mut body = String::new();
    response
        .body_mut()
        .as_reader()
        .read_to_string(&mut body)
        .with_context(|| format!("reading {}", ctx.updater_endpoint))?;

    let json: serde_json::Value =
        serde_json::from_str(&body).context("parsing public updater JSON")?;
    let version = json
        .get("version")
        .and_then(|value| value.as_str())
        .context("public updater JSON has no string version")?;
    if version != ctx.version {
        bail!(
            "public updater version mismatch: expected {}, got {version}",
            ctx.version
        );
    }

    let url = json
        .pointer("/platforms/windows-x86_64/url")
        .and_then(|value| value.as_str())
        .context("public updater JSON has no platforms.windows-x86_64.url")?;
    if !url.ends_with(&ctx.installer_name) {
        bail!(
            "public updater URL does not end with {}: {url}",
            ctx.installer_name
        );
    }

    println!("public updater endpoint passed: {}", ctx.updater_endpoint);
    Ok(())
}

fn ps_quote(path: &Path) -> String {
    path.as_os_str().to_string_lossy().replace('\'', "''")
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReleaseState {
    is_prerelease: bool,
    assets: Vec<ReleaseAsset>,
}

#[derive(Deserialize)]
struct ReleaseAsset {
    name: String,
}
