#!/usr/bin/env nu

# Binary packaging tool - packages platform binaries for different architectures
#
# Packages:
# - Cross-compiled binaries for multiple platforms
# - Standalone executable packages
# - Platform-specific installers
# - Binary verification and signing

use std log

def main [
    --source-dir: string = "dist/platform"
    --output-dir: string = "packages/binaries"
    --platforms: string = "linux-amd64,macos-amd64,windows-amd64"
    --format: string = "archive"
    --compress
    --sign
    --verify
    --strip
    --upx
    --verbose
] {

    let source_root = ($source_dir | path expand)
    let output_root = ($output_dir | path expand)

    let packaging_config = {
        source_dir: $source_root
        output_dir: $output_root
        platforms: ($platforms | split row "," | each { str trim })
        format: $format
        compress: $compress
        sign: $sign
        verify: $verify
        strip: $strip
        upx: $upx
        verbose: $verbose
    }

    log info $"Starting binary packaging with config: ($packaging_config)"

    # Validate source directory
    if not ($source_root | path exists) {
        log error $"Source directory does not exist: ($source_root)"
        exit 1
    }

    # Ensure output directory exists
    mkdir $output_root

    # Find available binaries
    let available_binaries = find_available_binaries $source_root $packaging_config

    if ($available_binaries | length) == 0 {
        log warning "No binaries found to package"
        return {
            status: "skipped"
            reason: "no binaries found"
            binaries_processed: 0
        }
    }

    log info $"Found ($available_binaries | length) binaries to package"

    # Package binaries for each platform
    mut packaging_results = []

    for platform in $packaging_config.platforms {
        let platform_result = package_platform_binaries $platform $available_binaries $packaging_config
        $packaging_results = ($packaging_results | append $platform_result)
    }

    let summary = {
        total_platforms: ($packaging_config.platforms | length)
        successful_platforms: ($packaging_results | where status == "success" | length)
        failed_platforms: ($packaging_results | where status == "failed" | length)
        total_packages: ($packaging_results | get packages_created | math sum)
        total_size: ($packaging_results | get total_size | math sum)
        packaging_config: $packaging_config
        results: $packaging_results
    }

    if $summary.failed_platforms > 0 {
        log error $"Binary packaging completed with ($summary.failed_platforms) platform failures"
        exit 1
    } else {
        log info $"Binary packaging completed successfully - ($summary.total_packages) packages created for ($summary.successful_platforms) platforms"
    }

    return $summary
}

# Find available binaries in source directory
def find_available_binaries [
    source_dir: string
    packaging_config: record
] {
    # Find all executable files using glob
    let executables = (glob ($source_dir | path join "**" "*") | where { |path| ($path | path exists) and ((ls $path | get 0.type) == "file") })

    $executables | each {|binary|
        let binary_info = analyze_binary $binary $packaging_config
        {
            path: $binary
            name: ($binary | path basename)
            size: (ls $binary | get 0.size)
            architecture: $binary_info.architecture
            platform: $binary_info.platform
            format: $binary_info.format
            stripped: $binary_info.stripped
        }
    }
}

# Analyze binary file to determine its properties
def analyze_binary [
    binary_path: string
    packaging_config: record
] {
    let result = (do {
        file $binary_path
    } | complete)

    if $result.exit_code != 0 {
        return {
            architecture: "unknown"
            platform: "unknown"
            format: "unknown"
            stripped: false
        }
    }

    let file_info = $result.stdout

    let architecture = if ($file_info =~ "x86-64") or ($file_info =~ "x86_64") {
        "amd64"
    } else if ($file_info =~ "ARM64") or ($file_info =~ "aarch64") {
        "arm64"
    } else if ($file_info =~ "i386") {
        "i386"
    } else {
        "unknown"
    }

    let platform = if ($file_info =~ "Linux") {
        "linux"
    } else if ($file_info =~ "Mach-O") {
        "macos"
    } else if ($file_info =~ "PE32") {
        "windows"
    } else {
        "unknown"
    }

    let format = if ($file_info =~ "ELF") {
        "elf"
    } else if ($file_info =~ "Mach-O") {
        "macho"
    } else if ($file_info =~ "PE32") {
        "pe"
    } else {
        "unknown"
    }

    let stripped = ($file_info =~ "stripped")

    {
        architecture: $architecture
        platform: $platform
        format: $format
        stripped: $stripped
    }
}

