/*
Kam/src/cmds/repo.rs

Repository helper: implement pacman-style remote search and download:
- -Ss <term>  -> search remote catalog
- -S  <module> -> download latest release ZIP for module

This module is intentionally compact and uses the blocking reqwest client
and indicatif progress bars to provide a simple, user-friendly interface.

Notes:
*/

use crate::errors::KamError;
use crate::utils::Utils;
use clap::{Args, Subcommand};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
use reqwest::header::USER_AGENT;
use serde::Deserialize;
use std::fs::File;
use std::io::IsTerminal;
use std::io::{Read, Write, stdin, stdout};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use std::time::Duration;

mod repo_search;

const BASE_URL: &str = "https://modules.kernelsu.org";
const SEARCH_INDEX_PATH: &str = "/search-index.json";
const MODULE_JSON_PREFIX: &str = "/module/"; // {id}.json appended

/// CLI args for the `kam repo` subcommand
#[derive(Args, Debug, Clone)]
pub struct RepoArgs {
    /// Subcommands for repo (e.g., `repo sync`)
    #[command(subcommand)]
    pub command: Option<RepoCommand>,

    /// Pacman-style sync (download) flag (equivalent to pacman -S)
    #[arg(short = 'S', long = "sync")]
    pub sync: bool,

    /// Pacman-style search modifier (use with -S as '-Ss' to search or '-s' alone to search)
    #[arg(short = 's', long = "search")]
    pub search: bool,

    // `--modules-url` flag is handled at top-level (global). Local override removed to avoid duplicate long option.
    /// Suppress progress output (quiet mode)
    #[arg(short = 'q', long = "quiet")]
    pub quiet: bool,

    /// Positional targets: module IDs or search terms (used with -S / -s)
    #[arg(value_name = "TARGETS", num_args = 0..)]
    pub targets: Vec<String>,
}

/// Entrypoint for `kam repo` subcommand.
#[derive(Subcommand, Debug, Clone)]
pub enum RepoCommand {
    /// Sync (refresh) the index cache from remote (similar to pacman -Sy)
    Sync(SyncArgs),
    /// Search the module repository
    Search(SearchArgs),
    /// Download one or more modules from the repository
    Download(DownloadArgs),
}

/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public struct 'SyncArgs'. Replace with real documentation. (2025-12-30T22:23:21Z)
#[derive(Args, Debug, Clone)]
pub struct SyncArgs {
    /// Force refresh even if cached
    #[arg(long = "force")]
    pub force: bool,

    /// Number of concurrent module index fetch jobs (overrides env KAM_REPO_CONCURRENCY)
    #[arg(short = 'j', long = "jobs", value_name = "N")]
    pub jobs: Option<usize>,

    // Per-sync `--modules-url` override removed; use the global/top-level setting instead.
    /// Suppress progress output (quiet mode)
    #[arg(short = 'q', long = "quiet")]
    pub quiet: bool,
}

/// Arguments for `kam repo search`.
#[derive(Args, Debug, Clone)]
pub struct SearchArgs {
    /// Search terms to match against the remote module catalog
    #[arg(value_name = "QUERY", required = true, num_args = 1..)]
    pub query: Vec<String>,
}

/// Arguments for `kam repo download`.
#[derive(Args, Debug, Clone)]
pub struct DownloadArgs {
    /// Module IDs to download
    #[arg(value_name = "MODULE", required = true, num_args = 1..)]
    pub modules: Vec<String>,

    /// Assume "yes" to all confirmation prompts
    #[arg(short = 'y', long = "yes")]
    pub assume_yes: bool,

    /// Suppress progress output (quiet mode)
    #[arg(short = 'q', long = "quiet")]
    pub quiet: bool,
}

/// Entrypoint for `kam repo` subcommand.
///
/// The original `run(args)` signature is preserved for backwards compatibility
/// and forwards to `run_with_modules_url` which accepts an optional
/// `modules_url` override (useful when callers want to forward the
/// top-level `--modules-url` value into the repo command).
///
/// # Errors
/// Returns `KamError` when network, I/O, or parsing operations fail
/// (for example: `FetchFailed`, `Io`, or `PackageNotFound`).
pub fn run(args: RepoArgs) -> Result<(), KamError> {
    run_with_modules_url(args, None)
}

/// Entrypoint variant that accepts a global/top-level `modules_url` override.
///
/// Callers that wish to forward the top-level `--modules-url` flag into the
/// repo command should use this variant. The implementation respects the
/// override for both `repo sync` and pacman-style `-S`/`-s` handling.
///
/// # Errors
/// Returns `KamError` on failures performing network requests, parsing remote
/// payloads, or underlying I/O operations (for example: `FetchFailed`, `Io`).
pub fn run_with_modules_url(args: RepoArgs, modules_url: Option<&str>) -> Result<(), KamError> {
    // If a nested repo subcommand is provided, handle it first (e.g., `repo sync`)
    if let Some(command) = args.command {
        return match command {
            RepoCommand::Sync(sync_args) => {
                // Determine effective base URL (allow a forwarded override)
                let base = effective_base_url(modules_url);
                // Use the CLI-provided jobs value (if any) to control concurrency for this sync.
                // Pass through the quiet flag so the sync implementation can suppress progress output.
                repo_sync_with_jobs(&base, sync_args.force, sync_args.jobs, sync_args.quiet)
            }
            RepoCommand::Search(search_args) => handle_pacman_style(
                false,
                true,
                &search_args.query,
                false,
                modules_url,
                args.quiet,
            ),
            RepoCommand::Download(download_args) => handle_pacman_style(
                true,
                false,
                &download_args.modules,
                download_args.assume_yes,
                modules_url,
                download_args.quiet,
            ),
        };
    }

    // fallback to existing pacman-style handling (-S / -s)
    handle_pacman_style(
        args.sync,
        args.search,
        &args.targets,
        false,
        modules_url,
        args.quiet,
    )
}

