/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com

   Project: Zygaena
  Filename: install.reef
   Authors: Chris Tusa <chris.tusa@leafscale.com>
   License: <see LICENSE file included with this source code>
Description: Install command - install packages from archives

******************************************************************************/

module commands.install

import sys.args
import sys.process
import io.file
import io.dir
import io.path
import core.str
import core.config
import core.database
import core.pkgdb
import core.package
import core.groups
import core.repository
import core.signing
import core.resolver
import core.port
import core.portindex
import util.checksum
import util.mtree
import util.http as uhttp
import util.prompt
import util.version as ver
import types
import util.color
import util.priv
import exitcodes as ec

export
    fn execute(opts: types.GlobalOptions): int
end export

fn execute(opts: types.GlobalOptions): int
    let argc = args.count()

    // Package arguments start after the command
    if argc <= opts.cmd_index + 1
        print_usage()
        return ec.EXIT_USAGE()
    end if

    // Load configuration and apply command-line overrides
    let base_cfg = config.load()
    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix, opts.no_chroot)
    uhttp.enable_proxy(config.get_use_proxy(cfg))

    // Auto-refresh port index if configured
    if config.get_auto_refresh(cfg)
        portindex.rebuild_port_index()
    end if

    // Show root/prefix if set (verbose mode)
    if opts.verbose
        let root = config.get_root(cfg)
        let prefix = config.get_prefix(cfg)
        if str.length(root) > 0
            color.print_info("Using root: " + root)
        end if
        if str.length(prefix) > 0
            color.print_info("Using prefix: " + prefix)
        end if
    end if

    // Collect all packages to install (expanding groups)
    mut packages: [string] = new [string](256)
    mut pkg_count = 0
    mut direct_files: [string] = new [string](16)
    mut direct_count = 0

    // Process each package argument (starting after the command)
    mut i = opts.cmd_index + 1
    while i < argc and pkg_count < 256
        let pkg_arg = args.get(i)

        // Skip flags and their values
        if pkg_arg == "--root" or pkg_arg == "--prefix"
            i = i + 2  // Skip flag and its value
            continue
        elif pkg_arg == "--no-chroot-scripts"
            i = i + 1  // valueless flag: skip only itself
            continue
        end if
        if str.starts_with(pkg_arg, "-")
            i = i + 1
            continue
        end if

        // Check if this is a direct file path to a .pkg.tar.xz
        if str.ends_with(pkg_arg, ".pkg.tar.xz") and file.fileExists(pkg_arg)
            direct_files[direct_count] = pkg_arg
            direct_count = direct_count + 1
            i = i + 1
            continue
        end if

        // Check if this is a group reference
        if groups.is_group(pkg_arg)
            // Expand group to member packages
            mut group_members: [string] = new [string](128)
            let member_count = groups.expand_group(pkg_arg, group_members, 128)

            if member_count == 0
                color.print_error("Group not found or empty: " + pkg_arg)
                i = i + 1
                continue
            end if

            color.print_info("Expanding group " + pkg_arg + " (" + int_to_str(member_count) + " packages)")

            // Add all group members to packages list
            mut j = 0
            while j < member_count and pkg_count < 256
                packages[pkg_count] = group_members[j]
                pkg_count = pkg_count + 1
                j = j + 1
            end while
        else
            // Regular package
            packages[pkg_count] = pkg_arg
            pkg_count = pkg_count + 1
        end if

        i = i + 1
    end while

    // Handle direct file installs — bypass resolver, install immediately
    if direct_count > 0
        let root = get_root(cfg)
        let root_for_db = config.get_root(cfg)
        if str.length(root_for_db) > 0
            if not database.init_db_rooted(root_for_db)
                color.print_error("Failed to initialize database in " + root_for_db)
                return ec.EXIT_DB_ERROR()
            end if
        else
            if not database.init_db()
                color.print_error("Failed to initialize package database")
                return ec.EXIT_DB_ERROR()
            end if
        end if

        mut di = 0
        while di < direct_count
            if not install_package(direct_files[di], cfg, root_for_db, opts)
                color.print_error("Failed to install: " + direct_files[di])
                return ec.EXIT_INSTALL_FAILED()
            end if
            di = di + 1
        end while

        if pkg_count == 0
            return ec.EXIT_SUCCESS()
        end if
    end if

    // Resolve dependencies for all packages
    // Use rooted resolver if installing to alternate root
    mut install_order: [string] = new [string](512)
    let root_for_resolve = config.get_root(cfg)
    mut install_count = 0
    if str.length(root_for_resolve) > 0
        install_count = resolver.get_install_order_rooted(packages, pkg_count, install_order, 512, root_for_resolve)
    else
        install_count = resolver.get_install_order(packages, pkg_count, install_order, 512)
    end if

    // When force/reinstall is set, add requested packages that the resolver
    // skipped (because they're already installed) back into the install order
    if opts.force and install_count < pkg_count
        mut fi = 0
        while fi < pkg_count
            let pkg = packages[fi]
            if not array_contains(install_order, install_count, pkg)
                install_order[install_count] = pkg
                install_count = install_count + 1
            end if
            fi = fi + 1
        end while
    end if

    if install_count == 0 and pkg_count > 0
        color.print_warning("No packages to install (all already installed or not found)")
        return ec.EXIT_SUCCESS()
    end if

    // Separate new installs from already-installed (for display)
    mut to_install: [string] = new [string](512)
    mut install_idx = 0

    mut dep_idx = 0
    while dep_idx < install_count
        to_install[install_idx] = install_order[dep_idx]
        install_idx = install_idx + 1
        dep_idx = dep_idx + 1
    end while

    // Ask for confirmation (unless -y flag)
    mut empty_upgrade: [string] = new [string](1)
    if not prompt.confirm_install(to_install, install_idx, empty_upgrade, 0, opts)
        color.print_info("Installation cancelled")
        return ec.EXIT_CANCELLED()
    end if

    // Dry-run: show what would be done and exit
    if opts.dry_run
        color.print_info("Dry run — no changes made")
        return ec.EXIT_SUCCESS()
    end if

    // Get root path for installation
    let root = get_root(cfg)

    // Initialize database in the target root
    let root_for_db = config.get_root(cfg)
    if str.length(root_for_db) > 0
        if not database.init_db_rooted(root_for_db)
            color.print_error("Failed to initialize database in " + root_for_db)
            return ec.EXIT_DB_ERROR()
        end if
    else
        if not database.init_db()
            color.print_error("Failed to initialize package database")
            return ec.EXIT_DB_ERROR()
        end if
    end if

    // Install all packages in resolved order (dependencies first)
    i = 0
    while i < install_count
        let pkg_arg = install_order[i]

        // Install this package
        if not install_package(pkg_arg, cfg, root_for_db, opts)
            color.print_error("Failed to install: " + pkg_arg)
            color.print_error("Aborting installation - dependency failed")
            return ec.EXIT_INSTALL_FAILED()
        end if

        i = i + 1
    end while

    return ec.EXIT_SUCCESS()
