use std::fmt::Write as _;

use anyhow::{Result, anyhow};
use rebecca::core::cache::{
    CacheDoctorReport, CacheInventory, CacheInventoryEntryStatus, CacheNamespace, CachePruneReport,
    CachePurgeMode, CachePurgeReport, doctor_app_cache, inspect_app_cache,
    prune_app_cache_inventory, purge_app_cache, purge_app_cache_with_backend,
};
use rebecca::core::config::{load_app_paths, load_runtime_config};

use crate::cache_view::CachePurgeProjection;
use crate::cli::OutputMode;
use crate::output::{format_bytes, format_shell_command};
use crate::trash_backend::recoverable_trash_backend;

#[derive(Debug)]
pub struct CachePurgeOptions {
    pub dry_run: bool,
    pub output_mode: OutputMode,
    pub yes: bool,
    pub permanent: bool,
}

#[derive(Debug)]
pub struct CacheInspectOptions {
    pub output_mode: OutputMode,
    pub namespace: CacheNamespace,
}

#[derive(Debug)]
pub struct CacheDoctorOptions {
    pub output_mode: OutputMode,
}

#[derive(Debug)]
pub struct CachePruneOptions {
    pub output_mode: OutputMode,
    pub namespace: CacheNamespace,
    pub stale_only: bool,
    pub limit: Option<std::num::NonZeroUsize>,
    pub dry_run: bool,
    pub yes: bool,
}

pub fn inspect(options: CacheInspectOptions) -> Result<()> {
    let runtime_config = load_runtime_config()?;
    let inventory = inspect_app_cache(
        &runtime_config.app_paths,
        options.namespace,
        runtime_config.scan_cache_policy,
    );
    crate::output::print_command_success(
        "cache inspect",
        "cache-inventory",
        options.output_mode,
        || &inventory,
        || {
            print!("{}", render_cache_inventory(&inventory)?);
            Ok(())
        },
    )
}

pub fn doctor(options: CacheDoctorOptions) -> Result<()> {
    let runtime_config = load_runtime_config()?;
    let report = doctor_app_cache(&runtime_config.app_paths, runtime_config.scan_cache_policy);
    crate::output::print_command_success(
        "cache doctor",
        "cache-doctor",
        options.output_mode,
        || &report,
        || {
            print!("{}", render_cache_doctor_report(&report)?);
            Ok(())
        },
    )
}

pub fn prune(options: CachePruneOptions) -> Result<()> {
    if options.dry_run && options.yes {
        return Err(anyhow!("--dry-run cannot be combined with --yes"));
    }

    let runtime_config = load_runtime_config()?;
    let dry_run = !options.yes || options.dry_run;
    let report = prune_app_cache_inventory(
        &runtime_config.app_paths,
        options.namespace,
        runtime_config.scan_cache_policy,
        options.stale_only,
        options.limit.map(std::num::NonZeroUsize::get),
        dry_run,
    );

    crate::output::print_command_success(
        "cache prune",
        "cache-prune-report",
        options.output_mode,
        || &report,
        || {
            print!("{}", render_cache_prune_report(&report)?);
            Ok(())
        },
    )
}

pub fn purge(options: CachePurgeOptions) -> Result<()> {
    if options.dry_run && options.yes {
        return Err(anyhow!("--dry-run cannot be combined with --yes"));
    }
    if options.permanent && (options.dry_run || !options.yes) {
        return Err(anyhow!(
            "--permanent requires --yes and cannot be combined with --dry-run"
        ));
    }

    let paths = load_app_paths()?;
    let mode = if options.yes && !options.dry_run {
        if options.permanent {
            CachePurgeMode::PermanentDelete
        } else {
            CachePurgeMode::RecoverableDelete
        }
    } else {
        CachePurgeMode::DryRun
    };
    let report = match mode {
        CachePurgeMode::DryRun | CachePurgeMode::PermanentDelete => purge_app_cache(&paths, mode)?,
        CachePurgeMode::RecoverableDelete => purge_app_cache_recoverably(&paths)?,
    };

    crate::output::print_command_success(
        "cache purge",
        "cache-purge-report",
        options.output_mode,
        || &report,
        || {
            print!("{}", render_cache_purge_report(&report)?);
            Ok(())
        },
    )
}