#[derive(Debug, Deserialize)]
struct SearchEntry {
    pub name: String,
    pub description: Option<String>,
    pub summary: Option<String>,
    pub authors: Option<String>,
    pub url: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ModuleDetail {
    pub module_id: String,
    pub module_name: Option<String>,
    pub url: Option<String>,
    pub homepage_url: Option<String>,
    pub authors: Option<Vec<Author>>,
    pub latest_release: Option<String>,
    pub latest_release_time: Option<String>,
    pub releases: Option<Vec<Release>>,
    pub summary: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Author {
    name: String,
    link: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Release {
    name: Option<String>,
    #[serde(rename = "releaseAssets")]
    assets: Option<Vec<Asset>>,
    version: Option<String>,
    created_at: Option<String>,
    published_at: Option<String>,
    updated_at: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Asset {
    name: String,
    content_type: Option<String>,
    download_url: String,
    size: Option<u64>,
}

/// Handle pacman-style flags invoked at the top-level CLI.
///
/// Semantics:
/// - If `search` is true (e.g. user passed `-s` or `-Ss`) then treat the
///   joined `targets` as the search query and perform remote search.
/// - Else if `sync` is true (user passed `-S`) then treat `targets` as a list
///   of module ids to download (latest release ZIP).
///
/// # Errors
/// Returns `KamError` when network, I/O, or parsing operations fail
/// (for example: `FetchFailed`, `Io`).
#[allow(clippy::fn_params_excessive_bools)]
pub fn handle_pacman_style(
    sync: bool,
    search: bool,
    targets: &[String],
    yes: bool,
    modules_url: Option<&str>,
    quiet: bool,
) -> Result<(), KamError> {
    // Respect explicit --yes/-y flag passed from CLI (as param) or environment arg fallback
    let assume_yes = yes || std::env::args().any(|a| a == "-y" || a == "--yes");
    // Determine effective base URL (override -> env -> config -> default)
    let base = effective_base_url(modules_url);

    // Search mode (supports fuzzy search)
    if search {
        let q = targets.join(" ");
        if q.is_empty() {
            return Err(KamError::CommandFailed(
                "Search requires a query e.g. `-Ss <term>`".into(),
            ));
        }
        return search_remote(&q, &base);
    }

    // Download mode with interactive confirmation per-target
    if sync {
        if targets.is_empty() {
            return Err(KamError::CommandFailed(
                "Download requires a module id(s), e.g. `-S <moduleId>`".into(),
            ));
        }

        let client = Client::builder()
            .timeout(Duration::from_secs(30))
            .build()
            .map_err(|e| KamError::FetchFailed(format!("Failed to build HTTP client: {e}")))?;

        for module_id in targets {
            let section_title = trf!("repo.download", module_id);
            if !quiet {
                Utils::section(&section_title);
            }

            // Fetch module details
            let _md = match fetch_module_detail(&client, module_id, &base) {
                Ok(md) => {
                    // Process the module normally
                    process_module_download(&md, module_id, &client, assume_yes, quiet)?;
                    continue;
                }
                Err(KamError::FetchFailed(ref e))
                    if e.contains("404") || e.contains("not found") || e.contains("Not Found") =>
                {
                    // If the exact module wasn't found, suggest similar modules via interactive search
                    Utils::warn(trf!("repo.module_not_found_showing_similar", module_id));
                    if let Some(selected_module) = search_remote_interactive(module_id, &base)? {
                        Utils::info(trf!("repo.selected_module", selected_module));
                        // Now fetch the details for the selected module
                        let md = fetch_module_detail(&client, &selected_module, &base)?;
                        // Continue with the download process using the selected module
                        process_module_download(&md, &selected_module, &client, assume_yes, quiet)?;
                    } else {
                        Utils::info(crate::i18n::tr("repo.skipped_selection"));
                    }
                    continue;
                }
                Err(e) => return Err(e),
            };
        }

        return Ok(());
    }

    // Nothing to do
    Ok(())
}

/// Helper function to process module download with asset selection and confirmation
fn parse_confirm_input(input: &str, default_yes: bool) -> bool {
    let s = input.trim();
    if s.is_empty() {
        default_yes
    } else {
        let s = s.to_ascii_lowercase();
        s == "y" || s == "yes"
    }
}

fn select_zip_asset(md: &ModuleDetail) -> Option<(&Asset, &str)> {
    if let Some(rels) = md.releases.as_ref() {
        for r in rels {
            if let Some(assets) = r.assets.as_ref()
                && let Some(a) = assets.iter().find(|x| {
                    x.content_type
                        .as_deref()
                        .is_some_and(|ct| ct.to_lowercase().contains("zip"))
                        || x.name.to_lowercase().ends_with(".zip")
                })
            {
                let release_label = r
                    .name
                    .as_deref()
                    .or(r.version.as_deref())
                    .unwrap_or("latest");
                return Some((a, release_label));
            }
        }
    }
    None
}

fn prompt_confirm_download(
    module_id: &str,
    asset_name: &str,
    assume_yes: bool,
) -> Result<bool, KamError> {
    if assume_yes {
        return Ok(true);
    }
    print!("{}", trf!("repo.confirm_download", module_id, asset_name));
    stdout().flush().map_err(KamError::Io)?;
    let mut input = String::new();
    stdin().read_line(&mut input).map_err(KamError::Io)?;
    let input_trimmed = input.trim();
    let ok = parse_confirm_input(input_trimmed, false);
    if !ok {
        Utils::warn(trf!("repo.skipped_download", module_id));
    }
    Ok(ok)
}

fn process_module_download(
    md: &ModuleDetail,
    module_id: &str,
    client: &Client,
    assume_yes: bool,
    quiet: bool,
) -> Result<(), KamError> {
    // Select an asset (first zip-like asset in releases)
    let Some((asset, release_label)) = select_zip_asset(md) else {
        Utils::warn(trf!("repo.no_downloadable_zip_asset", module_id));
        return Ok(());
    };

    // Print detailed info and ask for confirmation unless assume_yes
    let size_str = asset
        .size
        .map(|s| format!(" ({})", format_size(s)))
        .unwrap_or_default();
    println!(
        "{}",
        trf!(
            "repo.module_detail.title",
            md.module_id.as_str(),
            md.module_name.as_deref().unwrap_or("")
        )
    );

    // Additional module metadata (if present)
    if let Some(u) = md.url.as_deref()
        && !u.trim().is_empty()
    {
        println!("{}", trf!("repo.module_detail.url", u));
    }

    if let Some(h) = md.homepage_url.as_deref()
        && !h.trim().is_empty()
    {
        println!("{}", trf!("repo.module_detail.homepage", h));
    }

    if let Some(s) = md.summary.as_deref()
        && !s.trim().is_empty()
    {
        println!("{}", trf!("repo.module_detail.summary", s));
    }

    if let Some(auths) = md.authors.as_ref()
        && !auths.is_empty()
    {
        let authors_str = auths
            .iter()
            .map(|a| {
                if let Some(link) = a.link.as_deref()
                    && !link.trim().is_empty()
                {
                    return format!("{name} ({link})", name = a.name, link = link);
                }
                a.name.clone()
            })
            .collect::<Vec<_>>()
            .join(", ");
        println!("{}: {}", trf!("repo.authors"), authors_str);
    }

    println!("{}", trf!("repo.module_detail.release", release_label));
    println!("{}", trf!("repo.module_detail.asset", asset.name, size_str));
    println!(
        "{}",
        trf!("repo.module_detail.download_url", asset.download_url)
    );

    let confirmed = prompt_confirm_download(module_id, &asset.name, assume_yes)?;

    if !confirmed {
        return Ok(());
    }

    // Proceed to download
    match download_asset(client, asset, None, quiet) {
        Ok(path) => {
            if !quiet {
                Utils::success(trf!("repo.saved", path.display().to_string()));
            }
        }
        Err(e) => {
            {
                // Avoid referencing a temporary (`&e.to_string()`) directly in the slice.
                // Build owned strings and pass a slice of `&dyn Display` to `tr_fmt`.
                let err_str = e.to_string();
                let args: Vec<&dyn std::fmt::Display> = vec![&module_id, &err_str];
                Utils::error(crate::i18n::tr_fmt("repo.failed_to_download", &args));
            }
        }
    }

    Ok(())
}

/// Search the remote catalog (search-index.json) for the provided query
/// (case-insensitive substring match across name/description/summary/authors).
pub(crate) fn cache_root_dir() -> Result<PathBuf, KamError> {
    // Allow override for tests or custom installs via env var.
    if let Ok(dir) = std::env::var("KAM_CACHE_DIR") {
        let p = PathBuf::from(dir);
        std::fs::create_dir_all(&p)?;
        return Ok(p);
    }

    // Default to user-level Kam directory (defaults to ~/.kam). Respect KAM_HOME via crate::utils::kam_home_dir().
    let base = crate::utils::kam_home_dir()?;
    std::fs::create_dir_all(&base)?;
    Ok(base)
}

fn sanitize_filename(s: &str) -> String {
    s.chars()
        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
        .collect()
}

fn index_cache_path(base_url: &str) -> Result<PathBuf, KamError> {
    let mut p = cache_root_dir()?;
    let fname = format!("index_{}.json", sanitize_filename(base_url));
    p.push(fname);
    Ok(p)
}

fn module_cache_path(module_id: &str) -> Result<PathBuf, KamError> {
    let mut p = cache_root_dir()?;
    p.push("modules");
    std::fs::create_dir_all(&p)?;
    p.push(format!("{module_id}.json"));
    Ok(p)
}

fn write_atomic(path: &Path, contents: &str) -> Result<(), KamError> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension("tmp");
    std::fs::write(&tmp, contents)?;
    std::fs::rename(&tmp, path)?;
    Ok(())
}

fn try_find_index_in_cache_dir(dir: &Path) -> Option<Vec<SearchEntry>> {
    // Look for files matching index_*.json and pick the most recently modified one that parses.
    let mut candidates: Vec<(std::time::SystemTime, PathBuf)> = Vec::new();
    if let Ok(rd) = std::fs::read_dir(dir) {
        for e in rd.flatten() {
            let p = e.path();
            if p.is_file()
                && let Some(name) = p.file_name().and_then(|n| n.to_str())
                && name.starts_with("index_")
                && std::path::Path::new(name)
                    .extension()
                    .is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
                && let Ok(meta) = p.metadata()
                && let Ok(m) = meta.modified()
            {
                candidates.push((m, p));
            }
        }
    }
    // Sort by modified desc
    candidates.sort_by_key(|candidate| std::cmp::Reverse(candidate.0));
    for (_mtime, p) in candidates {
        if let Ok(buf) = std::fs::read_to_string(&p)
            && let Ok(entries) = serde_json::from_str::<Vec<SearchEntry>>(&buf)
        {
            return Some(entries);
        }
    }
    None
}

fn fetch_index_cached(client: &Client, base_url: &str) -> Result<Vec<SearchEntry>, KamError> {
    let path = index_cache_path(base_url)?;
    let force_refresh = std::env::var("KAM_FORCE_INDEX_REFRESH").is_ok();

    // 1) If a cached index exists and no explicit force refresh is requested, try it first.
    if path.exists()
        && !force_refresh
        && let Ok(buf) = std::fs::read_to_string(&path)
        && let Ok(entries) = serde_json::from_str::<Vec<SearchEntry>>(&buf)
    {
        return Ok(entries);
    }
    // corrupted or incompatible cache; we'll try fallback scan or network below

    // 2) Try to find an alternative cached index in the same cache directory (index_*.json)
    if let Some(parent) = path.parent()
        && let Some(entries) = try_find_index_in_cache_dir(parent)
    {
        return Ok(entries);
    }

    // 3) Attempt network fetch; if network fails or non-success status, try fallback cache scan before returning error.
    let url = format!("{base_url}{SEARCH_INDEX_PATH}");
    match client
        .get(&url)
        .header(USER_AGENT, "kam/repo-search")
        .send()
    {
        Ok(resp) => {
            if !resp.status().is_success() {
                // network returned non-success; try fallback cache scan
                if let Some(parent) = path.parent()
                    && let Some(entries) = try_find_index_in_cache_dir(parent)
                {
                    return Ok(entries);
                }
                return Err(KamError::FetchFailed(format!(
                    "{url} returned status {}",
                    resp.status()
                )));
            }
            let body = resp
                .text()
                .map_err(|e| KamError::FetchFailed(format!("Failed to read {url} body: {e}")))?;
            let entries: Vec<SearchEntry> = serde_json::from_str(&body)
                .map_err(|e| KamError::Json(format!("Failed to parse {url} JSON: {e}")))?;
            // Write to primary cache path (atomic)
            let _ = write_atomic(&path, &body);
            Ok(entries)
        }
        Err(e) => {
            // Network error - try fallback cache scan
            if let Some(parent) = path.parent()
                && let Some(entries) = try_find_index_in_cache_dir(parent)
            {
                return Ok(entries);
            }
            Err(KamError::FetchFailed(format!("GET {url} failed: {e}")))
        }
    }
}

/// Resolve a possibly-relative entry URL to an absolute URL using the registry base URL.
fn resolve_entry_url(base_url: &str, url: &str) -> String {
    let u = url.trim();
    if u.starts_with("http://") || u.starts_with("https://") {
        return u.to_string();
    }
    if u.starts_with('/') {
        return format!("{}{}", base_url.trim_end_matches('/'), u);
    }
    format!("{}/{}", base_url.trim_end_matches('/'), u)
}

/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public function 'repo_sync_with_jobs'. Replace with real documentation. (2025-12-30T22:23:21Z)
/// # Errors
/// Returns `KamError` when network, I/O, or JSON parsing operations fail.
#[allow(clippy::literal_string_with_formatting_args)]
#[allow(clippy::too_many_lines)]
pub fn repo_sync_with_jobs(
    base_url: &str,
    force: bool,
    jobs: Option<usize>,
    quiet: bool,
) -> Result<(), KamError> {
    // Always attempt to fetch the latest index from the remote and write it to cache.
    // `jobs` overrides env var and default parallelism to control number of worker threads.
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .map_err(|e| KamError::FetchFailed(format!("Failed to build HTTP client: {e}")))?;

    let op_start = std::time::Instant::now();
    let url = format!("{base_url}{SEARCH_INDEX_PATH}");
    let mut resp = client
        .get(&url)
        .header(USER_AGENT, "kam/repo-sync")
        .send()
        .map_err(|e| KamError::FetchFailed(format!("GET {url} failed: {e}")))?;

    if !resp.status().is_success() {
        return Err(KamError::FetchFailed(format!(
            "{} returned status {}",
            url,
            resp.status()
        )));
    }

    // Whether we should show progress bars at all (respect quiet and TTY)
    let show_progress = !quiet && std::io::stdout().is_terminal();

    // Show a progress indicator while fetching the master index (hidden when quiet)
    let index_len = resp.content_length();
    let index_pb = if show_progress {
        index_len.map_or_else(
            || {
                let pb = ProgressBar::new_spinner();
                pb.set_style(
                    ProgressStyle::with_template(
                        "{spinner} Fetching index {bytes}/{total_bytes} ({eta})",
                    )
                    .unwrap_or_else(|_| ProgressStyle::default_bar()),
                );
                pb.enable_steady_tick(Duration::from_millis(100));
                pb
            },
            |len| {
                let pb = ProgressBar::new(len);
                pb.set_style(
                    ProgressStyle::with_template(
                        "{spinner} Fetching index {bytes}/{total_bytes} ({eta})",
                    )
                    .unwrap_or_else(|_| ProgressStyle::default_bar()),
                );
                pb.enable_steady_tick(Duration::from_millis(100));
                pb
            },
        )
    } else {
        ProgressBar::hidden()
    };

    // Read the response body in chunks so the progress bar moves (hidden if quiet)
    let mut buf: Vec<u8> = Vec::new();
    let mut tmp = [0u8; 8 * 1024];
    loop {
        let n = resp
            .read(&mut tmp)
            .map_err(|e| KamError::FetchFailed(format!("Failed to read {url} body: {e}")))?;
        if n == 0 {
            break;
        }
        buf.extend_from_slice(&tmp[..n]);
        index_pb.inc(n as u64);
    }
    index_pb.finish();

    let body = String::from_utf8(buf)
        .map_err(|e| KamError::Json(format!("Failed to parse {url} body as UTF-8: {e}")))?;

    let path = index_cache_path(base_url)?;
    write_atomic(&path, &body)?;

    if !quiet {
        if force {
            let msg = trf!("repo.index_force_synced", url, path.display().to_string());
            Utils::success(&msg);
        } else {
            Utils::success(trf!("repo.index_synced", path.display().to_string()));
        }
    }

    // Parse the index and, if present, attempt to fetch each module's individual index with per-module progress
    let entries: Vec<SearchEntry> = match serde_json::from_str(&body) {
        Ok(v) => v,
        Err(e) => {
            return Err(KamError::Json(format!("Failed to parse {url} JSON: {e}")));
        }
    };

    if entries.is_empty() {
        return Ok(());
    }
    // Report how long it took to resolve the index into module entries
    let duration_ms = op_start.elapsed().as_millis();
    if !quiet {
        Utils::info(format!(
            "Resolved {count} modules in {ms}ms",
            count = entries.len(),
            ms = duration_ms
        ));
    }

    // MultiProgress will nicely render the master spinner above per-module bars.
    let mp = MultiProgress::new();
    // Use a visible spinner only when we actually want to show progress (not quiet and TTY).
    let top_pb = if show_progress {
        let pb = mp.add(ProgressBar::new_spinner());
        pb.set_style(
            ProgressStyle::with_template("{spinner} Preparing modules... ({pos}/{len})")
                .unwrap_or_else(|_| ProgressStyle::default_bar()),
        );
        pb.set_length(entries.len() as u64);
        pb.enable_steady_tick(Duration::from_millis(80));
        pb
    } else {
        // Hidden spinner so later `.inc()` calls are safe but nothing is rendered.
        ProgressBar::hidden()
    };
    // Count how many modules actually need fetching (not cached or forced)
    let mut need_fetch_count: usize = 0;
    for e in &entries {
        let module_cache = module_cache_path(&e.name);
        if let Ok(p) = &module_cache
            && !force
            && p.exists()
        {
            continue;
        }
        need_fetch_count += 1;
    }

    // If nothing requires network fetch, show a concise 'Everything up to date' and return.
    if need_fetch_count == 0 {
        // If quiet mode, just finish the progress spinner and return silently.
        if quiet {
            top_pb.finish();
            return Ok(());
        }
        let msg = crate::i18n::tr("repo.everything_up_to_date");
        Utils::success(&msg);
        top_pb.finish_with_message(msg);
        return Ok(());
    }

    // Prepare up to MAX_VISIBLE visible bars; subsequent modules are kept hidden (no visual).
    // Determine number of workers (CLI `--jobs` > env `KAM_REPO_CONCURRENCY` > default=core count).
    let default_workers =
        std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
    let mut num_workers = match jobs {
        Some(j) if j > 0 => j,
        _ => std::env::var("KAM_REPO_CONCURRENCY")
            .ok()
            .and_then(|s| s.parse::<usize>().ok())
            .filter(|&n| n > 0)
            .unwrap_or(default_workers),
    };
    // Don't spawn more workers than modules
    num_workers = std::cmp::min(num_workers, entries.len());
    let display_limit = num_workers;

    // Build per-module progress bars and a list of fetch tasks; visible entries limited to `display_limit`.
    let mut tasks: Vec<(String, String, PathBuf, ProgressBar)> = Vec::with_capacity(entries.len());
    let mut visible_cnt: usize = 0;
    let updated_count = Arc::new(AtomicUsize::new(0));

    for e in &entries {
        let module_id = e.name.clone();
        let module_cache = module_cache_path(&module_id);

        // Decide whether to skip fetching (cached & not forcing)
        let cached = matches!((&module_cache, force), (Ok(p), false) if p.exists());

        if visible_cnt < display_limit {
            // visible progress bar (or hidden placeholder when quiet)
            let pb = if show_progress {
                let p = mp.add(ProgressBar::new_spinner());
                p.set_style(
                    ProgressStyle::with_template(
                        "{msg:20} {bar:40.cyan/blue} {bytes}/{total_bytes}",
                    )
                    .unwrap_or_else(|_| ProgressStyle::default_bar()),
                );
                p.set_message(format!("{module_id:20}"));
                p
            } else {
                // Hidden progress bar so later operations still work but nothing is shown.
                let p = ProgressBar::hidden();
                p.set_message(format!("{module_id:20}"));
                p
            };

            if cached {
                // show cached as full bar immediately
                pb.set_length(1);
                pb.set_position(1);
                pb.finish_with_message(format!("{module_id:20} (cached)"));
                visible_cnt += 1;
                top_pb.inc(1);
                continue;
            }

            // needs fetch - add as visible task (or hidden placeholder when quiet)
            let murl = format!("{base_url}{MODULE_JSON_PREFIX}{module_id}.json");
            if let Ok(p) = module_cache {
                tasks.push((module_id, murl, p, pb));
            } else {
                // if we couldn't compute cache path, mark as done
                pb.finish_with_message(format!("{module_id:20}"));
                top_pb.inc(1);
            }
            visible_cnt += 1;
        } else {
            // hidden (not displayed) modules
            if cached {
                top_pb.inc(1);
            } else {
                // fetch but hide progress bar
                let pb = ProgressBar::hidden();
                let murl = format!("{base_url}{MODULE_JSON_PREFIX}{module_id}.json");
                if let Ok(p) = module_cache {
                    tasks.push((module_id, murl, p, pb));
                } else {
                    top_pb.inc(1);
                }
            }
        }
    }

    // Dispatch tasks to worker threads using a channel-backed thread-pool (dynamic scheduling).
    if !tasks.is_empty() {
        // `num_workers` was determined earlier and equals the visible count; ensure at least 1 worker
        let num_workers = std::cmp::max(1, visible_cnt.min(entries.len()));

        // Send all tasks into a channel consumed by workers (dynamic work-stealing via channel clones)
        let (tx, rx) = crossbeam_channel::unbounded::<(String, String, PathBuf, ProgressBar)>();
        for t in tasks {
            let _ = tx.send(t);
        }
        // Close the sender to signal workers when done
        drop(tx);

        // Spawn exactly `num_workers` worker threads (one per visible slot), each consuming from the receiver.
        let mut handles = Vec::new();
        for _ in 0..num_workers {
            let rx_clone = rx.clone();
            let client_clone = client.clone();
            let top = top_pb.clone();
            let updated = Arc::clone(&updated_count);
            let handle = std::thread::spawn(move || {
                while let Ok((module_id, murl, ppath, pb)) = rx_clone.recv() {
                    // Defensive check: if cache exists and not forcing, treat as cached
                    if ppath.exists() && !force {
                        pb.finish_with_message(format!("{module_id:20} (cached)"));
                        top.inc(1);
                        continue;
                    }

                    match client_clone
                        .get(&murl)
                        .header(USER_AGENT, "kam/repo-sync-module")
                        .send()
                    {
                        Ok(mut r) => {
                            if !r.status().is_success() {
                                Utils::warn(format!(
                                    "{murl} returned status {status}",
                                    status = r.status()
                                ));
                                pb.finish_with_message(format!("{module_id:20} (failed)"));
                                top.inc(1);
                                continue;
                            }
                            if let Some(len) = r.content_length() {
                                pb.set_length(len);
                            }

                            let mut mbuf: Vec<u8> = Vec::new();
                            let mut tmp2 = [0u8; 8 * 1024];
                            loop {
                                match r.read(&mut tmp2) {
                                    Ok(n) if n > 0 => {
                                        mbuf.extend_from_slice(&tmp2[..n]);
                                        pb.inc(n as u64);
                                    }
                                    Ok(_) => break,
                                    Err(e) => {
                                        Utils::warn(format!("Failed to read {murl} body: {e}"));
                                        pb.finish_with_message(format!("{module_id:20} (failed)"));
                                        break;
                                    }
                                }
                            }

                            // Validate and write cache atomically
                            if let Ok(s) = String::from_utf8(mbuf) {
                                if let Err(e) = write_atomic(&ppath, &s) {
                                    Utils::warn(format!("Failed to write cache {module_id}: {e}"));
                                    pb.finish_with_message(format!("{module_id:20} (failed)"));
                                } else {
                                    updated.fetch_add(1, Ordering::SeqCst);
                                    pb.finish_with_message(format!("{module_id:20}"));
                                }
                            } else {
                                Utils::warn(format!("Failed to parse {murl} body as UTF-8"));
                                pb.finish_with_message(format!("{module_id:20} (invalid)"));
                            }
                        }
                        Err(e) => {
                            Utils::warn(format!("GET {murl} failed: {e}"));
                            pb.finish_with_message(format!("{module_id:20} (failed)"));
                        }
                    }

                    top.inc(1);
                }
            });
            handles.push(handle);
        }

        // Wait for all workers to finish
        for h in handles {
            let _ = h.join();
        }
    }

    // If the workers did not actually update anything, tell the user that everything is up to date.
    let updated_total = updated_count.load(Ordering::SeqCst);
    if !quiet {
        if updated_total == 0 {
            let msg = crate::i18n::tr("repo.everything_up_to_date");
            Utils::success(msg);
        } else {
            // Always show a concise summary so users see an effect even when progress bars
            // are suppressed (quiet/non-tty).
            Utils::success(trf!("repo.updated_modules", updated_total.to_string()));
        }
    }

    top_pb.finish();
    Ok(())
}

/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public function 'search_remote'. Replace with real documentation. (2025-12-30T22:23:21Z)
/// # Errors
/// Returns `KamError` on network, I/O, or JSON parsing failures.
pub fn search_remote(query: &str, base_url: &str) -> Result<(), KamError> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .build()
        .map_err(|e| KamError::FetchFailed(format!("Failed to build HTTP client: {e}")))?;

    // Use cached index if available & fresh; otherwise fetch and cache.
    let entries = fetch_index_cached(&client, base_url)?;

    let q = query.to_lowercase().trim().to_string();
    if q.is_empty() {
        Utils::warn(crate::i18n::tr("repo.search.empty_query"));
        return Ok(());
    }

    // Score candidates using the shared scoring helper (exact substring => 1.0,
    // otherwise use token coverage/fuzzy similarity). Threshold tuned for search mode.
    let mut scored: Vec<(f64, &SearchEntry)> = entries
        .iter()
        .map(|e| (repo_search::score_search_entry(e, &q), e))
        .filter(|(score, _)| *score >= 0.60)
        .collect();

    if scored.is_empty() {
        Utils::warn(trf!("repo.no_results_for", query));
        return Ok(());
    }

    // sort by score descending
    scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));

