use ratatui::text::Line;
use rebecca::core::cleanup_advice::CleanupAdviceStatus;
use rebecca::core::disk_session::{DiskMapDistributionRow, DiskMapVisibleRow};
use rebecca::core::plan::CleanupPlan;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

use crate::output::format_bytes;
use crate::text::format_count;
use crate::tui::app::TuiApp;
use crate::tui::model::TuiScreen;
use crate::tui::navigation::RootChoice;
use crate::tui::progress::TuiTaskStatus;
use crate::tui::view::ViewOptions;

const BAR_WIDTH: usize = 12;

pub(crate) fn snapshot(app: &TuiApp, options: ViewOptions) -> String {
    let mut lines = Vec::new();
    let width = options.width;
    lines.push(trim_to_width(
        format!(
            "Rebecca TUI | {} | basket {} | sort {}{}",
            screen_label(app.screen),
            app.basket.len(),
            app.sort.label(),
            group_filter_suffix(app)
        ),
        width,
    ));
    match app.screen {
        TuiScreen::RootPicker => snapshot_root_picker(app, width, &mut lines),
        TuiScreen::Map => snapshot_map(app, options, &mut lines),
        TuiScreen::Treemap => snapshot_treemap(app, &mut lines, width),
        TuiScreen::Types | TuiScreen::Extensions => snapshot_distribution(app, options, &mut lines),
        TuiScreen::Busy => {
            lines.push(trim_to_width(format!("Busy: {}", app.message), width));
            for line in task_status_plain_lines(app.task_status.as_ref()) {
                lines.push(trim_to_width(line, width));
            }
        }
        TuiScreen::Preview => {
            snapshot_plan("Cleanup preview", app.preview.as_ref(), width, &mut lines)
        }
        TuiScreen::Confirm => {
            lines.push(trim_to_width(
                format!("Type {} to execute", app.confirmation_phrase()),
                width,
            ));
            lines.push(trim_to_width(format!("Input: {}", app.message), width));
        }
        TuiScreen::Executed => {
            snapshot_plan("Cleanup result", app.executed.as_ref(), width, &mut lines)
        }
        TuiScreen::History => snapshot_history(app, width, &mut lines),
        TuiScreen::Help => snapshot_help(width, &mut lines),
        TuiScreen::Error => lines.push(trim_to_width(format!("Error: {}", app.message), width)),
    }
    lines.push(trim_to_width(format!("Status: {}", app.message), width));
    lines.join("\n")
}

fn snapshot_root_picker(app: &TuiApp, width: usize, lines: &mut Vec<String>) {
    lines.push(trim_to_width("Roots".to_string(), width));
    for (index, choice) in app.root_choices.iter().enumerate() {
        lines.push(snapshot_root_choice(index, choice, app.selected, width));
    }
}

fn snapshot_root_choice(
    index: usize,
    choice: &RootChoice,
    selected: usize,
    width: usize,
) -> String {
    trim_to_width(
        format!(
            "{} {} {}",
            if index == selected { ">" } else { " " },
            choice.label,
            choice.path.display()
        ),
        width,
    )
}

fn snapshot_map(app: &TuiApp, options: ViewOptions, lines: &mut Vec<String>) {
    let width = options.width;
    let rows = app.visible_rows();
    lines.push(trim_to_width(map_title(app, "Map"), width));
    for (index, row) in rows.iter().enumerate().take(20) {
        let staged = row
            .cleanup_advice
            .as_ref()
            .and_then(|advice| advice.rule_id.as_ref())
            .is_some_and(|rule_id| app.basket.contains_key(rule_id));
        lines.push(trim_to_width(
            format!(
                "{}{} {:>10}{} {} {}",
                if index == app.selected { ">" } else { " " },
                if staged { "*" } else { " " },
                format_bytes(row.metrics.logical_bytes),
                if options.visual_bars {
                    format!(
                        " {}",
                        byte_bar(row.metrics.logical_bytes, max_logical(&rows), BAR_WIDTH)
                    )
                } else {
                    String::new()
                },
                row.name,
                advice_label(row)
            ),
            width,
        ));
    }
}

