use std::{
    env,
    path::{Path, PathBuf},
};

use crate::{AppMetadata, PackageFormat, Platform};

pub const LINUX_DEB_RUNTIME_DEPENDENCIES: &[&str] = &[
    "libgtk-3-0",
    "libayatana-appindicator3-1",
    "libx11-6",
    "libwayland-client0",
    "libxkbcommon0",
    "libfontconfig1",
    "libfreetype6",
    "libvulkan1",
    "libasound2",
    "xdg-utils",
];

pub const LINUX_RPM_RUNTIME_DEPENDENCIES: &[(&str, &str)] = &[
    ("gtk3", "*"),
    ("libayatana-appindicator-gtk3", "*"),
    ("libX11", "*"),
    ("wayland", "*"),
    ("libxkbcommon", "*"),
    ("fontconfig", "*"),
    ("freetype", "*"),
    ("vulkan-loader", "*"),
    ("alsa-lib", "*"),
    ("xdg-utils", "*"),
];

/// A generated cargo-packager invocation plan for one Liora app.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoPackagerPlan {
    pub app: AppMetadata,
    pub platform: Platform,
    pub formats: Vec<PackageFormat>,
    pub config_path: PathBuf,
    pub out_dir: PathBuf,
    pub binaries_dir: PathBuf,
}

impl CargoPackagerPlan {
    pub fn command_args(&self) -> Vec<String> {
        let mut args = vec![
            "packager".to_string(),
            "--config".to_string(),
            self.config_path.display().to_string(),
            "--out-dir".to_string(),
            self.out_dir.display().to_string(),
            "--binaries-dir".to_string(),
            self.binaries_dir.display().to_string(),
        ];

        let cargo_formats = self
            .formats
            .iter()
            .filter_map(|format| format.cargo_packager_format())
            .collect::<Vec<_>>()
            .join(",");
        if !cargo_formats.is_empty() {
            args.push("--formats".to_string());
            args.push(cargo_formats);
        }
        args
    }
}

/// Returns the subset of formats handled by cargo-packager directly.
pub fn cargo_packager_formats(formats: &[PackageFormat]) -> Vec<PackageFormat> {
    formats
        .iter()
        .copied()
        .filter(|format| format.cargo_packager_format().is_some())
        .collect()
}

/// Returns the subset of formats that require Liora supplemental backends.
pub fn supplemental_formats(formats: &[PackageFormat]) -> Vec<PackageFormat> {
    formats
        .iter()
        .copied()
        .filter(|format| format.cargo_packager_format().is_none())
        .collect()
}

pub fn generated_config_path(root: &Path, app: &AppMetadata) -> PathBuf {
    root.join("target")
        .join("liora-packager")
        .join(format!("Packager.{}.toml", app.app.key()))
}

pub fn package_out_dir(root: &Path, app: &AppMetadata, platform: Platform) -> PathBuf {
    root.join("target")
        .join("packages")
        .join(&app.package)
        .join(platform.as_str())
}

pub fn release_binaries_dir(root: &Path) -> PathBuf {
    root.join("target").join("release")
}

pub fn generated_rpm_config_path(root: &Path, app: &AppMetadata) -> PathBuf {
    root.join("target")
        .join("liora-packager")
        .join(format!("GenerateRpm.{}.toml", app.app.key()))
}