    // Print top results
    for (i, (score, e)) in scored.iter().take(50).enumerate() {
        let desc = e.description.as_deref().unwrap_or("");
        let score_suffix = if (*score - 1.0).abs() > f64::EPSILON {
            trf!("repo.score_format", format!("{:.2}", score))
        } else {
            String::new()
        };
        println!(
            "{}",
            trf!("repo.result_line_simple", e.name, desc, score_suffix)
        );
        if let Some(s) = &e.summary {
            println!("    {s}");
        }
        if let Some(a) = &e.authors {
            println!("    {}: {a}", crate::i18n::tr("repo.authors"));
        }
        if let Some(u) = &e.url {
            let pretty = resolve_entry_url(base_url, u);
            println!("    {}: {pretty}", crate::i18n::tr("repo.url"));
        }

        // Try to fetch module details for the top few results to show version/time info
        if i < 5
            && let Ok(md) = fetch_module_detail(&client, &e.name, base_url)
        {
            // Version
            if let Some(lr) = md.latest_release.as_deref() {
                println!("    {}: {lr}", crate::i18n::tr("repo.version"));
            } else if let Some(rels) = md.releases.as_ref()
                && let Some(first) = rels.first()
                && let Some(v) = first.version.as_deref().or(first.name.as_deref())
            {
                println!("    {}: {v}", crate::i18n::tr("repo.version"));
            }
            // Time
            if let Some(lt) = md.latest_release_time.as_deref() {
                println!("    {}: {lt}", crate::i18n::tr("repo.updated"));
            } else if let Some(rels) = md.releases.as_ref()
                && let Some(first) = rels.first()
                && let Some(pub_at) = first
                    .published_at
                    .as_deref()
                    .or(first.updated_at.as_deref())
                    .or(first.created_at.as_deref())
            {
                println!("    {}: {pub_at}", crate::i18n::tr("repo.updated"));
            }
        }

        println!();
    }