end execute

// Helper to check if array contains a string
fn array_contains(arr: [string], count: int, s: string): bool
    mut i = 0
    while i < count
        if str.equals(arr[i], s)
            return true
        end if
        i = i + 1
    end while
    return false
end array_contains

proc print_usage()
    println("Usage: coral install <package...>")
    println("")
    println("Install one or more packages.")
    println("")
    println("Arguments:")
    println("  <package>    Package name or path to .pkg.tar.xz file")
    println("")
    println("Options:")
    println("  -y, --yes    Don't ask for confirmation")
    println("  --force      Force reinstall if already installed")
    println("")
    println("Examples:")
    println("  coral install vim")
    println("  coral install /path/to/package.pkg.tar.xz")
    println("  coral install vim nano htop")
end print_usage

fn install_package(pkg_arg: string, cfg: config.Config, root_path: string, opts: types.GlobalOptions): bool
    // Determine if this is a file path or package name
    let pkg_path = resolve_package_path(pkg_arg, cfg)

    if str.length(pkg_path) == 0
        color.print_error("Package not found: " + pkg_arg)
        return false
    end if

    // Verify signature if required (for local packages not already verified by resolve_package_path)
    let require_signed = config.get_require_signed_packages(cfg)
    if require_signed
        let sig_path = pkg_path + ".sig"
        if file.fileExists(sig_path)
            color.print_info("Verifying package signature...")
            let verify_result = signing.verify_package(pkg_path, sig_path)
            if not verify_result.success or not verify_result.valid
                color.print_error("Signature verification failed: " + verify_result.error)
                return false
            end if
            color.print_success("Signature verified (key: " + verify_result.keyid + ")")
        else
            color.print_error("Package signature required but not found: " + sig_path)
            return false
        end if
    end if

    color.print_action("Installing " + pkg_arg + "...")

    // Create extraction directory
    let extract_dir = create_extract_dir(cfg)
    if str.length(extract_dir) == 0
        color.print_error("Failed to create extraction directory")
        return false
    end if

    // Extract package
    color.print_info("Extracting package...")
    let result = package.extract_package(pkg_path, extract_dir)
    if not result.success
        color.print_error(result.error)
        cleanup_extract_dir(extract_dir)
        return false
    end if

    // Read package info
    let info = package.read_pkginfo(extract_dir)
    if str.length(info.name) == 0
        color.print_error("Failed to read package info")
        cleanup_extract_dir(extract_dir)
        return false
    end if

    color.print_info("Package: " + info.name + " " + info.version)

    // Check for conflicts with installed packages
    if info.conflicts_count > 0
        mut ci = 0
        while ci < info.conflicts_count
            let conflict = info.conflicts[ci]
            if check_installed(conflict, root_path)
                color.print_error("Package " + info.name + " conflicts with installed package: " + conflict)
                if not opts.force
                    color.print_error("Use --force to override conflict check")
                    cleanup_extract_dir(extract_dir)
                    return false
                end if
                color.print_warning("Proceeding despite conflict (--force specified)")
            end if
            ci = ci + 1
        end while
    end if

    // Check if already installed (in target root)
    let already_installed = check_installed(info.name, root_path)
    mut is_upgrade = false
    mut old_version = ""

    if already_installed
        // Get old package info to check version
        let old_pkg = get_old_installed(info.name, root_path)
        old_version = old_pkg.info.version

        if not str.equals(old_version, info.version)
            // Different version — upgrade
            is_upgrade = true
            color.print_info("Upgrading " + info.name + " " + old_version + " -> " + info.version)
        else
            // Same version — skip or reinstall with --force
            if not opts.force
                color.print_warning(info.name + " " + info.version + " is already installed (use --force to reinstall)")
                cleanup_extract_dir(extract_dir)
                return true
            end if
            color.print_info("Reinstalling " + info.name + "...")
        end if
    end if

    let install_root = get_root(cfg)
    // Resolve script execution context; abort early if confinement is required but impossible.
    // Defense-in-depth: main.reef already bars non-root for all non-query commands, so this abort
    // is currently unreachable via the CLI. Kept so confinement fails safely if that global gate is
    // ever relaxed.
    if package.script_wants_chroot(install_root, config.get_chroot_scripts(cfg)) and not priv.is_root()
        color.print_error("--root " + install_root + " runs maintainer scripts confined to the target via chroot, which requires root privileges.")
        color.print_error("Re-run as root, or pass --no-chroot-scripts to run them on the host (unconfined).")
        cleanup_extract_dir(extract_dir)
        return false
    end if
    let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root())

    if is_upgrade
        // Upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade

        // Run pre-upgrade from stored scripts (old package)
        let old_scripts = database.get_scripts_dir(info.name)
        if database.has_stored_scripts(info.name)
            color.print_info("Running pre-upgrade script...")
            if not package.run_stored_pre_upgrade(script_ctx, old_scripts, old_version, info.version)
                color.print_error("Pre-upgrade script failed, aborting")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Remove old files
        color.print_info("Removing old version files...")
        mut old_files: [string] = new [string](4096)
        let old_file_count = get_old_files(info.name, old_files, 4096, root_path)
        if old_file_count > 0
            package.remove_files(old_files, old_file_count, install_root)
        end if

        // Load old manifest for config file checksum protection
        mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
        let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192)

        // Install new files (with upgrade config protection)
        color.print_info("Installing files...")
        if old_manifest_count > 0
            if not package.install_files_upgrade(extract_dir, install_root, old_manifest_entries, old_manifest_count)
                color.print_error("Failed to install files")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        else
            if not package.install_files(extract_dir, install_root)
                color.print_error("Failed to install files")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Run post-upgrade from new package's scripts
        if package.has_scripts(extract_dir)
            color.print_info("Running post-upgrade script...")
            if not package.run_post_upgrade(script_ctx, extract_dir, old_version, info.version)
                color.print_warning("Post-upgrade script failed")
            end if
        end if
    else
        // Fresh install flow

        // Run pre-install script (abort on failure)
        if package.has_scripts(extract_dir)
            color.print_info("Running pre-install script...")
            if not package.run_pre_install(script_ctx, extract_dir, info.version)
                color.print_error("Pre-install script failed")
                cleanup_extract_dir(extract_dir)
                return false
            end if
        end if

        // Install files to target root
        color.print_info("Installing files...")
        if not package.install_files(extract_dir, install_root)
            color.print_error("Failed to install files")
            cleanup_extract_dir(extract_dir)
            return false
        end if

        // Run post-install script (warn on failure, don't abort)
        if package.has_scripts(extract_dir)
            color.print_info("Running post-install script...")
            if not package.run_post_install(script_ctx, extract_dir, info.version)
                color.print_warning("Post-install script failed")
            end if
        end if
    end if

    // Register in database (use rooted version if root is set)
    color.print_info("Registering package...")
    if not register_installed_fast_rooted(info, extract_dir, root_path)
        color.print_error("Failed to register package in database")
        // Files are installed but not registered - warn but don't fail
    end if

    // Store scripts for later use (removal)
    if package.has_scripts(extract_dir)
        if not store_scripts_rooted(info.name, extract_dir, root_path)
            color.print_warning("Failed to store scripts for removal")
        end if
    end if

    // Store config files list for upgrade handling
    if not store_config_files_rooted(info.name, extract_dir, root_path)
        color.print_warning("Failed to store config files list")
    end if

    // Update package database indexes
    if not pkgdb.rebuild_indexes_rooted(root_path)
        color.print_warning("Failed to update package indexes")
    end if

    // Cleanup
    cleanup_extract_dir(extract_dir)

    color.print_success("Installed " + info.name + " " + info.version)
    return true