fn purge_app_cache_recoverably(
    paths: &rebecca::core::config::AppPaths,
) -> Result<CachePurgeReport> {
    let backend = recoverable_trash_backend();
    Ok(purge_app_cache_with_backend(
        paths,
        CachePurgeMode::RecoverableDelete,
        &backend,
    )?)
}

fn render_cache_purge_report(report: &CachePurgeReport) -> Result<String> {
    let projection = CachePurgeProjection::new(report);
    let mut output = String::new();
    writeln!(output, "Rebecca cache: {}", projection.cache_dir.display())?;
    writeln!(output, "Mode: {}", projection.mode_label)?;
    writeln!(
        output,
        "Lifecycle: {} ({})",
        projection.lifecycle_label, projection.retention_label
    )?;
    writeln!(
        output,
        "Cache directory exists: {}",
        projection.cache_dir_exists_label
    )?;
    writeln!(
        output,
        "Preserves cache directory: {}",
        projection.preserves_cache_dir_label
    )?;
    writeln!(
        output,
        "Entries: {}, files: {}, directories: {}",
        projection.summary.total_entries, projection.summary.files, projection.summary.directories
    )?;
    writeln!(
        output,
        "Entry status: {} would delete, {} recoverably deleted, {} permanently deleted, {} skipped, {} failed",
        projection.summary.would_delete_entries,
        projection.summary.recoverably_deleted_entries,
        projection.summary.permanently_deleted_entries,
        projection.summary.skipped_entries,
        projection.summary.failed_entries
    )?;
    writeln!(
        output,
        "Estimated bytes: {} ({})",
        projection.summary.estimated_bytes,
        format_bytes(projection.summary.estimated_bytes)
    )?;
    writeln!(
        output,
        "Reclaimed bytes: {} ({})",
        projection.summary.reclaimed_bytes,
        format_bytes(projection.summary.reclaimed_bytes)
    )?;
    writeln!(
        output,
        "Pending reclaim bytes: {} ({})",
        projection.summary.pending_reclaim_bytes,
        format_bytes(projection.summary.pending_reclaim_bytes)
    )?;

    if !projection.issue_matrix().is_empty() {
        writeln!(output, "Issue matrix:")?;
        for issue in projection.issue_matrix() {
            writeln!(
                output,
                "- {} {}: {}, {} ({})",
                issue.status_label,
                issue.reason_label,
                issue.entries_label,
                issue.estimated_bytes,
                format_bytes(issue.estimated_bytes)
            )?;
        }
    }

    if projection.is_empty() {
        writeln!(output, "No cache entries found.")?;
        return Ok(output);
    }

    if projection.show_delete_hint() {
        writeln!(
            output,
            "Run with --yes to move these rebuildable cache entries to recoverable trash, or --yes --permanent to delete them permanently."
        )?;
    }

    writeln!(output, "Cache entries:")?;
    for entry in projection.entries() {
        writeln!(
            output,
            "- {}: {} ({}; {} file(s), {} dir(s)){}",
            entry.status_label,
            entry.path.display(),
            format_bytes(entry.estimated_bytes),
            entry.files,
            entry.directories,
            entry.reason_suffix
        )?;
    }

    Ok(output)
}

