use std::path::PathBuf;
use std::time::Duration;

use anyhow::{Context, Result, anyhow};
use indicatif::ProgressBar;
use rebecca_core::config::{AppRuntimeConfig, load_runtime_config};
use rebecca_core::executor::execute_cleanup_plan_parallel_with_policy;
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::ScanCancellationToken;
use rebecca_core::scan_cache::ScanCacheStore;
use rebecca_core::{DeleteMode, PlanRequest, Platform, RuleDefinition};

use crate::clean_view::ScanCacheProgressSummary;
use crate::cli::OutputMode;
use crate::output::{MachineErrorRendered, NdjsonEventWriter, format_bytes};
use crate::{info, output};
use rebecca_core::environment::SystemEnvironment;

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

pub(crate) struct WorkflowRunOptions<'a> {
    pub(crate) request: PlanRequest,
    pub(crate) rules: &'a [RuleDefinition],
    pub(crate) output_mode: OutputMode,
    pub(crate) yes: bool,
    pub(crate) no_progress: bool,
    pub(crate) scan_cache: bool,
    pub(crate) exclude_paths: Vec<PathBuf>,
    pub(crate) cancellation_message: &'static str,
    #[cfg_attr(windows, allow(dead_code))]
    pub(crate) unsupported_execution_message: &'static str,
    pub(crate) confirmation_kind: ConfirmationKind,
}

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

pub fn run(options: CleanOptions) -> Result<()> {
    let mode = if options.yes && !options.dry_run {
        DeleteMode::RecycleBin
    } else {
        DeleteMode::DryRun
    };

    let mut request = PlanRequest::for_platform(Platform::Windows, mode);
    request.selected_categories = options.categories;
    request.selected_rule_ids = options.rules;
    request.allow_moderate = options.allow_moderate;
    request.allow_risky = options.allow_risky;

    let catalog = rebecca_rules::builtin_rules()?;
    run_workflow(WorkflowRunOptions {
        request,
        rules: &catalog,
        output_mode: options.output_mode,
        yes: options.yes,
        no_progress: options.no_progress,
        scan_cache: options.scan_cache,
        exclude_paths: options.exclude_paths,
        cancellation_message: "Cleanup cancelled.",
        unsupported_execution_message: "cleanup execution is Windows-only at this stage; omit --yes to preview",
        confirmation_kind: ConfirmationKind::Cleanup,
    })
}

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

pub(crate) fn run_workflow_with_runtime_config(
    options: WorkflowRunOptions<'_>,
    runtime_config: AppRuntimeConfig,
) -> Result<()> {
    let cancellation = ScanCancellationToken::new();
    install_cancellation_handler(cancellation.clone())?;
    let mut progress = PlanProgressReporter::new(
        options.output_mode.is_human() && !options.no_progress,
        options
            .output_mode
            .is_ndjson()
            .then(|| NdjsonEventWriter::new("clean")),
    );
    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_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 plan_result = build_cleanup_plan_with_context(
        &options.request,
        options.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 output::print_plan_with_events(
            &plan,
            options.output_mode,
            scan_cache_summary,
            event_writer,
        );
    }

    #[cfg(not(windows))]
    {
        let err = rebecca_core::RebeccaError::PlatformUnavailable(
            options.unsupported_execution_message.to_string(),
        )
        .into();
        return finish_stream_with_error(event_writer, err);
    }

    #[cfg(windows)]
    {
        if plan.summary.allowed_targets == 0 {
            return output::print_plan_with_events(
                &plan,
                options.output_mode,
                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 = rebecca_windows::WindowsRecycleBinBackend::new();
        let mut execution_policy =
            ProtectionPolicy::new().with_protected_storage(&protected_storage);
        if !protected_paths.is_empty() {
            execution_policy = execution_policy.with_protected_paths(&protected_paths);
        }
        if let Err(err) =
            execute_cleanup_plan_parallel_with_policy(&mut plan, &backend, execution_policy)
        {
            return finish_stream_with_error(event_writer, err.into());
        }

        if let Err(err) =
            HistoryStore::new(runtime_config.app_paths.history_file).append_plan(&plan)
        {
            return finish_stream_with_error(event_writer, err.into());
        }

        output::print_plan_with_events(&plan, options.output_mode, 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)
}

fn install_cancellation_handler(token: ScanCancellationToken) -> Result<()> {
    ctrlc::set_handler(move || token.cancel()).context("failed to install Ctrl+C handler")?;
    Ok(())
}

struct PlanProgressReporter {
    bar: Option<ProgressBar>,
    event_writer: Option<NdjsonEventWriter>,
    event_error: Option<anyhow::Error>,
    scanned_targets: u64,
    scan_cache_summary: ScanCacheProgressSummary,
}

impl PlanProgressReporter {
    fn new(enabled: bool, event_writer: Option<NdjsonEventWriter>) -> Self {
        let bar = enabled.then(|| {
            let bar = ProgressBar::new_spinner();
            bar.enable_steady_tick(Duration::from_millis(120));
            bar.set_message("Building cleanup plan");
            bar
        });

        Self {
            bar,
            event_writer,
            event_error: None,
            scanned_targets: 0,
            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() {
            if let Some(writer) = &mut self.event_writer {
                if 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 } => {
                bar.set_message(format!("Scanning {rule_id}: {}", path.display()));
            }
            PlanProgressEvent::TargetFinished {
                status,
                estimated_bytes,
                ..
            } => {
                self.scanned_targets = self.scanned_targets.saturating_add(1);
                bar.set_message(format!(
                    "Scanned {} target(s); last {status:?}, {} bytes",
                    self.scanned_targets, estimated_bytes
                ));
                bar.tick();
            }
            PlanProgressEvent::FileMeasured {
                files_scanned,
                bytes_scanned,
                ..
            } => {
                bar.set_message(format!(
                    "Scanning files: {files_scanned}, {}",
                    format_bytes(bytes_scanned)
                ));
            }
            PlanProgressEvent::ScanCacheHit {
                rule_id,
                path,
                estimated_bytes,
            } => {
                bar.set_message(format!(
                    "Scan cache hit {rule_id}: {} ({})",
                    path.display(),
                    format_bytes(estimated_bytes)
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCacheMiss {
                rule_id,
                path,
                reason,
                ..
            } => {
                bar.set_message(format!(
                    "Scan cache miss {rule_id}: {} ({})",
                    path.display(),
                    reason.label()
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCacheWriteSkipped { rule_id, path } => {
                bar.set_message(format!(
                    "Scan cache write skipped {rule_id}: {}",
                    path.display()
                ));
                bar.tick();
            }
            PlanProgressEvent::ScanCachePruned { report } => {
                bar.set_message(format!(
                    "Scan cache pruned {} record(s) after inspecting {}",
                    report.pruned, report.inspected
                ));
                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
    }
}

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

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