end install_package

// Check if package is installed, using rooted check if root is set
fn check_installed(name: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.is_installed_rooted(name, root_path)
    end if
    return database.is_installed(name)
end check_installed

// Register package using rooted version if root is set
fn register_installed_fast_rooted(info: types.PackageInfo, extract_dir: string, root_path: string): bool
    mut files: [string] = new [string](1)
    mut pkg = new_installed_pkg(info, files, 0)

    if str.length(root_path) > 0
        return database.register_from_extract_dir_rooted(pkg, extract_dir, root_path)
    end if
    return database.register_from_extract_dir(pkg, extract_dir)
end register_installed_fast_rooted

// Store scripts using rooted version if root is set
fn store_scripts_rooted(name: string, extract_dir: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.store_scripts_rooted(name, extract_dir, root_path)
    end if
    return database.store_scripts(name, extract_dir)
end store_scripts_rooted

// Store config files using rooted version if root is set
fn store_config_files_rooted(name: string, extract_dir: string, root_path: string): bool
    if str.length(root_path) > 0
        return database.store_config_files_rooted(name, extract_dir, root_path)
    end if
    return database.store_config_files(name, extract_dir)
end store_config_files_rooted

// Find package file - either direct path, repository, or search in packages directory
fn resolve_package_path(pkg_arg: string, cfg: config.Config): string
    // If it's a file path that exists, use it directly
    if file.fileExists(pkg_arg)
        return pkg_arg
    end if

    // Search in local packages cache first
    let packages_dir = config.get_packages_dir(cfg)

    // Find all matching packages and select highest version
    mut entries: [string] = new [string](256)
    let entry_count = dir.list_dir(packages_dir, entries, 256)

    // Collect all matching package files
    mut best_file = ""
    mut best_version = ""
    mut best_release = 0

    mut i = 0
    while i < entry_count
        let entry = entries[i]
        // Check if entry starts with package name and has correct suffix
        if str.starts_with(entry, pkg_arg + "-") and str.ends_with(entry, ".pkg.tar.xz")
            // Extract version from filename
            let entry_version = ver.extract_version_from_filename(entry, pkg_arg)
            let entry_release = ver.extract_release_from_filename(entry)

            // Compare with current best
            if str.length(best_file) == 0
                // First match
                best_file = entry
                best_version = entry_version
                best_release = entry_release
            else
                // Compare versions
                let cmp = ver.compare_versions(entry_version, best_version)
                if cmp > 0
                    // Entry has higher version
                    best_file = entry
                    best_version = entry_version
                    best_release = entry_release
                elif cmp == 0 and entry_release > best_release
                    // Same version but higher release
                    best_file = entry
                    best_version = entry_version
                    best_release = entry_release
                end if
            end if
        end if
        i = i + 1
    end while

    // Return the best matching package
    if str.length(best_file) > 0
        return path.join_path(packages_dir, best_file)
    end if

    // Also try with .pkg.tar.xz appended
    let direct_path = path.join_path(packages_dir, pkg_arg + ".pkg.tar.xz")
    if file.fileExists(direct_path)
        return direct_path
    end if

    // Try to build from source if configured
    if config.get_fallback_to_source(cfg)
        let port_result = port.find(pkg_arg)
        if port_result.success
            color.print_info("No binary package found for " + pkg_arg + ", building from source...")
            let build_cmd = "coral build -y " + pkg_arg
            let build_pid = process.process_spawn_shell(build_cmd)
            if build_pid > 0
                let build_exit = process.process_wait(build_pid)
                if build_exit == 0
                    // Build succeeded, look for the package it produced
                    let p = port_result.port
                    let built_pkg = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
                    let built_path = path.join_path(packages_dir, built_pkg)
                    if file.fileExists(built_path)
                        return built_path
                    end if
                end if
            end if
            color.print_warning("Source build failed for " + pkg_arg + ", trying repositories...")
        end if
    end if

    // Try to find in repositories and download
    let remote_result = repository.find_remote_package(pkg_arg)
    if remote_result.success
        let remote_pkg = remote_result.pkg
        color.print_info("Found " + pkg_arg + " " + remote_pkg.version + " in repository: " + remote_pkg.repo_name)

        // Download the package
        let downloaded_path = repository.download_package(remote_pkg)
        if str.length(downloaded_path) > 0
            // Verify checksum if available
            if str.length(remote_pkg.sha256) > 0
                color.print_info("Verifying checksum...")
                if not checksum.verify_checksum(downloaded_path, remote_pkg.sha256)
                    color.print_error("Checksum verification failed!")
                    return ""
                end if
                color.print_success("Checksum verified")
            end if

            // Verify signature if required or available
            let require_signed = config.get_require_signed_packages(cfg)
            if require_signed or str.length(remote_pkg.signature) > 0
                let sig_path = downloaded_path + ".sig"

                // Download signature file if we don't have it
                if not file.fileExists(sig_path) and str.length(remote_pkg.signature) > 0
                    color.print_info("Downloading signature...")
                    let sig_url = get_sig_url(remote_pkg)
                    if not download_signature(sig_url, sig_path)
                        if require_signed
                            color.print_error("Failed to download package signature")
                            return ""
                        else
                            color.print_warning("Could not download signature, skipping verification")
                        end if
                    end if
                end if

                // Verify signature if we have it
                if file.fileExists(sig_path)
                    color.print_info("Verifying signature...")
                    let verify_result = signing.verify_package(downloaded_path, sig_path)
                    if verify_result.success and verify_result.valid
                        color.print_success("Signature verified (key: " + verify_result.keyid + ")")
                    else
                        if require_signed
                            color.print_error("Signature verification failed: " + verify_result.error)
                            return ""
                        else
                            color.print_warning("Signature verification failed: " + verify_result.error)
                        end if
                    end if
                elif require_signed
                    color.print_error("Package signature required but not available")
                    return ""
                end if
            end if

            return downloaded_path
        end if
    end if

    return ""
end resolve_package_path

// Create a temporary extraction directory
fn create_extract_dir(cfg: config.Config): string
    let work_dir = config.get_work_dir(cfg)
    let pid = process.getpid()
    let extract_dir = path.join_path(work_dir, "install_" + int_to_str(pid))

    if dir.dir_exists(extract_dir)
        // Clean existing
        let cmd = "rm -rf \"" + extract_dir + "\""
        let rm_pid = process.process_spawn_shell(cmd)
        if rm_pid > 0
            process.process_wait(rm_pid)
        end if
    end if

    if not dir.create_dir_all(extract_dir)
        return ""
    end if

    return extract_dir
end create_extract_dir

// Cleanup extraction directory
fn cleanup_extract_dir(dir_path: string): bool
    if str.length(dir_path) == 0
        return true
    end if

    let cmd = "rm -rf \"" + dir_path + "\""
    let pid = process.process_spawn_shell(cmd)
    if pid < 0
        return false
    end if
    process.process_wait(pid)
    return true
end cleanup_extract_dir

// Get installation root from config (can be overridden with --root)
fn get_root(cfg: config.Config): string
    let root = config.get_root(cfg)
    if str.length(root) == 0
        return "/"
    end if
    return root
end get_root

// Check for file conflicts with other installed packages
// TODO: Implement efficient conflict detection using a file database
fn check_conflicts(files: [string], file_count: int, pkg_name: string): bool
    // Skip conflict checking for performance - file overwrites are handled
    // by the manifest-driven installer and dependencies are managed separately
    return true
end check_conflicts

// Helper to create InstalledPackage
fn new_installed_pkg(info: types.PackageInfo, files: [string], file_count: int): types.InstalledPackage
    mut pkg = types.new_installed_package()
    pkg.info = info
    pkg.install_date = get_date_string()
    pkg.install_reason = "explicit"
    pkg.files = files
    pkg.files_count = file_count
    return pkg
end new_installed_pkg

// Get current date as string (simplified)
fn get_date_string(): string
    // Use date command for simplicity
    // TODO: Use proper time functions when available
    return "2025-01-24"
end get_date_string

// Get installed package info (root-aware)
fn get_old_installed(name: string, root_path: string): types.InstalledPackage
    if str.length(root_path) > 0
        return database.get_installed_rooted(name, root_path)
    end if
    return database.get_installed(name)
end get_old_installed

// Get old file list for a package (root-aware)
fn get_old_files(name: string, files: [string], max_count: int, root_path: string): int
    if str.length(root_path) > 0
        return database.get_files_rooted(name, files, max_count, root_path)
    end if
    return database.get_files(name, files, max_count)
end get_old_files

// Load old manifest from database for upgrade config protection
fn load_old_manifest(name: string, root_path: string, entries: [mtree.ManifestEntry], max_count: int): int
    let manifest_path = database.get_manifest_path(name)
    if not file.fileExists(manifest_path)
        return 0
    end if

    let content = file.readFile(manifest_path)
    if str.length(content) == 0
        return 0
    end if

    return mtree.parse_manifest(content, entries, max_count)
end load_old_manifest

// Helper: convert int to string
fn int_to_str(n: int): string
    if n == 0
        return "0"
    end if

    mut negative = false
    mut value = n
    if n < 0
        negative = true
        value = 0 - n
    end if

    mut result = ""
    while value > 0
        let digit = value % 10
        result = str.concat(str.substring("0123456789", digit, 1), result)
        value = value / 10
    end while

    if negative
        result = str.concat("-", result)
    end if

    return result
end int_to_str

// Build signature URL from remote package info
fn get_sig_url(pkg: repository.RemotePackage): string
    mut base_url = pkg.repo_url
    if not str.ends_with(base_url, "/")
        base_url = base_url + "/"
    end if

    // If signature field contains a full URL, use it
    if str.starts_with(pkg.signature, "http")
        return pkg.signature
    end if

    // Otherwise construct from filename
    return base_url + "packages/" + pkg.filename + ".sig"
end get_sig_url

// Download a signature file
fn download_signature(url: string, dest: string): bool
    return uhttp.download(url, dest)
end download_signature

end module