# Package binaries for a specific platform
def package_platform_binaries [
    platform: string
    available_binaries: list
    packaging_config: record
] {
    log info $"Packaging binaries for platform: ($platform)"

    let start_time = (date now)
    let platform_parts = ($platform | split row "-")
    let platform_os = ($platform_parts | get 0)
    let platform_arch = ($platform_parts | get 1)

    # Filter binaries for this platform
    let platform_binaries = ($available_binaries | where {|binary|
        ($binary.platform == $platform_os) and ($binary.architecture == $platform_arch)
    })

    if ($platform_binaries | length) == 0 {
        log warning $"No binaries found for platform: ($platform)"
        return {
            platform: $platform
            status: "skipped"
            reason: "no binaries found"
            packages_created: 0
            total_size: 0
            duration: ((date now) - $start_time)
        }
    }

    mut packaging_errors = []
    mut processed_binaries = []
    mut total_package_size = 0

    # Process each binary
    for binary in $platform_binaries {
        let binary_result = process_single_binary $binary $platform $packaging_config

        if $binary_result.status == "success" {
            $processed_binaries = ($processed_binaries | append $binary_result)
            $total_package_size = $total_package_size + $binary_result.package_size
        } else {
            $packaging_errors = ($packaging_errors | append $binary_result.errors)
        }
    }

    # Create platform package
    let platform_package = create_platform_package $platform $processed_binaries $packaging_config

    let status = if (($packaging_errors | length) > 0) or ($platform_package.status != "success") {
        "failed"
    } else {
        "success"
    }

    {
        platform: $platform
        status: $status
        packages_created: ($processed_binaries | length)
        total_size: $total_package_size
        platform_package: $platform_package
        processed_binaries: $processed_binaries
        errors: $packaging_errors
        duration: ((date now) - $start_time)
    }
}

# Process a single binary
def process_single_binary [
    binary: record
    platform: string
    packaging_config: record
] {
    if $packaging_config.verbose {
        log info $"Processing binary: ($binary.name) for ($platform)"
    }

    let start_time = (date now)
    let output_name = $"($binary.name)-($platform)"
    let temp_binary = ($packaging_config.output_dir | path join "tmp" $output_name)

    let result = (do {
        # Ensure temp directory exists
        mkdir ($temp_binary | path dirname)

        # Copy binary to temp location
        cp $binary.path $temp_binary

        # Strip debug symbols if requested and not already stripped
        if $packaging_config.strip and not $binary.stripped {
            strip_binary $temp_binary $packaging_config
        }

        # Apply UPX compression if requested
        if $packaging_config.upx {
            upx_compress_binary $temp_binary $packaging_config
        }

        # Verify binary integrity
        if $packaging_config.verify {
            let verification_result = verify_binary_integrity $temp_binary $packaging_config
            if $verification_result.status != "success" {
                return {
                    binary: $binary.name
                    status: "failed"
                    reason: $"verification failed: ($verification_result.reason)"
                    errors: [$verification_result]
                    duration: ((date now) - $start_time)
                }
            }
        }

        # Sign binary if requested
        if $packaging_config.sign {
            let signing_result = sign_binary $temp_binary $packaging_config
            if $signing_result.status != "success" {
                return {
                    binary: $binary.name
                    status: "failed"
                    reason: $"signing failed: ($signing_result.reason)"
                    errors: [$signing_result]
                    duration: ((date now) - $start_time)
                }
            }
        }

        # Package binary according to format
        let package_result = package_binary $temp_binary $output_name $packaging_config

        if $package_result.status == "success" {
            # Clean up temp file
            rm $temp_binary

            {
                binary: $binary.name
                status: "success"
                output_name: $output_name
                package_path: $package_result.package_path
                package_size: $package_result.package_size
                original_size: $binary.size
                compression_ratio: $package_result.compression_ratio
                duration: ((date now) - $start_time)
            }
        } else {
            {
                binary: $binary.name
                status: "failed"
                reason: $package_result.reason
                errors: [$package_result]
                duration: ((date now) - $start_time)
            }
        }
    } | complete)

    if $result.exit_code != 0 {
        {
            binary: $binary.name
            status: "failed"
            reason: $result.stderr
            errors: [{ error: $result.stderr }]
            duration: ((date now) - $start_time)
        }
    } else {
        $result.stdout
    }
}

