#!/usr/bin/env nu

# Checksum generation tool - generates and verifies checksums for distribution packages
#
# Generates:
# - SHA256 checksums (primary)
# - MD5 checksums (legacy compatibility)
# - SHA512 checksums (high security)
# - File integrity verification

use std log

def main [
    --input-dir: string = "packages"            # Directory containing packages to checksum
    --output-file: string = ""                  # Output file for checksums (auto-generated if empty)
    --algorithms: string = "sha256,md5"         # Hash algorithms: sha256, md5, sha512, all
    --format: string = "standard"               # Output format: standard, json, csv
    --verify: string = ""                       # Verify checksums from existing file
    --recursive                                   # Process directories recursively
    --pattern: string = "*"                     # File pattern to match
    --verbose                                   # Enable verbose logging
]
{

    let input_root = ($input_dir | path expand)
    let algorithms_list = if $algorithms == "all" {
        ["sha256", "md5", "sha512"]
    } else {
        ($algorithms | split row "," | each { str trim })
    }

    let checksum_config = {
        input_dir: $input_root
        output_file: (if $output_file == "" {
            ($input_root | path join $"checksums-($algorithms_list.0).txt")
        } else {
            $output_file | path expand
        })
        algorithms: $algorithms_list
        format: $format
        verify_file: (if $verify == "" { "" } else { $verify | path expand })
        recursive: $recursive
        pattern: $pattern
        verbose: $verbose
    }

    log info $"Starting checksum generation with config: ($checksum_config)"

    # Validate input directory
    if not ($input_root | path exists) {
        log error $"Input directory does not exist: ($input_root)"
        exit 1
    }

    # If verifying, run verification instead
    if $checksum_config.verify_file != "" {
        return verify_checksums $checksum_config
    }

    # Find files to checksum
    let files = find_checksum_files $checksum_config

    if ($files | length) == 0 {
        log warning "No files found to checksum"
        return {
            status: "skipped"
            reason: "no files found"
            files_processed: 0
        }
    }

    log info $"Found ($files | length) files to checksum"

    # Generate checksums
    let checksum_results = generate_checksums_for_files $files $checksum_config

    # Save checksums to file
    let save_result = save_checksums $checksum_results $checksum_config

    let summary = {
        total_files: ($files | length)
        successful_files: ($checksum_results | where status == "success" | length)
        failed_files: ($checksum_results | where status == "failed" | length)
        algorithms_used: $checksum_config.algorithms
        output_file: $checksum_config.output_file
        checksum_config: $checksum_config
        results: $checksum_results
        save_result: $save_result
    }

    if $summary.failed_files > 0 {
        log error $"Checksum generation completed with ($summary.failed_files) failures"
        exit 1
    } else {
        log info $"Checksum generation completed successfully - ($summary.successful_files) files processed"
    }

    return $summary
}

# Find files to generate checksums for
def find_checksum_files [checksum_config: record]
{
    let find_command = if $checksum_config.recursive {
        $"find ($checksum_config.input_dir) -name \"($checksum_config.pattern)\" -type f"
    } else {
        $"find ($checksum_config.input_dir) -maxdepth 1 -name \"($checksum_config.pattern)\" -type f"
    }

    let found_files = (bash -c $find_command | lines | where $it != "")

    # Filter out checksum files themselves
    $found_files | where {|file|
        not ($file =~ "checksum" or $file =~ "\.sha256$" or $file =~ "\.md5$" or $file =~ "\.sha512$")
    }
}

# Generate checksums for all files
def generate_checksums_for_files [
    files: list
    checksum_config: record
]
{
    $files | each {|file|
        generate_checksums_for_file $file $checksum_config
    }
}

# Generate checksums for a single file
def generate_checksums_for_file [
    file: string
    checksum_config: record
]
{
    if $checksum_config.verbose {
        log info $"Generating checksums for: ($file)"
    }

    let start_time = (date now)
    let checksums = {}
    let errors = []

    # Get file info
    let file_info = (ls $file | get 0)
    let relative_path = ($file | str replace $checksum_config.input_dir "" | str trim-left "/")

    # Generate each requested algorithm
    for algorithm in $checksum_config.algorithms {
        try {
            let checksum = match $algorithm {
                "sha256" => { generate_sha256 $file }
                "md5" => { generate_md5 $file }
                "sha512" => { generate_sha512 $file }
                _ => {
                    $errors = ($errors | append $"Unknown algorithm: ($algorithm)")
                    ""
                }
            }

            if $checksum != "" {
                $checksums = ($checksums | insert $algorithm $checksum)
            }

        } catch {|err|
            $errors = ($errors | append $"Failed to generate ($algorithm): ($err.msg)")
        }
    }

    {
        file: $file
        relative_path: $relative_path
        status: (if ($errors | length) > 0 { "failed" } else { "success" })
        size: $file_info.size
        modified: $file_info.modified
        checksums: $checksums
        errors: $errors
        duration: ((date now) - $start_time)
    }
}