fn render_cache_inventory(inventory: &CacheInventory) -> Result<String> {
    let mut output = String::new();
    writeln!(output, "Rebecca cache: {}", inventory.cache_dir.display())?;
    writeln!(output, "Namespace: {}", inventory.namespace.label())?;
    writeln!(
        output,
        "Entries: {}, valid: {}, stale: {}, corrupt: {}, missing payloads: {}, prunable: {}",
        inventory.summary.total_entries,
        inventory.summary.valid_entries,
        inventory.summary.stale_entries,
        inventory.summary.corrupt_entries,
        inventory.summary.missing_payloads,
        inventory.summary.prunable_entries
    )?;
    writeln!(
        output,
        "Cache bytes: {} ({})",
        inventory.summary.bytes,
        format_bytes(inventory.summary.bytes)
    )?;

    if inventory.entries.is_empty() {
        writeln!(output, "No cache records found.")?;
    } else {
        writeln!(output, "Cache records:")?;
        for entry in &inventory.entries {
            let reason = entry
                .reason_code
                .as_deref()
                .map(|reason| format!("; {reason}"))
                .unwrap_or_default();
            writeln!(
                output,
                "- {} {}: {} ({}; {}){}",
                entry.namespace.label(),
                cache_status_label(entry.status),
                entry.display_path.display(),
                entry.bytes,
                format_bytes(entry.bytes),
                reason
            )?;
        }
    }

    if !inventory.diagnostics.is_empty() {
        writeln!(output, "Diagnostics:")?;
        for diagnostic in &inventory.diagnostics {
            writeln!(
                output,
                "- {}: {} ({})",
                diagnostic.reason_code,
                diagnostic.display_path.display(),
                diagnostic.message
            )?;
        }
    }

    Ok(output)
}

fn render_cache_doctor_report(report: &CacheDoctorReport) -> Result<String> {
    let mut output = String::new();
    writeln!(output, "Cache health: {}", cache_health_label(report))?;
    writeln!(
        output,
        "Prunable records: {}",
        report.inventory.summary.prunable_entries
    )?;
    if let Some(parts) = report
        .recommendations
        .iter()
        .filter_map(|recommendation| recommendation.suggested_command.as_ref())
        .next()
    {
        writeln!(
            output,
            "Recommended next command: {}",
            format_shell_command("rebecca", parts)
        )?;
    }
    writeln!(output)?;
    output.push_str(&render_cache_inventory(&report.inventory)?);
    if report.recommendations.is_empty() {
        writeln!(output, "Recommendations: none")?;
    } else {
        writeln!(output, "Recommendations:")?;
        for recommendation in &report.recommendations {
            let command = recommendation
                .suggested_command
                .as_ref()
                .map(|parts| format!(" [{}]", parts.join(" ")))
                .unwrap_or_default();
            writeln!(
                output,
                "- {:?}: {}{}",
                recommendation.severity, recommendation.message, command
            )?;
        }
    }
    Ok(output)
}

fn cache_health_label(report: &CacheDoctorReport) -> &'static str {
    if report.inventory.summary.prunable_entries > 0 {
        "needs pruning"
    } else if report.recommendations.is_empty() {
        "healthy"
    } else {
        "review recommended"
    }
}

fn render_cache_prune_report(report: &CachePruneReport) -> Result<String> {
    let mut output = String::new();
    writeln!(output, "Rebecca cache: {}", report.cache_dir.display())?;
    writeln!(output, "Namespace: {}", report.namespace.label())?;
    writeln!(
        output,
        "Mode: {}",
        if report.dry_run { "dry-run" } else { "delete" }
    )?;
    writeln!(
        output,
        "Stale only: {}",
        if report.stale_only { "yes" } else { "no" }
    )?;
    writeln!(
        output,
        "Selected records: {}",
        report.selected_entries.len()
    )?;
    writeln!(
        output,
        "Execution: {} completed, {} skipped, {} failed, {} bytes reclaimed",
        report.execution_report.summary.completed_actions,
        report.execution_report.summary.skipped_actions,
        report.execution_report.summary.failed_actions,
        report.execution_report.summary.confirmed_reclaimed_bytes
    )?;
    if report.selected_entries.is_empty() {
        writeln!(output, "No cache records selected.")?;
    } else {
        writeln!(output, "Selected cache records:")?;
        for entry in &report.selected_entries {
            writeln!(
                output,
                "- {} {}: {} ({})",
                entry.namespace.label(),
                cache_status_label(entry.status),
                entry.display_path.display(),
                format_bytes(entry.bytes)
            )?;
        }
    }
    if report.dry_run && !report.selected_entries.is_empty() {
        writeln!(
            output,
            "Run with --yes to delete the selected cache metadata records."
        )?;
    }
    Ok(output)
}