pub fn render_generate_rpm_config(root: &Path, app: &AppMetadata) -> String {
    let mut out = String::new();
    line(
        &mut out,
        "# Generated by cargo xtask package. Do not edit by hand.",
    );
    line(&mut out, "[package.metadata.generate-rpm]");
    kv(&mut out, "name", &app.package);
    kv(&mut out, "version", &rpm_package_version());
    kv(&mut out, "license", "LicenseRef-Liora");
    kv(&mut out, "summary", &app.short_description);
    kv(&mut out, "url", "https://github.com/yhyzgn/liora");
    kv(&mut out, "vendor", "Liora");
    kv(&mut out, "release", "1");
    kv(&mut out, "auto-req", "builtin");
    line(&mut out, "require-sh = false");

    line(&mut out, "assets = [");
    rpm_asset(
        &mut out,
        &root.join("target/release").join(&app.binary),
        &format!("/usr/bin/{}", app.binary),
        "755",
    );
    rpm_asset(
        &mut out,
        &app.linux_desktop_path(root),
        &format!("/usr/share/applications/{}.desktop", app.binary),
        "644",
    );
    rpm_asset(
        &mut out,
        &app.linux_metainfo_path(root),
        &format!("/usr/share/metainfo/{}.metainfo.xml", app.binary),
        "644",
    );
    rpm_asset(
        &mut out,
        &root
            .join("packaging/icons")
            .join(format!("{}.png", app.icon_stem)),
        &format!("/usr/share/icons/hicolor/512x512/apps/{}.png", app.binary),
        "644",
    );
    rpm_asset(
        &mut out,
        &root
            .join("packaging/icons")
            .join(format!("{}.svg", app.icon_stem)),
        &format!("/usr/share/icons/hicolor/scalable/apps/{}.svg", app.binary),
        "644",
    );
    line(&mut out, "]");

    line(&mut out, "");
    line(&mut out, "[package.metadata.generate-rpm.requires]");
    for (package, version) in LINUX_RPM_RUNTIME_DEPENDENCIES {
        kv(&mut out, package, version);
    }
    out
}

fn rpm_asset(out: &mut String, source: &Path, dest: &str, mode: &str) {
    out.push_str("  { source = \"");
    out.push_str(&escape(&source.display().to_string()));
    out.push_str("\", dest = \"");
    out.push_str(&escape(dest));
    out.push_str("\", mode = \"");
    out.push_str(mode);
    out.push_str("\" },\n");
}

pub fn render_cargo_packager_config(
    root: &Path,
    app: &AppMetadata,
    formats: &[PackageFormat],
    out_dir: &Path,
    binaries_dir: &Path,
) -> String {
    let cargo_formats = cargo_packager_formats(formats)
        .into_iter()
        .filter_map(PackageFormat::cargo_packager_format)
        .collect::<Vec<_>>();

    let mut out = String::new();
    line(
        &mut out,
        "# Generated by cargo xtask package. Do not edit by hand.",
    );
    kv(&mut out, "name", &app.package);
    kv(&mut out, "productName", &app.name);
    kv(&mut out, "version", &package_version());
    kv(&mut out, "identifier", app.id.as_str());
    kv(&mut out, "description", &app.short_description);
    kv(&mut out, "homepage", "https://github.com/yhyzgn/liora");
    arr(&mut out, "authors", &["Liora Contributors"]);
    kv(&mut out, "publisher", "Liora");
    kv(&mut out, "copyright", "Copyright © Liora Contributors");
    kv(&mut out, "category", &app.category);
    arr(&mut out, "formats", &cargo_formats);
    path_kv(&mut out, "outDir", out_dir);
    path_kv(&mut out, "binariesDir", binaries_dir);
    arr(
        &mut out,
        "icons",
        &[
            &abs(root
                .join("packaging/icons")
                .join(format!("{}.png", app.icon_stem))),
            &abs(root
                .join("packaging/icons")
                .join(format!("{}.icns", app.icon_stem))),
            &abs(root
                .join("packaging/icons")
                .join(format!("{}.ico", app.icon_stem))),
        ],
    );

    line(&mut out, "");
    line(&mut out, "[[binaries]]");
    kv(&mut out, "path", &app.binary);
    line(&mut out, "main = true");

    line(&mut out, "");
    line(&mut out, "[linux]");
    line(&mut out, "generateDesktopEntry = true");

    line(&mut out, "");
    line(&mut out, "[deb]");
    path_kv(&mut out, "desktopTemplate", &app.linux_desktop_path(root));
    kv(&mut out, "section", "devel");
    kv(&mut out, "priority", "optional");
    kv(&mut out, "packageName", &app.package);
    arr(&mut out, "depends", LINUX_DEB_RUNTIME_DEPENDENCIES);

    line(&mut out, "");
    line(&mut out, "[macos]");
    path_kv(
        &mut out,
        "entitlements",
        &root.join("packaging/macos/Entitlements.plist"),
    );

    line(&mut out, "");
    line(&mut out, "[nsis]");
    path_kv(
        &mut out,
        "installerIcon",
        &root
            .join("packaging/icons")
            .join(format!("{}.ico", app.icon_stem)),
    );
    line(&mut out, "installMode = \"currentUser\"");

    out
}