# Generate SHA256 checksum
def generate_sha256 [file: string]
{
    let result = (run-external --redirect-combine "shasum" "-a" "256" $file | complete)
    if $result.exit_code == 0 {
        ($result.stdout | split row " " | get 0)
    } else {
        # Fallback to other SHA256 tools
        let openssl_result = (run-external --redirect-combine "openssl" "sha256" $file | complete)
        if $openssl_result.exit_code == 0 {
            ($openssl_result.stdout | str replace $"SHA256\\(.*\\)= " "" | str trim)
        } else {
            error $"Failed to generate SHA256 for ($file)"
        }
    }
}

# Generate MD5 checksum
def generate_md5 [file: string]
{
    let result = (run-external --redirect-combine "md5sum" $file | complete)
    if $result.exit_code == 0 {
        ($result.stdout | split row " " | get 0)
    } else {
        # Fallback for macOS
        let md5_result = (run-external --redirect-combine "md5" "-r" $file | complete)
        if $md5_result.exit_code == 0 {
            ($md5_result.stdout | split row " " | get 0)
        } else {
            # Fallback to openssl
            let openssl_result = (run-external --redirect-combine "openssl" "md5" $file | complete)
            if $openssl_result.exit_code == 0 {
                ($openssl_result.stdout | str replace $"MD5\\(.*\\)= " "" | str trim)
            } else {
                error $"Failed to generate MD5 for ($file)"
            }
        }
    }
}

# Generate SHA512 checksum
def generate_sha512 [file: string]
{
    let result = (run-external --redirect-combine "shasum" "-a" "512" $file | complete)
    if $result.exit_code == 0 {
        ($result.stdout | split row " " | get 0)
    } else {
        # Fallback to openssl
        let openssl_result = (run-external --redirect-combine "openssl" "sha512" $file | complete)
        if $openssl_result.exit_code == 0 {
            ($openssl_result.stdout | str replace $"SHA512\\(.*\\)= " "" | str trim)
        } else {
            error $"Failed to generate SHA512 for ($file)"
        }
    }
}

# Save checksums to file
def save_checksums [
    checksum_results: list
    checksum_config: record
]
{
    log info $"Saving checksums to: ($checksum_config.output_file)"

    let successful_results = ($checksum_results | where status == "success")

    try {
        match $checksum_config.format {
            "standard" => {
                save_standard_format $successful_results $checksum_config
            }
            "json" => {
                save_json_format $successful_results $checksum_config
            }
            "csv" => {
                save_csv_format $successful_results $checksum_config
            }
            _ => {
                error $"Unknown format: ($checksum_config.format)"
            }
        }

        {
            status: "success"
            format: $checksum_config.format
            file: $checksum_config.output_file
            entries: ($successful_results | length)
        }

    } catch {|err|
        {
            status: "failed"
            reason: $err.msg
        }
    }
}

# Save in standard format (compatible with shasum/md5sum)
def save_standard_format [
    results: list
    checksum_config: record
] {
    let output_lines = []

    # Add header
    $output_lines = ($output_lines | append $"# Checksums generated on (date now)")
    $output_lines = ($output_lines | append $"# Algorithms: ($checksum_config.algorithms | str join ', ')")
    $output_lines = ($output_lines | append "")

    # Add checksums for each algorithm
    for algorithm in $checksum_config.algorithms {
        $output_lines = ($output_lines | append $"# ($algorithm | str upcase) checksums")

        for result in $results {
            if ($algorithm in ($result.checksums | columns)) {
                let checksum = ($result.checksums | get $algorithm)
                $output_lines = ($output_lines | append $"($checksum)  ($result.relative_path)")
            }
        }

        $output_lines = ($output_lines | append "")
    }

    ($output_lines | str join "\n") | save $checksum_config.output_file
}

# Save in JSON format
def save_json_format [
    results: list
    checksum_config: record
] {
    let output_data = {
        generated_at: (date now)
        algorithms: $checksum_config.algorithms
        total_files: ($results | length)
        files: ($results | each {|result|
            {
                file: $result.relative_path
                size: $result.size
                modified: $result.modified
                checksums: $result.checksums
            }
        })
    }

    $output_data | to json | save $checksum_config.output_file
}

# Save in CSV format
def save_csv_format [
    results: list
    checksum_config: record
] {
    let csv_data = []

    # Create header
    let header = ["file", "size", "modified"]
    $header = ($header | append $checksum_config.algorithms)
    $csv_data = ($csv_data | append ($header | str join ","))

    # Add data rows
    for result in $results {
        let row = [$result.relative_path, ($result.size | into string), ($result.modified | format date "%Y-%m-%d %H:%M:%S")]

        for algorithm in $checksum_config.algorithms {
            let checksum = if ($algorithm in ($result.checksums | columns)) {
                ($result.checksums | get $algorithm)
            } else {
                ""
            }
            $row = ($row | append $checksum)
        }

        $csv_data = ($csv_data | append ($row | str join ","))
    }

    ($csv_data | str join "\n") | save $checksum_config.output_file
}