    Ok(())
}

/// Search the remote catalog and allow interactive selection of results
/// # Errors
/// Returns `KamError` on network, I/O, or parsing failures.
#[allow(clippy::too_many_lines)]
pub(crate) fn search_remote_interactive(
    query: &str,
    base_url: &str,
) -> Result<Option<String>, KamError> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .build()
        .map_err(|e| KamError::FetchFailed(format!("Failed to build HTTP client: {e}")))?;

    // Use cached index if available & fresh; otherwise fetch and cache.
    let entries = fetch_index_cached(&client, base_url)?;

    let q = query.to_lowercase().trim().to_string();
    if q.is_empty() {
        Utils::warn(crate::i18n::tr("repo.search.empty_query"));
        return Ok(None);
    }

    // Score candidates using shared helper; lower threshold for interactive selection.
    let mut scored: Vec<(f64, &SearchEntry)> = entries
        .iter()
        .map(|e| (repo_search::score_search_entry(e, &q), e))
        .filter(|(score, _)| *score >= 0.30)
        .collect();

    if scored.is_empty() {
        Utils::warn(trf!("repo.no_results_for", query));
        return Ok(None);
    }

    // sort by score descending
    scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));

    // Print numbered results
    println!(
        "{}",
        trf!(
            "repo.similar_packages_header",
            scored.len().to_string(),
            query
        )
    );
    println!();
    for (i, (score, e)) in scored.iter().take(20).enumerate() {
        // Show up to 20 results
        let desc = e.description.as_deref().unwrap_or("");
        let score_suffix = if (*score - 1.0).abs() > f64::EPSILON {
            trf!("repo.score_format", format!("{:.2}", score))
        } else {
            String::new()
        };
        println!(
            "{}",
            trf!(
                "repo.result_line",
                (i + 1).to_string(),
                e.name,
                desc,
                score_suffix
            )
        );
        if let Some(s) = &e.summary {
            println!("    {s}");
        }
        if let Some(a) = &e.authors {
            println!("    {}: {a}", crate::i18n::tr("repo.authors"));
        }
        if let Some(u) = &e.url {
            let pretty = resolve_entry_url(base_url, u);
            println!("    {}: {pretty}", crate::i18n::tr("repo.url"));
        }

        // Try to fetch module details for the top few results to show version/time info
        if i < 5
            && let Ok(md) = fetch_module_detail(&client, &e.name, base_url)
        {
            if let Some(lr) = md.latest_release.as_deref() {
                println!("    {}: {}", crate::i18n::tr("repo.version"), lr);
            } else if let Some(rels) = md.releases.as_ref()
                && let Some(first) = rels.first()
                && let Some(v) = first.version.as_deref().or(first.name.as_deref())
            {
                println!("    {}: {}", crate::i18n::tr("repo.version"), v);
            }
            if let Some(lt) = md.latest_release_time.as_deref() {
                println!("    {}: {}", crate::i18n::tr("repo.updated"), lt);
            } else if let Some(rels) = md.releases.as_ref()
                && let Some(first) = rels.first()
                && let Some(pub_at) = first
                    .published_at
                    .as_deref()
                    .or(first.updated_at.as_deref())
                    .or(first.created_at.as_deref())
            {
                println!("    {}: {}", crate::i18n::tr("repo.updated"), pub_at);
            }
        }

        println!();
    }

    // Ask user to select
    print!("{}", crate::i18n::tr("repo.prompt.enter_number"));
    stdout().flush().map_err(KamError::Io)?;
    let mut input = String::new();
    stdin().read_line(&mut input).map_err(KamError::Io)?;
    let input_trimmed = input.trim();

    if input_trimmed.is_empty() {
        return Ok(None);
    }

    match input_trimmed.parse::<usize>() {
        Ok(num) if num > 0 && num <= scored.len() => {
            let selected = &scored[num - 1].1;
            Ok(Some(selected.name.clone()))
        }
        Ok(_) => {
            Utils::warn(crate::i18n::tr("repo.invalid_selection_out_of_range"));
            Ok(None)
        }
        Err(_) => {
            Utils::warn(crate::i18n::tr("repo.invalid_input_number"));
            Ok(None)
        }
    }
}