fn cache_status_label(status: CacheInventoryEntryStatus) -> &'static str {
    match status {
        CacheInventoryEntryStatus::Valid => "valid",
        CacheInventoryEntryStatus::Stale => "stale",
        CacheInventoryEntryStatus::Corrupt => "corrupt",
        CacheInventoryEntryStatus::Unreadable => "unreadable",
        CacheInventoryEntryStatus::MissingPayload => "missing-payload",
        CacheInventoryEntryStatus::Payload => "payload",
        CacheInventoryEntryStatus::Unknown => "unknown",
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use rebecca::core::cache::{
        CachePurgeEntry, CachePurgeEntryKind, CachePurgeEntryReason, CachePurgeEntryStatus,
        CachePurgeIssueSummary, CachePurgeSummary,
    };
    use rebecca::core::config::{AppStorageLifecycle, AppStorageRetention};

    use super::{CachePurgeMode, CachePurgeReport, render_cache_purge_report};

    #[test]
    fn render_cache_purge_report_includes_issue_matrix_when_present() {
        let report = CachePurgeReport {
            cache_dir: PathBuf::from("cache"),
            cache_dir_lifecycle: AppStorageLifecycle::RebuildableCache,
            cache_dir_retention: AppStorageRetention::Rebuildable,
            cache_dir_exists: true,
            preserves_cache_dir: true,
            mode: CachePurgeMode::DryRun,
            deleted: false,
            summary: CachePurgeSummary {
                total_entries: 1,
                would_delete_entries: 0,
                deleted_entries: 0,
                skipped_entries: 1,
                failed_entries: 0,
                files: 0,
                directories: 0,
                estimated_bytes: 0,
                reclaimed_bytes: 0,
                pending_reclaim_bytes: 0,
                recoverably_deleted_entries: 0,
                permanently_deleted_entries: 0,
                issue_matrix: vec![CachePurgeIssueSummary {
                    status: CachePurgeEntryStatus::Skipped,
                    reason_code: CachePurgeEntryReason::SymlinkSkipped,
                    entries: 1,
                    estimated_bytes: 0,
                }],
            },
            entries: vec![CachePurgeEntry {
                path: PathBuf::from("cache/link"),
                kind: CachePurgeEntryKind::Symlink,
                status: CachePurgeEntryStatus::Skipped,
                estimated_bytes: 0,
                reclaimed_bytes: 0,
                pending_reclaim_bytes: 0,
                files: 0,
                directories: 0,
                reason: Some("symlink entries are skipped".to_string()),
                reason_code: Some(CachePurgeEntryReason::SymlinkSkipped),
            }],
            execution_report: Default::default(),
        };

        let rendered = render_cache_purge_report(&report).unwrap();

        assert!(rendered.contains("Issue matrix:"));
        assert!(rendered.contains("- skipped symlink-skipped: 1 entry, 0 (0 B)"));
        assert!(rendered.contains(
            "Run with --yes to move these rebuildable cache entries to recoverable trash"
        ));
    }

    #[test]
    fn render_cache_purge_report_omits_empty_issue_matrix() {
        let report = CachePurgeReport {
            cache_dir: PathBuf::from("cache"),
            cache_dir_lifecycle: AppStorageLifecycle::RebuildableCache,
            cache_dir_retention: AppStorageRetention::Rebuildable,
            cache_dir_exists: false,
            preserves_cache_dir: true,
            mode: CachePurgeMode::DryRun,
            deleted: false,
            summary: CachePurgeSummary::default(),
            entries: Vec::new(),
            execution_report: Default::default(),
        };

        let rendered = render_cache_purge_report(&report).unwrap();

        assert!(!rendered.contains("Issue matrix:"));
        assert!(rendered.contains("No cache entries found."));
    }
}