fn snapshot_treemap(app: &TuiApp, lines: &mut Vec<String>, width: usize) {
    let rows = app.visible_rows();
    let total = rows
        .iter()
        .map(|row| row.metrics.logical_bytes)
        .sum::<u64>()
        .max(1);
    lines.push(trim_to_width(map_title(app, "Treemap"), width));
    for line in treemap_context_lines(app) {
        lines.push(trim_to_width(line, width));
    }
    if rows.is_empty() {
        lines.push(trim_to_width(treemap_empty_message(app), width));
        return;
    }
    for (index, row) in rows.iter().enumerate().take(20) {
        lines.push(trim_to_width(
            format!(
                "{} {:>10} {:>6.1}% {} {}",
                if index == app.selected { ">" } else { " " },
                format_bytes(row.metrics.logical_bytes),
                (row.metrics.logical_bytes as f64 / total as f64) * 100.0,
                row.name,
                advice_label(row)
            ),
            width,
        ));
    }
}

fn snapshot_distribution(app: &TuiApp, options: ViewOptions, lines: &mut Vec<String>) {
    let width = options.width;
    let rows = app.distribution_rows();
    let max = max_distribution_logical(&rows);
    let selected = app.selected_distribution_index();
    lines.push(trim_to_width(
        distribution_title(app.screen).to_string(),
        width,
    ));
    if rows.is_empty() {
        lines.push(trim_to_width(
            distribution_empty_label(app.screen).to_string(),
            width,
        ));
        return;
    }
    for (index, row) in rows.iter().enumerate().take(20) {
        lines.push(trim_to_width(
            format!(
                "{} {:>10} {:>7} {:>14}{} {}",
                if index == selected { ">" } else { " " },
                format_bytes(row.metrics.logical_bytes),
                distribution_share_label(row),
                distribution_count_label(row),
                if options.visual_bars {
                    format!(" {}", byte_bar(row.metrics.logical_bytes, max, BAR_WIDTH))
                } else {
                    String::new()
                },
                row.label,
            ),
            width,
        ));
    }
}

fn snapshot_plan(
    title: &'static str,
    plan: Option<&CleanupPlan>,
    width: usize,
    lines: &mut Vec<String>,
) {
    lines.push(trim_to_width(title.to_string(), width));
    for line in plan_lines(plan) {
        lines.push(trim_to_width(line_to_plain(&line), width));
    }
}

fn snapshot_help(width: usize, lines: &mut Vec<String>) {
    for line in help_lines() {
        lines.push(trim_to_width(line_to_plain(&line), width));
    }
}

fn snapshot_history(app: &TuiApp, width: usize, lines: &mut Vec<String>) {
    for line in history_lines(app) {
        lines.push(trim_to_width(line_to_plain(&line), width));
    }
}

fn plan_lines(plan: Option<&CleanupPlan>) -> Vec<Line<'static>> {
    let Some(plan) = plan else {
        return vec![Line::from("No plan available.")];
    };
    vec![
        Line::from(format!(
            "Targets: {} total, {} allowed, {} blocked, {} skipped, {} failed",
            plan.summary.total_targets,
            plan.summary.allowed_targets,
            plan.summary.blocked_targets,
            plan.summary.skipped_targets,
            plan.summary.failed_targets
        )),
        Line::from(format!(
            "Estimated: {} ({})",
            plan.summary.estimated_bytes,
            format_bytes(plan.summary.estimated_bytes)
        )),
        Line::from(format!(
            "Freed: {} ({})",
            plan.summary.freed_bytes,
            format_bytes(plan.summary.freed_bytes)
        )),
        Line::from("e execute  Esc return  q quit"),
    ]
}

fn help_lines() -> Vec<Line<'static>> {
    vec![
        Line::from("j/k or arrows move"),
        Line::from("Enter/l opens a directory or treemap tile, h/Esc moves back"),
        Line::from("1 map, 4/w treemap, 2/t types, 3/x extensions, Tab cycles views"),
        Line::from("Enter filters by selected type/extension; Backspace clears group filter"),
        Line::from("/ filters the active view, s cycles sort"),
        Line::from("r patches the active directory, R refreshes the root"),
        Line::from(
            "Mouse: click tabs, map rows, treemap tiles, or distribution rows; click selects only",
        ),
        Line::from("Space stages the cleanup rule; preview includes all matching targets"),
        Line::from("e executes only after typed confirmation"),
        Line::from("g shows recent cleanup history"),
        Line::from("q quits"),
    ]
}