/// Format bytes into a human readable string
fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        let whole = bytes / GB;
        let frac = (bytes % GB) * 100 / GB;
        format!("{whole}.{frac:02} GiB")
    } else if bytes >= MB {
        let whole = bytes / MB;
        let frac = (bytes % MB) * 100 / MB;
        format!("{whole}.{frac:02} MiB")
    } else if bytes >= KB {
        let whole = bytes / KB;
        let frac = (bytes % KB) * 100 / KB;
        format!("{whole}.{frac:02} KiB")
    } else {
        format!("{bytes} B")
    }
}

/// Fetch module JSON details using provided client
/// Derive the effective base URL for modules registry.
///
/// Priority:
/// 1) `override_url` passed to the function call
/// 2) `KAM_MODULES_URL` environment variable
/// 3) `~/.kam/config.toml` -> `[modules] base_url = "..."`
/// 4) default builtin URL
#[must_use]
pub fn effective_base_url(override_url: Option<&str>) -> String {
    if let Some(u) = override_url
        && !u.trim().is_empty()
    {
        return u.to_string();
    }
    if let Ok(env) = std::env::var("KAM_MODULES_URL")
        && !env.trim().is_empty()
    {
        return env;
    }
    if let Ok(cfg_home) = crate::utils::kam_home_dir() {
        let cfg = cfg_home.join("config.toml");
        if cfg.exists()
            && let Ok(content) = std::fs::read_to_string(&cfg)
            && let Ok(v) = toml::from_str::<toml::Value>(&content)
            && let Some(m) = v
                .get("modules")
                .and_then(|m| m.get("base_url"))
                .and_then(|x| x.as_str())
        {
            return m.to_string();
        }
    }
    "https://modules.kernelsu.org".to_string()
}

