use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use anyhow::{Context, Result, anyhow};
use indicatif::ProgressBar;
use rebecca::core::config::{AppRuntimeConfig, load_runtime_config};
use rebecca::core::environment::SystemEnvironment;
use rebecca::core::executor::execute_cleanup_plan_parallel_with_policy_and_cancellation;
use rebecca::core::external_rules::ExternalRuleStore;
use rebecca::core::history::HistoryStore;
use rebecca::core::plan::CleanupPlan;
use rebecca::core::planner::{
    PlanBuildContext, PlanProgressEvent, build_cleanup_plan_with_context,
};
use rebecca::core::protection::ProtectionPolicy;
use rebecca::core::scan::ScanBackendKind;
use rebecca::core::scan_cache::ScanCacheStore;
use rebecca::core::{
    DeleteMode, PlanRequest, Platform, RebeccaError, RuleDefinition, TargetStatus,
};

use crate::clean_view::ScanCacheProgressSummary;
use crate::cli::{OutputMode, ProgressDetail};
use crate::output::{
    HumanPlanRenderer, MachineErrorRendered, NdjsonEventWriter, WorkflowOutputContract,
    WorkflowSuccessRenderer, format_bytes,
};
use crate::progress::{
    HumanProgressThrottle, PROGRESS_PATH_MAX_CHARS, compact_progress_path, format_byte_rate,
    format_file_rate, stderr_spinner,
};
use crate::runtime::CliRuntime;
use crate::text::format_count;
use crate::trash_backend::recoverable_trash_backend;
use crate::{info, output, render};

#[derive(Debug)]
pub struct CleanOptions {
    pub dry_run: bool,
    pub output_mode: OutputMode,
    pub yes: bool,
    pub no_progress: bool,
    pub progress_detail: ProgressDetail,
    pub scan_cache: bool,
    pub scan_backend: ScanBackendKind,
    pub categories: Vec<String>,
    pub rules: Vec<String>,
    pub exclude_paths: Vec<PathBuf>,
    pub allow_moderate: bool,
    pub allow_risky: bool,
    pub allow_warnings: Vec<String>,
}

pub(crate) struct WorkflowRunOptions<'a> {
    pub(crate) request: PlanRequest,
    pub(crate) rule_source: WorkflowRuleSource<'a>,
    pub(crate) output_mode: OutputMode,
    pub(crate) yes: bool,
    pub(crate) no_progress: bool,
    pub(crate) progress_detail: ProgressDetail,
    pub(crate) scan_cache: bool,
    pub(crate) scan_backend: ScanBackendKind,
    pub(crate) exclude_paths: Vec<PathBuf>,
    pub(crate) output_contract: WorkflowOutputContract,
    pub(crate) human_renderer: HumanPlanRenderer,
    pub(crate) success_renderer: WorkflowSuccessRenderer,
    pub(crate) cancellation_message: &'static str,
    pub(crate) confirmation_kind: ConfirmationKind,
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum WorkflowRuleSource<'a> {
    RuleCatalog(&'a [RuleDefinition]),
    NativeWorkflow,
}

