//! Windows NSIS installer creation.
//!
//! Creates lightweight, fast Windows installers using NSIS (Nullsoft Scriptable Install System).
//! Supports Modern UI, multiple architectures, compression algorithms, and install modes.

use super::sign;
use crate::bundler::{
    error::{Context, Error, ErrorExt, Result},
    settings::{Arch, NSISInstallerMode, NsisCompression, Settings},
};

#[cfg(windows)]
use crate::bundler::utils::http;
use handlebars::Handlebars;
use std::{
    collections::BTreeMap,
    io::Write,
    path::{Path, PathBuf},
};
use tokio::io::AsyncWriteExt;

// NSIS download constants (Windows only)
#[cfg(windows)]
const NSIS_URL: &str =
    "https://github.com/tauri-apps/binary-releases/releases/download/nsis-3/nsis-3.zip";
#[cfg(windows)]
const NSIS_SHA1: &str = "057e83c7d82462ec394af76c87d06733605543d4";

// NSI script template
const NSI_TEMPLATE: &str = r#"
; NSIS Installer Script
; Generated by kodegen_release

!define PRODUCT_NAME "{{product_name}}"
!define PRODUCT_VERSION "{{version}}"
!define PRODUCT_PUBLISHER "{{publisher}}"
!define INSTALL_DIR "{{install_dir}}"
!define BINARY_NAME "{{binary_name}}"
!define ARCH "{{arch}}"

; Compression
!if "{{compression}}" == "none"
  SetCompress off
!else
  SetCompressor /SOLID "{{compression}}"
!endif

; Modern UI
!include "MUI2.nsh"
!include "x64.nsh"
!include "FileFunc.nsh"