fn history_lines(app: &TuiApp) -> Vec<Line<'static>> {
    if app.history.is_empty() {
        return vec![Line::from("No cleanup history entries yet.")];
    }

    let mut lines = Vec::with_capacity(app.history.len() + 2);
    lines.push(Line::from("Recent cleanup history"));
    for (index, entry) in app.history.iter().rev().enumerate() {
        let bytes = entry
            .summary
            .freed_bytes
            .saturating_add(entry.summary.pending_reclaim_bytes);
        lines.push(Line::from(format!(
            "#{:>2} {} | targets {} done, {} blocked, {} skipped | {}",
            index + 1,
            entry.recorded_at_unix_seconds,
            entry.summary.completed_targets,
            entry.summary.blocked_targets,
            entry.summary.skipped_targets,
            format_bytes(bytes),
        )));
    }
    lines.push(Line::from("Esc returns to map, q quits."));
    lines
}

fn task_status_plain_lines(status: Option<&TuiTaskStatus>) -> Vec<String> {
    let Some(status) = status else {
        return Vec::new();
    };

    let mut lines = Vec::new();
    lines.push(format!("Task: {} | {}", status.label, status.phase));
    if status.cancel_requested {
        lines.push(status.cancel_wait_message().to_string());
    }
    if let Some(backend) = &status.backend {
        lines.push(format!("Backend: {backend}"));
    }
    if status.root_count > 0 {
        lines.push(format!(
            "Roots: {}/{}",
            status.roots_finished, status.root_count
        ));
    }
    if status.files > 0 || status.directories > 0 || status.logical_bytes > 0 {
        lines.push(format!(
            "Scanned: {} files, {} directories, {}",
            status.files,
            status.directories,
            format_bytes(status.logical_bytes)
        ));
    }
    if status.targets_started > 0 || status.targets_finished > 0 {
        lines.push(format!(
            "Targets: {} started, {} finished, {} estimated",
            status.targets_started,
            status.targets_finished,
            format_bytes(status.estimated_bytes)
        ));
    }
    if status.cache_hits > 0 || status.cache_misses > 0 || status.cache_write_skipped > 0 {
        lines.push(format!(
            "Scan cache: {} hits, {} misses, {} skipped writes, {} pruned",
            status.cache_hits, status.cache_misses, status.cache_write_skipped, status.cache_pruned
        ));
    }
    if let Some(rule_id) = &status.current_rule_id {
        lines.push(format!("Rule: {rule_id}"));
    }
    if let Some(path) = &status.current_path {
        lines.push(format!("Current: {}", path.display()));
    }
    if !status.last_event.is_empty() {
        lines.push(format!("Last: {}", status.last_event));
    }
    lines
}

fn group_filter_suffix(app: &TuiApp) -> String {
    app.active_group_filter_label()
        .map(|label| format!(" | filter {label}"))
        .unwrap_or_default()
}

fn map_title(app: &TuiApp, prefix: &str) -> String {
    match app.active_group_filter_label() {
        Some(label) => format!("{prefix}: {} [{label}]", app.current_node_name()),
        None => format!("{prefix}: {}", app.current_node_name()),
    }
}

fn treemap_context_lines(app: &TuiApp) -> Vec<String> {
    let mut lines = Vec::new();
    lines.push(format!("Scope: {}", app.current_node_name()));
    lines.push(format!("Breadcrumb: {}", app.current_scope_breadcrumb()));
    lines.push(format!("Zoom depth: {}", app.zoom_depth()));
    lines.push(format!(
        "Filter: {}",
        app.active_scope_filter_summary()
            .unwrap_or_else(|| "none".to_string())
    ));
    if let Some(summary) = app.treemap_selection_summary() {
        lines.push(format!("Selected tile: {}", summary.name));
        lines.push(format!("Kind: {}", summary.kind));
        lines.push(format!(
            "Drillable: {}",
            if summary.drillable { "yes" } else { "no" }
        ));
        if let Some(reason) = summary.non_drillable_reason {
            lines.push(format!("Reason: {reason}"));
        }
        lines.push(format!("Action: {}", summary.primary_action));
    } else {
        lines.push("Selected tile: none".to_string());
        lines.push("Drillable: no".to_string());
        lines.push("Action: select a directory tile".to_string());
    }
    lines
}