fn fetch_module_detail(
    client: &Client,
    module_id: &str,
    base_url: &str,
) -> Result<ModuleDetail, KamError> {
    let path = module_cache_path(module_id)?;
    let force_refresh = std::env::var("KAM_FORCE_MODULE_REFRESH").is_ok();

    // Prefer cached copy if present and not explicitly forced to refresh.
    if path.exists() && !force_refresh {
        // Try to read & parse cached module JSON; if successful, return it.
        if let Ok(mut f) = File::open(&path) {
            let mut buf = String::new();
            if f.read_to_string(&mut buf).is_ok() {
                if let Ok(md) = serde_json::from_str::<ModuleDetail>(&buf) {
                    return Ok(md);
                }
                Utils::warn(format!(
                    "Cached module JSON for {module_id} could not be parsed; will attempt to refresh from registry"
                ));
            } else {
                Utils::warn(format!(
                    "Failed to read cached module JSON for {module_id}; will attempt to refresh from registry"
                ));
            }
        } else {
            // Could not open file; fall through to network fetch.
            Utils::warn(format!(
                "Failed to open cached module JSON for {module_id}; will attempt to refresh from registry"
            ));
        }
    }

    let url = format!("{base_url}{MODULE_JSON_PREFIX}{module_id}.json");
    let resp = client
        .get(&url)
        .header(USER_AGENT, "kam/repo-module")
        .send()
        .map_err(|e| KamError::FetchFailed(format!("GET {url} failed: {e}")))?;

    if !resp.status().is_success() {
        // Fallback to cached copy if available
        if path.exists() {
            let mut s = String::new();
            File::open(&path)?.read_to_string(&mut s)?;
            let md: ModuleDetail = serde_json::from_str(&s)?;
            return Ok(md);
        }
        return Err(KamError::FetchFailed(format!(
            "{url} returned status {status}",
            status = resp.status()
        )));
    }

    let body = resp
        .text()
        .map_err(|e| KamError::FetchFailed(format!("Failed to read {url} body: {e}")))?;
    let md: ModuleDetail = serde_json::from_str(&body)
        .map_err(|e| KamError::Json(format!("Failed to parse {url} JSON: {e}")))?;
    let _ = write_atomic(&path, &body);
    Ok(md)
}