; Custom Branding Images
{{#if header_image}}
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "{{header_image}}"
{{/if}}

{{#if sidebar_image}}
!define MUI_WELCOMEFINISHPAGE_BITMAP "{{sidebar_image}}"
{{/if}}

{{#if installer_icon}}
!define MUI_ICON "{{installer_icon}}"
{{/if}}

; General Settings
Name "${PRODUCT_NAME}"
OutFile "${OUTPUT_FILE}"
InstallDir "${INSTALL_DIR}"

; Request admin for perMachine, user for currentUser
!if "{{install_mode}}" == "perMachine"
  RequestExecutionLevel admin
!else if "{{install_mode}}" == "currentUser"
  RequestExecutionLevel user
!else
  RequestExecutionLevel admin
!endif

; Interface Settings
!define MUI_ABORTWARNING
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"

; Pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

; Uninstaller Pages
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES

; Languages
!insertmacro MUI_LANGUAGE "English"

; Version Info
VIProductVersion "{{version_nsis}}"
VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}"
VIAddVersionKey "CompanyName" "${PRODUCT_PUBLISHER}"
VIAddVersionKey "FileDescription" "${PRODUCT_NAME} Installer"
VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}"

; Installation Section
Section "MainSection" SEC01
  SetOutPath "$INSTDIR"
  
  ; Copy all binaries
  File "{{kodegen_install_path}}"
  File "{{kodegen_path}}"
  File "{{kodegend_path}}"
  
  ; Run installer wizard with GUI (BLOCKS until wizard completes)
  DetailPrint "Launching installation wizard..."
  ExecWait '"$INSTDIR\kodegen_install.exe" --from-platform nsis --gui'
  
  ; Create uninstaller
  WriteUninstaller "$INSTDIR\Uninstall.exe"
  
  ; Create Start Menu shortcuts
  CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
  CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" \
    "$INSTDIR\${BINARY_NAME}.exe"
  CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" \
    "$INSTDIR\Uninstall.exe"
  
  ; Write registry uninstall information
  !if "{{install_mode}}" == "perMachine"
    !define UNINST_ROOT "HKLM"
  !else
    !define UNINST_ROOT "HKCU"
  !endif
  
  WriteRegStr ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "DisplayName" "${PRODUCT_NAME}"
  WriteRegStr ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "DisplayVersion" "${PRODUCT_VERSION}"
  WriteRegStr ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "Publisher" "${PRODUCT_PUBLISHER}"
  WriteRegStr ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "UninstallString" "$INSTDIR\Uninstall.exe"
  WriteRegStr ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "InstallLocation" "$INSTDIR"
  
  ; Estimated size in KB
  ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
  IntFmt $0 "0x%08X" $0
  WriteRegDWORD ${UNINST_ROOT} \
    "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
    "EstimatedSize" "$0"
SectionEnd

; Uninstaller Section
Section "Uninstall"
  ; Remove all installed binaries
  Delete "$INSTDIR\kodegen_install.exe"
  Delete "$INSTDIR\kodegen.exe"
  Delete "$INSTDIR\kodegend.exe"
  Delete "$INSTDIR\Uninstall.exe"
  
  ; Remove Start Menu shortcuts
  Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
  Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
  RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
  
  ; Remove registry keys
  !if "{{install_mode}}" == "perMachine"
    DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
  !else
    DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
  !endif
  
  ; Remove installation directory
  RMDir "$INSTDIR"
SectionEnd
"#;

/// Bundle project as NSIS installer.
///
/// Creates a Windows .exe installer with Modern UI wizard interface.
/// Downloads NSIS toolset on Windows, uses system makensis on Linux/macOS.
pub async fn bundle_project(settings: &Settings) -> Result<Vec<PathBuf>> {
    log::info!("Building NSIS installer for {}", settings.product_name());

    // Get NSIS toolset
    let nsis_path = get_nsis_toolset().await?;

    // Map architecture
    let arch = map_arch(settings.binary_arch())?;

    // Create output directory
    let output_dir = settings
        .project_out_directory()
        .join("bundle/nsis")
        .join(arch);
    tokio::fs::create_dir_all(&output_dir).await
        .fs_context("creating NSIS output directory", &output_dir)?;

    // Generate NSI script
    let nsi_path = generate_nsi_script(settings, arch, &output_dir).await?;

    // Create installer name
    let installer_name = format!(
        "{}_{}_{}-setup.exe",
        settings.product_name(),
        settings.version_string(),
        arch
    );
    let installer_path = settings
        .project_out_directory()
        .join("bundle/nsis")
        .join(&installer_name);

    // Run makensis
    run_makensis(&nsis_path, &nsi_path, &installer_path).await?;

    // Sign the installer if configured
    if sign::should_sign(settings) {
        sign::sign_file(&installer_path, settings).await.context("signing NSIS installer")?;
    }

    log::info!("✓ Created NSIS installer: {}", installer_path.display());

    Ok(vec![installer_path])
}

/// Get or download NSIS toolset.
///
/// On Windows: Downloads NSIS from GitHub if not cached.
/// On Linux/macOS: Locates system-installed makensis.
async fn get_nsis_toolset() -> Result<PathBuf> {
    #[cfg(windows)]
    {
        // Determine cache directory
        let tools_dir = dirs::cache_dir()
            .ok_or_else(|| Error::GenericError("Could not find cache directory".into()))?
            .join("kodegen")
            .join("nsis");

        let nsis_path = tools_dir.join("NSIS");

        // Check if already downloaded
        if nsis_path.exists() && nsis_path.join("makensis.exe").exists() {
            log::debug!("NSIS found at {}", nsis_path.display());
            return Ok(nsis_path);
        }

        // Download NSIS
        log::info!("Downloading NSIS toolset...");

        let data = http::download_and_verify(NSIS_URL, NSIS_SHA1, http::HashAlgorithm::Sha1).await
            .context("failed to download NSIS")?;

        // Extract
        log::info!("Extracting NSIS...");
        tokio::fs::create_dir_all(&tools_dir).await
            .fs_context("creating NSIS tools directory", &tools_dir)?;
        http::extract_zip(&data, &tools_dir).await.context("failed to extract NSIS")?;

        // Rename extracted folder (handle version-specific naming)
        let extracted = tools_dir.join("nsis-3.08");
        if extracted.exists() {
            tokio::fs::rename(&extracted, &nsis_path).await
                .fs_context("renaming NSIS directory", &extracted)?;
        } else {
            // Try to find any nsis-* folder
            let mut entries =
                tokio::fs::read_dir(&tools_dir).await.fs_context("reading tools directory", &tools_dir)?;

            let mut found = false;
            while let Some(entry) = entries.next_entry().await.fs_context("reading directory entry", &tools_dir)? {
                let name = entry.file_name();
                if name.to_string_lossy().starts_with("nsis") {
                    tokio::fs::rename(entry.path(), &nsis_path).await
                        .fs_context("renaming NSIS directory", &entry.path())?;
                    found = true;
                    break;
                }
            }

            if !found {
                return Err(Error::GenericError(
                    "NSIS extraction failed: no nsis folder found in archive".into(),
                ));
            }
        }

        // Verify makensis.exe exists
        if !nsis_path.join("makensis.exe").exists() {
            return Err(Error::GenericError(format!(
                "NSIS installation incomplete: makensis.exe not found at {}",
                nsis_path.display()
            )));
        }

        Ok(nsis_path)
    }

    #[cfg(not(windows))]
    {
        // On Linux/macOS, find system-installed makensis
        match which::which("makensis") {
            Ok(path) => {
                let bin_dir = path.parent().ok_or_else(|| {
                    Error::GenericError("makensis path has no parent directory".into())
                })?;
                Ok(bin_dir.to_path_buf())
            }
            Err(_) => Err(Error::GenericError(
                "makensis not found. Please install NSIS (e.g., apt-get install nsis)".into(),
            )),
        }
    }
}

/// Format version string for NSIS VIProductVersion.
/// Ensures exactly 4 numeric parts (major.minor.patch.build).
fn format_version_for_nsis(version: &str) -> Result<String> {
    // Parse version components
    let parts: Vec<&str> = version.split('.').collect();

    match parts.len() {
        1 => Ok(format!("{}.0.0.0", parts[0])),
        2 => Ok(format!("{}.{}.0.0", parts[0], parts[1])),
        3 => Ok(format!("{}.{}.{}.0", parts[0], parts[1], parts[2])),
        4 => Ok(version.to_string()),
        _ => {
            // More than 4 parts, take first 4
            Ok(format!(
                "{}.{}.{}.{}",
                parts[0], parts[1], parts[2], parts[3]
            ))
        }
    }
}

/// Generate NSI installer script from template.
///
/// Uses handlebars to render template with settings data.
/// Writes output with UTF-8 BOM required by NSIS.
async fn generate_nsi_script(settings: &Settings, arch: &str, output_dir: &Path) -> Result<PathBuf> {
    let mut handlebars = Handlebars::new();
    handlebars.register_escape_fn(handlebars::no_escape);

    let mut data = BTreeMap::new();

    // Basic metadata
    data.insert("product_name", settings.product_name().to_string());
    data.insert("version", settings.version_string().to_string());

    // Format version for NSIS VIProductVersion (requires exactly 4 parts)
    let version_nsis = format_version_for_nsis(settings.version_string())?;
    data.insert("version_nsis", version_nsis);

    data.insert("arch", arch.to_string());

    // Publisher/manufacturer
    let publisher = settings
        .bundle_settings()
        .publisher
        .as_deref()
        .unwrap_or("Unknown Publisher");
    data.insert("publisher", publisher.to_string());

    // Get all three required binaries
    let binaries = settings.binaries();

    // Find kodegen_install binary
    let kodegen_install = binaries
        .iter()
        .find(|b| b.name() == "kodegen_install")
        .ok_or_else(|| Error::GenericError("kodegen_install binary not found".into()))?;

    // Find kodegen binary (main MCP server)
    let kodegen = binaries
        .iter()
        .find(|b| b.name() == "kodegen")
        .ok_or_else(|| Error::GenericError("kodegen binary not found".into()))?;

    // Find kodegend binary (daemon)
    let kodegend = binaries
        .iter()
        .find(|b| b.name() == "kodegend")
        .ok_or_else(|| Error::GenericError("kodegend binary not found".into()))?;

    // Insert binary paths for template
    data.insert(
        "kodegen_install_path",
        settings.binary_path(kodegen_install).display().to_string(),
    );
    data.insert(
        "kodegen_path",
        settings.binary_path(kodegen).display().to_string(),
    );
    data.insert(
        "kodegend_path",
        settings.binary_path(kodegend).display().to_string(),
    );

    // Keep binary_name for backward compatibility with other template sections
    data.insert("binary_name", "kodegen".to_string());

    // Install directory based on install mode
    let install_dir = match settings.bundle_settings().windows.nsis.install_mode {
        NSISInstallerMode::PerMachine => {
            format!("$PROGRAMFILES64\\{}", settings.product_name())
        }
        NSISInstallerMode::CurrentUser => format!("$LOCALAPPDATA\\{}", settings.product_name()),
        NSISInstallerMode::Both => format!("$PROGRAMFILES64\\{}", settings.product_name()),
    };
    data.insert("install_dir", install_dir);

    // Installer settings
    data.insert(
        "install_mode",
        map_install_mode(settings.bundle_settings().windows.nsis.install_mode).to_string(),
    );
    data.insert(
        "compression",
        map_compression(settings.bundle_settings().windows.nsis.compression).to_string(),
    );

    // Custom branding images
    let nsis_settings = &settings.bundle_settings().windows.nsis;

    if let Some(header) = &nsis_settings.header_image {
        data.insert("header_image", header.display().to_string());
    }

    if let Some(sidebar) = &nsis_settings.sidebar_image {
        data.insert("sidebar_image", sidebar.display().to_string());
    }

    if let Some(icon) = &nsis_settings.installer_icon {
        data.insert("installer_icon", icon.display().to_string());
    }

    // Render template
    handlebars
        .register_template_string("installer.nsi", NSI_TEMPLATE)
        .map_err(|e| Error::GenericError(format!("failed to register NSI template: {}", e)))?;

    let nsi_content = handlebars
        .render("installer.nsi", &data)
        .map_err(|e| Error::GenericError(format!("failed to render NSI template: {}", e)))?;

    // Write with UTF-8 BOM
    let nsi_path = output_dir.join("installer.nsi");
    write_utf8_bom(&nsi_path, &nsi_content).await?;

    Ok(nsi_path)
}

/// Run makensis to compile NSI script into installer executable.
async fn run_makensis(nsis_path: &Path, nsi_path: &Path, output_path: &Path) -> Result<()> {
    log::info!("Running makensis...");

    let makensis = if cfg!(windows) {
        nsis_path.join("makensis.exe")
    } else {
        PathBuf::from("makensis")
    };

    // Ensure output directory exists
    if let Some(parent) = output_path.parent() {
        tokio::fs::create_dir_all(parent).await
            .fs_context("creating installer output directory", parent)?;
    }

    // Run makensis
    let status = tokio::process::Command::new(&makensis)
        .args([
            "-V3", // Verbosity level 3
            "-INPUTCHARSET",
            "UTF8",
            "-OUTPUTCHARSET",
            "UTF8",
            &format!("-DOUTPUT_FILE={}", output_path.display()),
            nsi_path
                .to_str()
                .ok_or_else(|| Error::GenericError("NSI path is not valid UTF-8".into()))?,
        ])
        .status()
        .await
        .map_err(|e| Error::CommandFailed {
            command: "makensis".to_string(),
            error: e,
        })?;

    if !status.success() {
        return Err(Error::GenericError("makensis compilation failed".into()));
    }

    Ok(())
}

/// Write file with UTF-8 BOM (required by NSIS).
async fn write_utf8_bom(path: &Path, content: &str) -> Result<()> {
    let mut file = tokio::fs::File::create(path).await.fs_context("creating NSI script file", path)?;

    // Write UTF-8 BOM: EF BB BF
    file.write_all(&[0xEF, 0xBB, 0xBF]).await
        .fs_context("writing UTF-8 BOM", path)?;
    file.write_all(content.as_bytes()).await
        .fs_context("writing NSI content", path)?;
    file.flush().await
        .fs_context("flushing NSI file", path)?;

    Ok(())
}

/// Map architecture to NSIS arch string.
fn map_arch(arch: Arch) -> Result<&'static str> {
    match arch {
        Arch::X86_64 => Ok("x64"),
        Arch::X86 => Ok("x86"),
        Arch::AArch64 => Ok("arm64"),
        _ => Err(Error::ArchError(format!(
            "Unsupported architecture for NSIS: {:?}",
            arch
        ))),
    }
}

/// Map compression setting to NSIS compression string.
fn map_compression(compression: Option<NsisCompression>) -> &'static str {
    match compression.unwrap_or(NsisCompression::Lzma) {
        NsisCompression::None => "none",
        NsisCompression::Zlib => "zlib",
        NsisCompression::Bzip2 => "bzip2",
        NsisCompression::Lzma => "lzma",
    }
}

/// Map install mode to NSIS mode string.
fn map_install_mode(mode: NSISInstallerMode) -> &'static str {
    match mode {
        NSISInstallerMode::CurrentUser => "currentUser",
        NSISInstallerMode::PerMachine => "perMachine",
        NSISInstallerMode::Both => "both",
    }
}