impl<'a> WorkflowRuleSource<'a> {
    fn rules(self) -> &'a [RuleDefinition] {
        match self {
            Self::RuleCatalog(rules) => rules,
            Self::NativeWorkflow => &[],
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum ConfirmationKind {
    Cleanup,
    AppLeftovers,
    ProjectArtifacts,
}

pub(crate) fn run_with_runtime(options: CleanOptions, runtime: &CliRuntime) -> Result<()> {
    if options.dry_run && options.yes {
        return Err(anyhow!("--dry-run cannot be combined with --yes"));
    }

    let mode = if options.yes && !options.dry_run {
        DeleteMode::RecoverableDelete
    } else {
        DeleteMode::DryRun
    };

    let mut request = PlanRequest::for_platform(Platform::current(), mode);
    request.selected_categories = options.categories;
    request.selected_rule_ids = options.rules;
    request.allow_moderate = options.allow_moderate;
    request.allow_risky = options.allow_risky;
    for warning in &options.allow_warnings {
        request.add_allowed_warning(warning);
    }

    let catalog = rebecca::rules::builtin_rules()?;
    run_workflow(
        WorkflowRunOptions {
            request,
            rule_source: WorkflowRuleSource::RuleCatalog(&catalog),
            output_mode: options.output_mode,
            yes: options.yes,
            no_progress: options.no_progress,
            progress_detail: options.progress_detail,
            scan_cache: options.scan_cache,
            scan_backend: options.scan_backend,
            exclude_paths: options.exclude_paths,
            output_contract: WorkflowOutputContract::v1("clean", "cleanup-plan"),
            human_renderer: render::clean::print_plan,
            success_renderer: output::print_plan_with_events,
            cancellation_message: "Cleanup cancelled.",
            confirmation_kind: ConfirmationKind::Cleanup,
        },
        runtime,
    )
}

pub(crate) fn run_workflow(options: WorkflowRunOptions<'_>, runtime: &CliRuntime) -> Result<()> {
    let runtime_config = load_runtime_config()?;
    run_workflow_with_runtime_config(options, runtime_config, runtime)
}

pub(crate) fn run_workflow_with_runtime_config(
    options: WorkflowRunOptions<'_>,
    runtime_config: AppRuntimeConfig,
    runtime: &CliRuntime,
) -> Result<()> {
    let safety_knowledge =
        rebecca::rules::builtin_safety_knowledge_for_platform(options.request.platform)?;
    let cancellation = runtime.cancellation();
    let mut progress = PlanProgressReporter::new(
        options.output_mode.is_human() && !options.no_progress,
        options.progress_detail,
        options
            .output_mode
            .is_ndjson()
            .then(|| NdjsonEventWriter::with_contract(options.output_contract)),
    );
    let applications = info::application_discovery();
    let protected_storage = runtime_config.app_paths.storage_entries();
    let protected_paths = merged_protected_paths(
        runtime_config.protected_paths.as_slice(),
        options.exclude_paths.as_slice(),
    )?;
    let scan_cache_store = options
        .scan_cache
        .then(|| ScanCacheStore::from_app_paths(&runtime_config.app_paths));
    let mut context = PlanBuildContext::new(cancellation)
        .with_scan_backend(options.scan_backend)
        .with_safety_knowledge(&safety_knowledge)
        .with_protected_storage(&protected_storage);
    if !protected_paths.is_empty() {
        context = context.with_protected_paths(&protected_paths);
    }
    if options.scan_cache {
        context = context.with_scan_cache_policy(runtime_config.scan_cache_policy);
        if let Some(store) = &scan_cache_store {
            context = context.with_scan_cache(store);
        }
    }
    progress.started()?;
    let combined_rules;
    let rules = match options.rule_source {
        WorkflowRuleSource::RuleCatalog(rules) => {
            let external_rules =
                ExternalRuleStore::default_for_state_dir(&runtime_config.app_paths.state_dir)
                    .load_enabled_rules();
            for diagnostic in &external_rules.diagnostics {
                eprintln!("Warning: external rule skipped: {}", diagnostic.message);
            }
            if external_rules.rules.is_empty() {
                rules
            } else {
                combined_rules = rules
                    .iter()
                    .cloned()
                    .chain(external_rules.rules)
                    .collect::<Vec<_>>();
                &combined_rules
            }
        }
        WorkflowRuleSource::NativeWorkflow => options.rule_source.rules(),
    };
    let plan_result = build_cleanup_plan_with_context(
        &options.request,
        rules,
        &SystemEnvironment,
        applications.as_ref(),
        context,
        |event| progress.on_event(event),
    );
    progress.finish();
    if let Some(err) = progress.take_event_error() {
        return Err(err);
    }
    let mut plan = match plan_result {
        Ok(plan) => plan,
        Err(err) => {
            let event_writer = progress.into_event_writer();
            if matches!(&err, rebecca::core::RebeccaError::OperationCancelled(_)) {
                return finish_stream_with_cancellation(event_writer, options.cancellation_message);
            }

            return finish_stream_with_error(event_writer, err.into());
        }
    };

    let scan_cache_summary = options
        .output_mode
        .is_human()
        .then(|| progress.scan_cache_summary());
    let event_writer = progress.into_event_writer();

    if options.request.mode.is_dry_run() {
        return (options.success_renderer)(
            &plan,
            options.output_contract,
            options.output_mode,
            options.human_renderer,
            scan_cache_summary,
            event_writer,
        );
    }

    if plan.summary.allowed_targets == 0 {
        return (options.success_renderer)(
            &plan,
            options.output_contract,
            options.output_mode,
            options.human_renderer,
            scan_cache_summary,
            event_writer,
        );
    }

    let confirmed = if options.yes {
        true
    } else {
        match confirm_cleanup(&plan, options.confirmation_kind) {
            Ok(confirmed) => confirmed,
            Err(err) => return finish_stream_with_error(event_writer, err),
        }
    };
    if !confirmed {
        return finish_stream_with_cancellation(event_writer, options.cancellation_message);
    }

    let backend = recoverable_trash_backend();
    let mut execution_policy = ProtectionPolicy::new()
        .with_safety_knowledge(&safety_knowledge)
        .with_protected_storage(&protected_storage);
    if !protected_paths.is_empty() {
        execution_policy = execution_policy.with_protected_paths(&protected_paths);
    }
    let mut execution_report = match execute_cleanup_plan_parallel_with_policy_and_cancellation(
        &mut plan,
        &backend,
        execution_policy,
        cancellation,
    ) {
        Ok(report) => report,
        Err(RebeccaError::OperationCancelled(_)) => {
            return finish_stream_with_cancellation(event_writer, options.cancellation_message);
        }
        Err(err) => return finish_stream_with_error(event_writer, err.into()),
    };

    let history_append =
        HistoryStore::new(runtime_config.app_paths.history_file).append_plan_report(&plan);
    if let Some(warning) = history_append.warning {
        eprintln!("Warning: {}", warning.message);
        execution_report.push_warning(warning);
    }
    plan.execution_report = Some(execution_report);

    (options.success_renderer)(
        &plan,
        options.output_contract,
        options.output_mode,
        options.human_renderer,
        scan_cache_summary,
        event_writer,
    )
}

fn finish_stream_with_error(
    event_writer: Option<NdjsonEventWriter>,
    err: anyhow::Error,
) -> Result<()> {
    if let Some(mut writer) = event_writer {
        writer.emit_error(&err)?;
        return Err(MachineErrorRendered.into());
    }

    Err(err)
}

fn finish_stream_with_cancellation(
    event_writer: Option<NdjsonEventWriter>,
    message: &str,
) -> Result<()> {
    if let Some(mut writer) = event_writer {
        writer.emit_cancelled(message)?;
    } else {
        println!("{message}");
    }

    Ok(())
}

fn merged_protected_paths(config_paths: &[PathBuf], cli_paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
    let mut merged = Vec::with_capacity(config_paths.len() + cli_paths.len());
    for path in config_paths.iter().chain(cli_paths) {
        rebecca::core::config::validate_user_protected_path(path)
            .map_err(|message| anyhow!("invalid protected path {}: {message}", path.display()))?;
        if merged.iter().all(|existing| existing != path) {
            merged.push(path.clone());
        }
    }
    Ok(merged)
}

struct PlanProgressReporter {
    bar: Option<ProgressBar>,
    detail: ProgressDetail,
    event_writer: Option<NdjsonEventWriter>,
    event_error: Option<anyhow::Error>,
    scanned_targets: u64,
    planned_bytes: u64,
    current_target_started_at: Instant,
    human_file_progress: HumanProgressThrottle,
    scan_cache_summary: ScanCacheProgressSummary,
}

impl PlanProgressReporter {
    fn new(enabled: bool, detail: ProgressDetail, event_writer: Option<NdjsonEventWriter>) -> Self {
        let now = Instant::now();
        let bar = stderr_spinner(enabled, "plan | building cleanup plan");

        Self {
            bar,
            detail,
            event_writer,
            event_error: None,
            scanned_targets: 0,
            planned_bytes: 0,
            current_target_started_at: now,
            human_file_progress: HumanProgressThrottle::new(),
            scan_cache_summary: ScanCacheProgressSummary::default(),
        }
    }

    fn started(&mut self) -> Result<()> {
        if let Some(writer) = &mut self.event_writer {
            writer.emit_started()?;
        }
        Ok(())
    }

    fn on_event(&mut self, event: PlanProgressEvent<'_>) {
        self.record_event(event);

        if self.event_error.is_none()
            && let Some(writer) = &mut self.event_writer
            && self.detail.should_emit_machine_event(event)
            && let Err(err) = writer.emit_plan_progress(event)
        {
            self.event_error = Some(err);
        }

        let Some(bar) = &self.bar else {
            return;
        };

        match event {
            PlanProgressEvent::TargetScanning { rule_id, path } => {
                self.current_target_started_at = Instant::now();
                bar.set_message(target_scanning_message(
                    self.scanned_targets.saturating_add(1),
                    rule_id,
                    path,
                ));
            }
            PlanProgressEvent::TargetFinished {
                status,
                estimated_bytes,
                ..
            } => {
                self.scanned_targets = self.scanned_targets.saturating_add(1);
                self.planned_bytes = self.planned_bytes.saturating_add(estimated_bytes);
                bar.set_message(target_finished_message(
                    self.scanned_targets,
                    self.planned_bytes,
                    status,
                    estimated_bytes,
                ));
                bar.tick();
            }
            PlanProgressEvent::FileMeasured {
                rule_id,
                files_scanned,
                bytes_scanned,
                ..
            } => {
                if self.detail.includes_file_events() && self.human_file_progress.should_refresh() {
                    bar.set_message(file_progress_message(
                        rule_id,
                        files_scanned,
                        bytes_scanned,
                        self.current_target_started_at.elapsed(),
                    ));
                }
            }
            PlanProgressEvent::ScanCacheHit {
                rule_id,
                path,
                estimated_bytes,
            } => {
                bar.set_message(scan_cache_hit_message(
                    self.scan_cache_summary,
                    rule_id,
                    path,
                    estimated_bytes,
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCacheMiss {
                rule_id,
                path,
                reason,
                ..
            } => {
                bar.set_message(scan_cache_miss_message(
                    self.scan_cache_summary,
                    rule_id,
                    path,
                    reason.label(),
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCacheWriteSkipped { rule_id, path } => {
                bar.set_message(scan_cache_write_skipped_message(
                    self.scan_cache_summary,
                    rule_id,
                    path,
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCachePruned { report } => {
                bar.set_message(scan_cache_pruned_message(
                    self.scan_cache_summary,
                    report.pruned as u64,
                    report.inspected as u64,
                ));
                bar.tick();
            }
        }
    }

    fn record_event(&mut self, event: PlanProgressEvent<'_>) {
        match event {
            PlanProgressEvent::ScanCacheHit { .. } => {
                self.scan_cache_summary.hits = self.scan_cache_summary.hits.saturating_add(1);
            }
            PlanProgressEvent::ScanCacheMiss { pruned, .. } => {
                self.scan_cache_summary.misses = self.scan_cache_summary.misses.saturating_add(1);
                if pruned {
                    self.scan_cache_summary.pruned =
                        self.scan_cache_summary.pruned.saturating_add(1);
                }
            }
            PlanProgressEvent::ScanCacheWriteSkipped { .. } => {
                self.scan_cache_summary.write_skipped =
                    self.scan_cache_summary.write_skipped.saturating_add(1);
            }
            PlanProgressEvent::ScanCachePruned { report } => {
                self.scan_cache_summary.pruned = self
                    .scan_cache_summary
                    .pruned
                    .saturating_add(report.pruned as u64);
            }
            _ => {}
        }
    }

    fn scan_cache_summary(&self) -> ScanCacheProgressSummary {
        self.scan_cache_summary
    }

    fn finish(&self) {
        if let Some(bar) = &self.bar {
            bar.finish_and_clear();
        }
    }

    fn take_event_error(&mut self) -> Option<anyhow::Error> {
        self.event_error.take()
    }

    fn into_event_writer(self) -> Option<NdjsonEventWriter> {
        self.event_writer
    }
}

fn target_scanning_message(next_target: u64, rule_id: &str, path: &Path) -> String {
    format!(
        "plan | target {next_target} | scanning {rule_id} | {}",
        compact_progress_path(path, PROGRESS_PATH_MAX_CHARS)
    )
}

fn target_finished_message(
    scanned_targets: u64,
    planned_bytes: u64,
    status: TargetStatus,
    estimated_bytes: u64,
) -> String {
    format!(
        "plan | {} | {} found | last {}, {}",
        format_count(scanned_targets, "target", "targets"),
        format_bytes(planned_bytes),
        status.label(),
        format_bytes(estimated_bytes)
    )
}

fn file_progress_message(
    rule_id: &str,
    files_scanned: u64,
    bytes_scanned: u64,
    elapsed: Duration,
) -> String {
    format!(
        "scan | {rule_id} | {} | {} | {}, {}",
        format_count(files_scanned, "file", "files"),
        format_bytes(bytes_scanned),
        format_file_rate(files_scanned, elapsed),
        format_byte_rate(bytes_scanned, elapsed)
    )
}

fn scan_cache_hit_message(
    summary: ScanCacheProgressSummary,
    rule_id: &str,
    path: &Path,
    estimated_bytes: u64,
) -> String {
    format!(
        "cache | {} | hit {rule_id} | {} | {}",
        scan_cache_counts(summary),
        compact_progress_path(path, PROGRESS_PATH_MAX_CHARS),
        format_bytes(estimated_bytes)
    )
}

fn scan_cache_miss_message(
    summary: ScanCacheProgressSummary,
    rule_id: &str,
    path: &Path,
    reason_label: &str,
) -> String {
    format!(
        "cache | {} | miss {rule_id} | {} | {reason_label}",
        scan_cache_counts(summary),
        compact_progress_path(path, PROGRESS_PATH_MAX_CHARS)
    )
}

fn scan_cache_write_skipped_message(
    summary: ScanCacheProgressSummary,
    rule_id: &str,
    path: &Path,
) -> String {
    format!(
        "cache | {} | skip write {rule_id} | {}",
        scan_cache_counts(summary),
        compact_progress_path(path, PROGRESS_PATH_MAX_CHARS)
    )
}

fn scan_cache_pruned_message(
    summary: ScanCacheProgressSummary,
    pruned: u64,
    inspected: u64,
) -> String {
    format!(
        "cache | {} | pruned {} after {}",
        scan_cache_counts(summary),
        format_count(pruned, "record", "records"),
        format_count(inspected, "inspection", "inspections")
    )
}

fn scan_cache_counts(summary: ScanCacheProgressSummary) -> String {
    format!(
        "{}, {}",
        format_count(summary.hits, "hit", "hits"),
        format_count(summary.misses, "miss", "misses")
    )
}

impl ProgressDetail {
    fn should_emit_machine_event(self, event: PlanProgressEvent<'_>) -> bool {
        !matches!(event, PlanProgressEvent::FileMeasured { .. }) || self.includes_file_events()
    }
}

fn confirm_cleanup(plan: &CleanupPlan, kind: ConfirmationKind) -> Result<bool> {
    let prompt = match kind {
        ConfirmationKind::Cleanup => format!(
            "Move {} target(s), {} bytes, to recoverable trash?",
            plan.summary.allowed_targets, plan.summary.estimated_bytes
        ),
        ConfirmationKind::AppLeftovers => format!(
            "Move {} app leftover target(s), {} bytes, to recoverable trash?",
            plan.summary.allowed_targets, plan.summary.estimated_bytes
        ),
        ConfirmationKind::ProjectArtifacts => format!(
            "Move {} project artifact target(s), {} bytes, to recoverable trash?",
            plan.summary.allowed_targets, plan.summary.estimated_bytes
        ),
    };

    dialoguer::Confirm::new()
        .with_prompt(prompt)
        .default(false)
        .interact()
        .context("cleanup confirmation failed")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn progress_target_scanning_message_keeps_path_bounded() {
        let long_path = Path::new(
            r"C:\Users\Rebecca\AppData\Local\VeryLongVendorName\VeryLongProductName\Cache\Nested\Target",
        );

        let message = target_scanning_message(3, "windows.user-temp", long_path);

        assert!(message.starts_with("plan | target 3 | scanning windows.user-temp | "));
        let path = message
            .strip_prefix("plan | target 3 | scanning windows.user-temp | ")
            .expect("progress message should keep the path as the final segment");
        assert!(path.starts_with("..."));
        assert!(path.ends_with(r"\Cache\Nested\Target"));
        assert!(path.chars().count() <= PROGRESS_PATH_MAX_CHARS);
    }

    #[test]
    fn progress_target_finished_message_summarizes_targets_and_bytes() {
        let message = target_finished_message(2, 1536, TargetStatus::Allowed, 512);

        assert_eq!(
            message,
            "plan | 2 targets | 1.50 KiB found | last allowed, 512 B"
        );
    }

    #[test]
    fn progress_file_message_includes_scan_rates() {
        let message = file_progress_message("windows.user-temp", 4, 20, Duration::from_secs(1));

        assert_eq!(
            message,
            "scan | windows.user-temp | 4 files | 20 B | 4.0 files/s, 20 B/s"
        );
    }

    #[test]
    fn progress_cache_hit_message_includes_cache_counts() {
        let summary = ScanCacheProgressSummary {
            hits: 1,
            misses: 0,
            write_skipped: 0,
            pruned: 0,
        };

        let message =
            scan_cache_hit_message(summary, "windows.edge-cache", Path::new(r"C:\Cache"), 2048);

        assert_eq!(
            message,
            r"cache | 1 hit, 0 misses | hit windows.edge-cache | C:\Cache | 2.00 KiB"
        );
    }

    #[test]
    fn compact_progress_text_handles_tiny_widths() {
        assert_eq!(crate::progress::compact_progress_text("abcdef", 2), "..");
        assert_eq!(crate::progress::compact_progress_text("abcdef", 4), "...f");
    }
}