fn treemap_empty_message(app: &TuiApp) -> String {
    if app.active_scope_filter_summary().is_some() {
        "No entries match the active filters in this scope.".to_string()
    } else {
        "No entries for this scope.".to_string()
    }
}

fn screen_label(screen: TuiScreen) -> &'static str {
    match screen {
        TuiScreen::RootPicker => "root-picker",
        TuiScreen::Map => "map",
        TuiScreen::Treemap => "treemap",
        TuiScreen::Types => "types",
        TuiScreen::Extensions => "extensions",
        TuiScreen::Busy => "working",
        TuiScreen::Preview => "preview",
        TuiScreen::Confirm => "confirm",
        TuiScreen::Executed => "executed",
        TuiScreen::History => "history",
        TuiScreen::Help => "help",
        TuiScreen::Error => "error",
    }
}

fn advice_label(row: &DiskMapVisibleRow) -> String {
    row.cleanup_advice
        .as_ref()
        .map(|advice| advice.status.label().to_string())
        .unwrap_or_else(|| CleanupAdviceStatus::Unknown.label().to_string())
}

fn max_logical(rows: &[DiskMapVisibleRow]) -> u64 {
    rows.iter()
        .map(|row| row.metrics.logical_bytes)
        .max()
        .unwrap_or(0)
}

fn max_distribution_logical(rows: &[DiskMapDistributionRow]) -> u64 {
    rows.iter()
        .map(|row| row.metrics.logical_bytes)
        .max()
        .unwrap_or(0)
}

fn distribution_title(screen: TuiScreen) -> &'static str {
    match screen {
        TuiScreen::Types => "Types: file kind distribution",
        TuiScreen::Extensions => "Extensions: suffix distribution",
        _ => "Distribution",
    }
}

fn distribution_empty_label(screen: TuiScreen) -> &'static str {
    match screen {
        TuiScreen::Types => "No type distribution for this scan.",
        TuiScreen::Extensions => "No extension distribution for this scan.",
        _ => "No distribution rows for this scan.",
    }
}

fn distribution_count_label(row: &DiskMapDistributionRow) -> String {
    match (row.metrics.files, row.metrics.directories) {
        (files, 0) => format_count(files, "file", "files"),
        (0, directories) => format_count(directories, "directory", "directories"),
        (files, directories) => format!(
            "{}, {}",
            format_count(files, "file", "files"),
            format_count(directories, "directory", "directories")
        ),
    }
}

fn distribution_share_label(row: &DiskMapDistributionRow) -> String {
    if row.scope_logical_bytes == 0 {
        return "0.0%".to_string();
    }
    format!(
        "{:.1}%",
        (row.metrics.logical_bytes as f64 / row.scope_logical_bytes as f64) * 100.0
    )
}

fn byte_bar(value: u64, max: u64, width: usize) -> String {
    if width == 0 {
        return String::new();
    }
    let filled = if max == 0 {
        0
    } else {
        ((value as f64 / max as f64) * width as f64).round() as usize
    }
    .min(width);
    format!("{}{}", "#".repeat(filled), ".".repeat(width - filled))
}

fn trim_to_width(value: String, width: usize) -> String {
    if width == 0 {
        return String::new();
    }
    if UnicodeWidthStr::width(value.as_str()) <= width {
        return value;
    }

    let suffix = if width > 3 { "..." } else { "" };
    let target_width = width.saturating_sub(UnicodeWidthStr::width(suffix));
    let mut rendered_width = 0;
    let mut trimmed = String::new();
    for ch in value.chars() {
        let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
        if rendered_width + char_width > target_width {
            break;
        }
        trimmed.push(ch);
        rendered_width += char_width;
    }
    trimmed.push_str(suffix);
    trimmed
}

fn line_to_plain(line: &Line<'_>) -> String {
    line.spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>()
}