/// Download the latest release ZIP for `module_id`.
///
/// If `dest_dir` is provided it will be saved there; otherwise it will be
/// saved to the current working directory. Returns the saved file path. When
/// `quiet` is true, progress output will be suppressed.
/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public function 'download_module_latest'. Replace with real documentation. (2025-12-30T22:23:21Z)
/// # Errors
/// Returns `KamError` on network, I/O, or JSON parsing failures.
pub fn download_module_latest(
    module_id: &str,
    dest_dir: Option<&Path>,
    quiet: bool,
) -> Result<PathBuf, KamError> {
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .map_err(|e| KamError::FetchFailed(format!("Failed to build HTTP client: {e}")))?;

    let url = format!("{BASE_URL}{MODULE_JSON_PREFIX}{module_id}.json");
    let resp = client
        .get(&url)
        .header(USER_AGENT, "kam/repo-module")
        .send()
        .map_err(|e| KamError::FetchFailed(format!("GET {url} failed: {e}")))?;

    if !resp.status().is_success() {
        return Err(KamError::FetchFailed(format!(
            "{} returned status {}",
            url,
            resp.status()
        )));
    }

    // Note: ModuleDetail uses camelCase -> snake_case mapping via serde.
    let md: ModuleDetail = resp
        .json()
        .map_err(|e| KamError::Json(format!("Failed to parse {url} JSON: {e}")))?;

    if let Some(rels) = md.releases.as_ref() {
        for r in rels {
            if let Some(assets) = r.assets.as_ref() {
                // Prefer zip-like assets
                if let Some(a) = assets.iter().find(|x| {
                    x.content_type
                        .as_deref()
                        .is_some_and(|ct| ct.to_lowercase().contains("zip"))
                        || x.name.to_lowercase().ends_with(".zip")
                }) {
                    return download_asset(&client, a, dest_dir, quiet);
                }
            }
        }
    }

    Err(KamError::PackageNotFound(format!(
        "No downloadable zip asset found for module {module_id}"
    )))
}