fn package_version() -> String {
    env::var("LIORA_PACKAGE_VERSION").unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_string())
}

fn rpm_package_version() -> String {
    // RPM version strings cannot contain hyphens because '-' separates version and release.
    package_version().replace('-', "_")
}

fn abs(path: PathBuf) -> String {
    path.display().to_string()
}

fn line(out: &mut String, value: &str) {
    out.push_str(value);
    out.push('\n');
}

fn kv(out: &mut String, key: &str, value: &str) {
    out.push_str(key);
    out.push_str(" = \"");
    out.push_str(&escape(value));
    out.push_str("\"\n");
}

fn path_kv(out: &mut String, key: &str, value: &Path) {
    kv(out, key, &value.display().to_string());
}

fn arr(out: &mut String, key: &str, values: &[&str]) {
    out.push_str(key);
    out.push_str(" = [");
    for (idx, value) in values.iter().enumerate() {
        if idx > 0 {
            out.push_str(", ");
        }
        out.push('"');
        out.push_str(&escape(value));
        out.push('"');
    }
    out.push_str("]\n");
}

fn escape(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::KnownApp;

    #[test]
    fn renders_cargo_packager_config_with_binary_and_icons() {
        let root = Path::new("/repo/liora");
        let app = KnownApp::Gallery.metadata();
        let text = render_cargo_packager_config(
            root,
            &app,
            &[
                PackageFormat::Deb,
                PackageFormat::Rpm,
                PackageFormat::AppImage,
            ],
            Path::new("/repo/liora/target/packages/liora-gallery/linux"),
            Path::new("/repo/liora/target/release"),
        );
        assert!(text.contains("productName = \"Liora Gallery\""));
        assert!(text.contains("formats = [\"deb\", \"appimage\"]"));
        assert!(text.contains("depends = [\"libgtk-3-0\""));
        assert!(text.contains("\"libayatana-appindicator3-1\""));
        assert!(text.contains("\"libvulkan1\""));
        assert!(text.contains("[[binaries]]"));
        assert!(text.contains("path = \"liora-gallery\""));
        assert!(text.contains("installerIcon"));
    }

    #[test]
    fn renders_generate_rpm_config_with_desktop_and_icons() {
        let root = Path::new("/repo/liora");
        let app = KnownApp::Docs.metadata();
        let text = render_generate_rpm_config(root, &app);
        assert!(text.contains("[package.metadata.generate-rpm]"));
        assert!(text.contains("name = \"liora-docs\""));
        assert!(text.contains("/usr/bin/liora-docs"));
        assert!(text.contains("[package.metadata.generate-rpm.requires]"));
        assert!(text.contains("gtk3 = \"*\""));
        assert!(text.contains("vulkan-loader = \"*\""));
        assert!(text.contains("/usr/share/applications/liora-docs.desktop"));
        assert!(text.contains("/usr/share/icons/hicolor/scalable/apps/liora-docs.svg"));
    }
}

#[cfg(test)]
mod format_tests {
    use super::*;

    #[test]
    fn tar_gz_is_liora_supplemental_not_cargo_packager_pacman() {
        assert_eq!(PackageFormat::TarGz.cargo_packager_format(), None);
        assert_eq!(cargo_packager_formats(&[PackageFormat::TarGz]), Vec::new());
        assert_eq!(
            supplemental_formats(&[PackageFormat::TarGz]),
            vec![PackageFormat::TarGz]
        );
    }
}