# Strip debug symbols from binary
def strip_binary [binary_path: string, packaging_config: record] {
    if $packaging_config.verbose {
        log info $"Stripping debug symbols: ($binary_path)"
    }

    let result = (do {
        strip $binary_path
    } | complete)

    if $result.exit_code != 0 {
        log warning $"Failed to strip binary ($binary_path): ($result.stderr)"
    }
}

# Compress binary with UPX
def upx_compress_binary [binary_path: string, packaging_config: record] {
    if $packaging_config.verbose {
        log info $"UPX compressing: ($binary_path)"
    }

    # Check if UPX is available
    let upx_check = (do {
        which upx
    } | complete)

    if $upx_check.exit_code != 0 {
        log warning "UPX not available, skipping compression"
        return
    }

    # Apply UPX compression
    let result = (do {
        upx --best $binary_path
    } | complete)

    if $result.exit_code != 0 {
        log warning $"Failed to UPX compress binary ($binary_path): ($result.stderr)"
    }
}

# Verify binary integrity
def verify_binary_integrity [binary_path: string, packaging_config: record] {
    # Check if binary is still executable
    let file_result = (do {
        file $binary_path
    } | complete)

    if $file_result.exit_code != 0 {
        return {
            status: "failed"
            reason: "unable to read binary file"
        }
    }

    let file_info = $file_result.stdout
    if not ($file_info =~ "executable") {
        return {
            status: "failed"
            reason: "binary is not executable after processing"
        }
    }

    # Try to run the binary with --help or --version
    let help_test = (do {
        ^ $binary_path --help e>| null
    } | complete)

    let version_test = (do {
        ^ $binary_path --version e>| null
    } | complete)

    if ($help_test.exit_code == 0) or ($version_test.exit_code == 0) {
        {
            status: "success"
            verified: true
        }
    } else {
        {
            status: "failed"
            reason: "binary does not respond to --help or --version"
        }
    }
}

# Sign binary (placeholder - would need actual signing implementation)
def sign_binary [binary_path: string, packaging_config: record] {
    log warning "Binary signing not implemented - skipping"
    return {
        status: "success"
        signed: false
        reason: "signing not implemented"
    }
}

# Package binary according to specified format
def package_binary [
    binary_path: string
    output_name: string
    packaging_config: record
] {
    match $packaging_config.format {
        "archive" => { create_archive_package $binary_path $output_name $packaging_config }
        "installer" => { create_installer_package $binary_path $output_name $packaging_config }
        "standalone" => { create_standalone_package $binary_path $output_name $packaging_config }
        _ => {
            {
                status: "failed"
                reason: $"unsupported format: ($packaging_config.format)"
            }
        }
    }
}

# Create archive package
def create_archive_package [
    binary_path: string
    output_name: string
    packaging_config: record
] {
    let archive_name = $"($output_name).tar.gz"
    let archive_path = ($packaging_config.output_dir | path join $archive_name)

    let result = (do {
        # Create tar.gz archive
        let binary_dir = ($binary_path | path dirname)
        let binary_name = ($binary_path | path basename)

        cd $binary_dir
        tar -czf $archive_path $binary_name

        let archive_size = (ls $archive_path | get 0.size)
        let original_size = (ls $binary_path | get 0.size)
        let compression_ratio = (($archive_size | into float) / ($original_size | into float) * 100)

        {
            status: "success"
            package_path: $archive_path
            package_size: $archive_size
            compression_ratio: $compression_ratio
        }
    } | complete)

    if $result.exit_code != 0 {
        {
            status: "failed"
            reason: $result.stderr
        }
    } else {
        $result.stdout
    }
}