fn download_asset(
    client: &Client,
    asset: &Asset,
    dest_dir: Option<&Path>,
    quiet: bool,
) -> Result<PathBuf, KamError> {
    let url = &asset.download_url;

    let mut resp = client
        .get(url)
        .header(USER_AGENT, "kam/repo-download")
        .send()
        .map_err(|e| KamError::FetchFailed(format!("GET {url} failed: {e}")))?;

    if !resp.status().is_success() {
        return Err(KamError::FetchFailed(format!(
            "{url} returned status {status}",
            status = resp.status()
        )));
    }

    let filename = &asset.name;
    let dest = dest_dir.map_or_else(|| PathBuf::from(filename), |d| d.join(filename));

    // Try to determine size for progress
    let size = asset.size.or_else(|| resp.content_length());
    let show_progress = !quiet && std::io::stdout().is_terminal();

    let pb = if show_progress {
        size.map_or_else(ProgressBar::new_spinner, ProgressBar::new)
    } else {
        ProgressBar::hidden()
    };

    pb.set_style(
        ProgressStyle::with_template("{bar:40.cyan/blue} {bytes}/{total_bytes} ({eta})")
            .unwrap_or_else(|_| ProgressStyle::default_bar()),
    );

    let mut out = File::create(&dest).map_err(KamError::Io)?;

    let mut buf = [0u8; 8 * 1024];
    let mut written: u64 = 0;

    loop {
        let n = resp
            .read(&mut buf)
            .map_err(|e| KamError::FetchFailed(format!("Failed to read response body: {e}")))?;
        if n == 0 {
            break;
        }
        out.write_all(&buf[..n]).map_err(KamError::Io)?;
        written += n as u64;
        if size.is_some() {
            pb.set_position(written);
        } else {
            pb.inc(n as u64);
        }
    }

    pb.finish();

    Ok(dest)
}