# Verify checksums from existing file
def verify_checksums [checksum_config: record]
{
    log info $"Verifying checksums from: ($checksum_config.verify_file)"

    if not ($checksum_config.verify_file | path exists) {
        log error $"Checksum file does not exist: ($checksum_config.verify_file)"
        exit 1
    }

    let checksum_content = (open $checksum_config.verify_file --raw)
    let verification_results = parse_and_verify_checksums $checksum_content $checksum_config

    let summary = {
        total_verified: ($verification_results | length)
        successful_verifications: ($verification_results | where status == "success" | length)
        failed_verifications: ($verification_results | where status == "failed" | length)
        missing_files: ($verification_results | where status == "missing" | length)
        checksum_file: $checksum_config.verify_file
        results: $verification_results
    }

    if ($summary.failed_verifications > 0) or ($summary.missing_files > 0) {
        log error $"Checksum verification failed: ($summary.failed_verifications) mismatches, ($summary.missing_files) missing files"
        exit 1
    } else {
        log info $"Checksum verification completed successfully - all ($summary.successful_verifications) files verified"
    }

    return $summary
}

# Parse and verify checksums from file content
def parse_and_verify_checksums [
    content: string
    checksum_config: record
]
{
    let lines = ($content | lines | where {|line|
        not ($line | str starts-with "#") and ($line | str trim) != ""
    })

    $lines | each {|line|
        let parts = ($line | split row "  " | where $it != "")
        if ($parts | length) >= 2 {
            let expected_checksum = ($parts | get 0)
            let file_path = ($parts | get 1)
            let full_path = ($checksum_config.input_dir | path join $file_path)

            verify_single_checksum $expected_checksum $full_path $file_path
        } else {
            {
                file: "unknown"
                status: "failed"
                reason: $"Invalid checksum line: ($line)"
            }
        }
    }
}

# Verify a single checksum
def verify_single_checksum [
    expected_checksum: string
    full_path: string
    relative_path: string
]
{
    if not ($full_path | path exists) {
        return {
            file: $relative_path
            status: "missing"
            expected: $expected_checksum
            actual: ""
            reason: "file not found"
        }
    }

    try {
        # Determine algorithm by checksum length
        let algorithm = match ($expected_checksum | str length) {
            32 => "md5"
            64 => "sha256"
            128 => "sha512"
            _ => "unknown"
        }

        let actual_checksum = match $algorithm {
            "md5" => { generate_md5 $full_path }
            "sha256" => { generate_sha256 $full_path }
            "sha512" => { generate_sha512 $full_path }
            _ => { "" }
        }

        if $actual_checksum == $expected_checksum {
            {
                file: $relative_path
                status: "success"
                algorithm: $algorithm
                expected: $expected_checksum
                actual: $actual_checksum
            }
        } else {
            {
                file: $relative_path
                status: "failed"
                algorithm: $algorithm
                expected: $expected_checksum
                actual: $actual_checksum
                reason: "checksum mismatch"
            }
        }

    } catch {|err|
        {
            file: $relative_path
            status: "failed"
            expected: $expected_checksum
            actual: ""
            reason: $err.msg
        }
    }
}

# Show checksum file info
def "main info" [checksum_file: string = ""] {
    if $checksum_file == "" {
        # Look for checksum files in current directory
        let checksum_files = (find . -maxdepth 1 -name "*checksum*" -o -name "*.sha256" -o -name "*.md5" -o -name "*.sha512" | lines | where $it != "")

        return {
            current_directory: $env.PWD
            checksum_files_found: ($checksum_files | length)
            files: $checksum_files
        }
    }

    let checksum_path = ($checksum_file | path expand)

    if not ($checksum_path | path exists) {
        return { error: "checksum file not found", file: $checksum_path }
    }

    let content = (open $checksum_path --raw)
    let lines = ($content | lines)
    let checksum_lines = ($lines | where {|line|
        not ($line | str starts-with "#") and ($line | str trim) != ""
    })

    # Analyze checksum types
    let algorithms_detected = ($checksum_lines | each {|line|
        let parts = ($line | split row "  ")
        if ($parts | length) >= 2 {
            let checksum = ($parts | get 0)
            match ($checksum | str length) {
                32 => "md5"
                64 => "sha256"
                128 => "sha512"
                _ => "unknown"
            }
        } else {
            "unknown"
        }
    } | uniq)

    {
        file: $checksum_path
        total_lines: ($lines | length)
        checksum_lines: ($checksum_lines | length)
        algorithms_detected: $algorithms_detected
        size: (ls $checksum_path | get 0.size)
        modified: (ls $checksum_path | get 0.modified)
    }
}

# Quick verify command
def "main verify" [
    checksum_file: string                       # Checksum file to verify against
    --input-dir: string = "."                   # Directory containing files to verify
] {
    main --verify $checksum_file --input-dir $input_dir
}