# Create installer package
def create_installer_package [
    binary_path: string
    output_name: string
    packaging_config: record
] {
    # Placeholder - would create platform-specific installers
    log warning "Installer packages not implemented - using archive format"
    create_archive_package $binary_path $output_name $packaging_config
}

# Create standalone package
def create_standalone_package [
    binary_path: string
    output_name: string
    packaging_config: record
] {
    let standalone_path = ($packaging_config.output_dir | path join $output_name)

    let result = (do {
        # Just copy the binary as standalone
        cp $binary_path $standalone_path

        let package_size = (ls $standalone_path | get 0.size)

        {
            status: "success"
            package_path: $standalone_path
            package_size: $package_size
            compression_ratio: 100.0
        }
    } | complete)

    if $result.exit_code != 0 {
        {
            status: "failed"
            reason: $result.stderr
        }
    } else {
        $result.stdout
    }
}

# Create platform package (combines all binaries for a platform)
def create_platform_package [
    platform: string
    processed_binaries: list
    packaging_config: record
] {
    if ($processed_binaries | length) == 0 {
        return {
            status: "skipped"
            reason: "no binaries to package"
        }
    }

    let platform_package_name = $"provisioning-binaries-($platform).tar.gz"
    let platform_package_path = ($packaging_config.output_dir | path join $platform_package_name)

    let result = (do {
        # Create temporary directory for platform package
        let temp_platform_dir = ($packaging_config.output_dir | path join "tmp" $"platform-($platform)")
        mkdir $temp_platform_dir

        # Copy all processed binaries to platform directory
        for binary in $processed_binaries {
            let binary_name = ($binary.package_path | path basename)
            cp $binary.package_path ($temp_platform_dir | path join $binary_name)
        }

        # Create platform archive
        let temp_parent = ($temp_platform_dir | path dirname)
        let temp_name = ($temp_platform_dir | path basename)

        cd $temp_parent
        tar -czf $platform_package_path $temp_name

        # Clean up temporary directory
        rm -rf $temp_platform_dir

        let package_size = (ls $platform_package_path | get 0.size)

        log info $"Created platform package: ($platform_package_path)"

        {
            status: "success"
            package_path: $platform_package_path
            package_size: $package_size
            binaries_included: ($processed_binaries | length)
        }
    } | complete)

    if $result.exit_code != 0 {
        {
            status: "failed"
            reason: $result.stderr
        }
    } else {
        $result.stdout
    }
}

# Show binary information
def "main info" [source_dir: string = "dist/platform"] {
    let source_root = ($source_dir | path expand)

    if not ($source_root | path exists) {
        return { error: "source directory not found", directory: $source_root }
    }

    let dummy_config = { verbose: false }
    let available_binaries = find_available_binaries $source_root $dummy_config

    {
        source_directory: $source_root
        total_binaries: ($available_binaries | length)
        binaries_by_platform: ($available_binaries | group-by platform | items {|platform, binaries| { platform: $platform, count: ($binaries | length) } })
        binaries_by_architecture: ($available_binaries | group-by architecture | items {|arch, binaries| { architecture: $arch, count: ($binaries | length) } })
        total_size: ($available_binaries | get size | math sum)
        binaries: $available_binaries
    }
}

# List packaged binaries
def "main list" [packages_dir: string = "packages/binaries"] {
    let packages_root = ($packages_dir | path expand)

    if not ($packages_root | path exists) {
        return { error: "packages directory not found", directory: $packages_root }
    }

    let binary_packages = (glob ($packages_root | path join "*.tar.gz") | append (glob ($packages_root | path join "provisioning-*")))

    $binary_packages | each {|package|
        let package_info = (ls $package | get 0)
        {
            name: ($package | path basename)
            path: $package
            size: $package_info.size
            modified: $package_info.modified
        }
    }
}
