use semver::Version;
use serde::Serialize;
use tracing::info;

use crate::{
    cli::{OutputColorMode, OutputFormat},
    commands::print_output,
    errors::{NodeupError, Result},
    installer::InstallState,
    release_index::normalize_version,
    resolver::ResolvedRuntimeTarget,
    selectors::RuntimeSelector,
    types::RuntimeSelectorSource,
    NodeupApp,
};

#[derive(Debug, Serialize)]
struct CheckEntry {
    runtime: String,
    latest_available: Option<String>,
    has_update: bool,
}

#[derive(Debug, Serialize)]
struct UpdateEntry {
    selector: String,
    previous_runtime: Option<String>,
    updated_runtime: Option<String>,
    status: String,
}

#[derive(Debug, Clone, Copy)]
struct UpdateSelectorContext {
    source: &'static str,
    tracked_selectors: usize,
    installed_runtimes: usize,
}

pub fn check(output: OutputFormat, color: Option<OutputColorMode>, app: &NodeupApp) -> Result<i32> {
    let installed = app.store.list_installed_versions()?;
    let mut results = Vec::new();

    for runtime in installed {
        let latest = latest_newer_version(app, &runtime)?;
        results.push(CheckEntry {
            runtime,
            has_update: latest.is_some(),
            latest_available: latest,
        });
    }

    let human = if results.is_empty() {
        "No installed runtimes found".to_string()
    } else {
        format!("Checked {} installed runtime(s)", results.len())
    };

    print_output(output, color, &human, &results)?;
    Ok(0)
}

pub fn update(
    runtimes: Vec<String>,
    output: OutputFormat,
    color: Option<OutputColorMode>,
    app: &NodeupApp,
) -> Result<i32> {
    let (selectors, selector_context) = if runtimes.is_empty() {
        selectors_for_update(app)?
    } else {
        (
            runtimes,
            UpdateSelectorContext {
                source: "explicit-args",
                tracked_selectors: 0,
                installed_runtimes: 0,
            },
        )
    };

    if selectors.is_empty() {
        return Err(NodeupError::not_found_with_hint(
            format!(
                "No runtimes are eligible for update (selector_source={}, tracked_selectors={}, \
                 installed_runtimes={}, resolved_selectors={})",
                selector_context.source,
                selector_context.tracked_selectors,
                selector_context.installed_runtimes,
                selectors.len()
            ),
            "Install a runtime with `nodeup toolchain install <runtime>` or configure tracked \
             selectors first.",
        ));
    }

    let mut updates = Vec::new();
    for selector in selectors {
        let parsed = RuntimeSelector::parse(&selector)?;
        match parsed {
            RuntimeSelector::LinkedName(_) => {
                updates.push(UpdateEntry {
                    selector,
                    previous_runtime: None,
                    updated_runtime: None,
                    status: "skipped-linked-runtime".to_string(),
                });
            }
            RuntimeSelector::Channel(_) => {
                let resolved = app
                    .resolver
                    .resolve_selector_with_source(&selector, RuntimeSelectorSource::Explicit)?;
                let version = match resolved.target {
                    ResolvedRuntimeTarget::Version { version } => version,
                    ResolvedRuntimeTarget::LinkedPath { .. } => unreachable!(),
                };
                let report = app.installer.ensure_installed(&version, &app.releases)?;
                updates.push(UpdateEntry {
                    selector,
                    previous_runtime: None,
                    updated_runtime: Some(report.version),
                    status: if report.state == InstallState::AlreadyInstalled {
                        "already-up-to-date".to_string()
                    } else {
                        "updated".to_string()
                    },
                });
                if let Some(entry) = updates.last() {
                    info!(
                        command_path = "nodeup.update.channel",
                        selector = %entry.selector,
                        updated_runtime = ?entry.updated_runtime,
                        status = %entry.status,
                        "Processed channel update selector"
                    );
                }
            }
            RuntimeSelector::Version(version) => {
                let current = format!("v{version}");
                let next = latest_newer_version(app, &current)?;
                if let Some(next_version) = next {
                    let report = app
                        .installer
                        .ensure_installed(&next_version, &app.releases)?;
                    let status = if report.state == InstallState::AlreadyInstalled {
                        "already-up-to-date"
                    } else {
                        "updated"
                    };
                    updates.push(UpdateEntry {
                        selector,
                        previous_runtime: Some(current),
                        updated_runtime: Some(report.version),
                        status: status.to_string(),
                    });
                    if let Some(entry) = updates.last() {
                        info!(
                            command_path = "nodeup.update.version",
                            selector = %entry.selector,
                            previous_runtime = ?entry.previous_runtime,
                            updated_runtime = ?entry.updated_runtime,
                            status = %entry.status,
                            "Processed explicit version update selector"
                        );
                    }
                } else {
                    updates.push(UpdateEntry {
                        selector,
                        previous_runtime: Some(current.clone()),
                        updated_runtime: Some(current),
                        status: "already-up-to-date".to_string(),
                    });
                    if let Some(entry) = updates.last() {
                        info!(
                            command_path = "nodeup.update.version",
                            selector = %entry.selector,
                            previous_runtime = ?entry.previous_runtime,
                            updated_runtime = ?entry.updated_runtime,
                            status = %entry.status,
                            "Processed explicit version update selector"
                        );
                    }
                }
            }
        }
    }

    let human = format!("Processed updates for {} selector(s)", updates.len());
    print_output(output, color, &human, &updates)?;
    Ok(0)
}

fn selectors_for_update(app: &NodeupApp) -> Result<(Vec<String>, UpdateSelectorContext)> {
    let settings = app.store.load_settings()?;
    if !settings.tracked_selectors.is_empty() {
        let tracked_count = settings.tracked_selectors.len();
        return Ok((
            settings.tracked_selectors,
            UpdateSelectorContext {
                source: "tracked-selectors",
                tracked_selectors: tracked_count,
                installed_runtimes: 0,
            },
        ));
    }

    let installed = app.store.list_installed_versions()?;
    let installed_count = installed.len();
    Ok((
        installed,
        UpdateSelectorContext {
            source: "installed-runtimes",
            tracked_selectors: 0,
            installed_runtimes: installed_count,
        },
    ))
}

fn latest_newer_version(app: &NodeupApp, current: &str) -> Result<Option<String>> {
    let current_semver = Version::parse(normalize_version(current).trim_start_matches('v'))?;
    let mut best: Option<Version> = None;

    for entry in app.releases.fetch_index()? {
        let candidate = match Version::parse(entry.version.trim_start_matches('v')) {
            Ok(version) => version,
            Err(_) => continue,
        };
        if candidate <= current_semver {
            continue;
        }
        if best
            .as_ref()
            .is_none_or(|best_version| candidate > *best_version)
        {
            best = Some(candidate);
        }
    }

    Ok(best.map(|version| format!("v{version}")))
}
