use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use crate::app_leftovers::derive_app_leftover_candidates;
use crate::applications::{ApplicationDiscovery, NoopApplicationDiscovery};
use crate::config::AppStorageEntry;
use crate::discovery::{TargetResolution, resolve_rule_target_with_applications};
use crate::environment::{Environment, SystemEnvironment};
use crate::error::{RebeccaError, Result};
use crate::model::{CleanupWorkflow, PlanRequest, RuleDefinition};
use crate::plan::{CleanupPlan, CleanupTarget, CleanupTargetIssueReason};
use crate::project_artifacts::{
    ProjectArtifactScanOptions, discover_project_artifacts, project_artifact_matches_selectors,
    validate_project_artifact_selectors,
};
use crate::protection::ProtectionPolicy;
use crate::safety::{
    PATH_DOES_NOT_EXIST_REASON, PathDisposition, assess_existing_path_with_policy,
};
use crate::scan::{ScanCancellationToken, run_scoped_scan};
use crate::scan_cache::{ScanCacheMiss, ScanCachePolicy, ScanCachePruneReport, ScanCacheStore};
use rayon::prelude::*;

mod measure;

use measure::{
    PathMeasureProgressEvent, app_leftover_rule_id, app_leftover_skipped_target, dedupe_key,
    emit_measured_target_progress, emit_target_finished, finalize_plan,
    measure_app_leftover_candidate, measure_path_with_optional_scan_cache,
    measure_project_artifact_candidate, prune_scan_cache,
};

#[derive(Debug, Clone, Copy)]
pub enum PlanProgressEvent<'a> {
    TargetScanning {
        rule_id: &'a str,
        path: &'a Path,
    },
    TargetFinished {
        rule_id: &'a str,
        path: &'a Path,
        status: crate::TargetStatus,
        estimated_bytes: u64,
    },
    FileMeasured {
        rule_id: &'a str,
        target_path: &'a Path,
        path: &'a Path,
        file_size: u64,
        files_scanned: u64,
        bytes_scanned: u64,
    },
    ScanCacheHit {
        rule_id: &'a str,
        path: &'a Path,
        estimated_bytes: u64,
    },
    ScanCacheMiss {
        rule_id: &'a str,
        path: &'a Path,
        reason: ScanCacheMiss,
        pruned: bool,
    },
    ScanCacheWriteSkipped {
        rule_id: &'a str,
        path: &'a Path,
    },
    ScanCachePruned {
        report: ScanCachePruneReport,
    },
}

#[derive(Debug, Clone, Copy)]
pub struct PlanBuildContext<'a> {
    cancellation: &'a ScanCancellationToken,
    scan_cache: Option<&'a ScanCacheStore>,
    scan_cache_policy: ScanCachePolicy,
    protection_policy: ProtectionPolicy<'a>,
}

impl<'a> PlanBuildContext<'a> {
    pub fn new(cancellation: &'a ScanCancellationToken) -> Self {
        Self {
            cancellation,
            scan_cache: None,
            scan_cache_policy: ScanCachePolicy::default(),
            protection_policy: ProtectionPolicy::new(),
        }
    }

    pub fn with_scan_cache(mut self, scan_cache: &'a ScanCacheStore) -> Self {
        self.scan_cache = Some(scan_cache);
        self
    }

    pub fn with_scan_cache_policy(mut self, scan_cache_policy: ScanCachePolicy) -> Self {
        self.scan_cache_policy = scan_cache_policy;
        self
    }

    pub fn with_protected_storage(mut self, protected_storage: &'a [AppStorageEntry]) -> Self {
        self.protection_policy = self
            .protection_policy
            .with_protected_storage(protected_storage);
        self
    }

    pub fn with_protected_paths(mut self, protected_paths: &'a [PathBuf]) -> Self {
        self.protection_policy = self.protection_policy.with_protected_paths(protected_paths);
        self
    }

    pub fn cancellation(&self) -> &'a ScanCancellationToken {
        self.cancellation
    }

    pub fn scan_cache(&self) -> Option<&'a ScanCacheStore> {
        self.scan_cache
    }

    pub fn scan_cache_policy(&self) -> ScanCachePolicy {
        self.scan_cache_policy
    }

    pub fn protection_policy(&self) -> ProtectionPolicy<'a> {
        self.protection_policy
    }
}

pub fn build_cleanup_plan(request: &PlanRequest, rules: &[RuleDefinition]) -> Result<CleanupPlan> {
    build_cleanup_plan_with_progress(request, rules, |_| {})
}

pub fn build_cleanup_plan_with_progress<F>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    progress: F,
) -> Result<CleanupPlan>
where
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    build_cleanup_plan_with_environment_and_progress_and_cancellation(
        request,
        rules,
        &SystemEnvironment,
        &ScanCancellationToken::new(),
        progress,
    )
}

pub fn build_cleanup_plan_with_environment(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &impl Environment,
) -> Result<CleanupPlan> {
    build_cleanup_plan_with_environment_and_progress(request, rules, env, |_| {})
}

pub fn build_cleanup_plan_with_environment_and_progress<E, F>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &E,
    progress: F,
) -> Result<CleanupPlan>
where
    E: Environment,
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    build_cleanup_plan_with_environment_and_progress_and_cancellation(
        request,
        rules,
        env,
        &ScanCancellationToken::new(),
        progress,
    )
}

pub fn build_cleanup_plan_with_environment_and_progress_and_cancellation<E, F>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &E,
    cancellation: &ScanCancellationToken,
    progress: F,
) -> Result<CleanupPlan>
where
    E: Environment,
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    build_cleanup_plan_with_environment_applications_progress_and_cancellation(
        request,
        rules,
        env,
        &NoopApplicationDiscovery::new(),
        cancellation,
        progress,
    )
}

pub fn build_cleanup_plan_with_environment_and_applications<E, A>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &E,
    applications: &A,
) -> Result<CleanupPlan>
where
    E: Environment,
    A: ApplicationDiscovery + ?Sized,
{
    build_cleanup_plan_with_context(
        request,
        rules,
        env,
        applications,
        PlanBuildContext::new(&ScanCancellationToken::new()),
        |_| {},
    )
}

pub fn build_cleanup_plan_with_environment_applications_progress_and_cancellation<E, A, F>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &E,
    applications: &A,
    cancellation: &ScanCancellationToken,
    progress: F,
) -> Result<CleanupPlan>
where
    E: Environment,
    A: ApplicationDiscovery + ?Sized,
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    build_cleanup_plan_with_context(
        request,
        rules,
        env,
        applications,
        PlanBuildContext::new(cancellation),
        progress,
    )
}

pub fn build_cleanup_plan_with_context<E, A, F>(
    request: &PlanRequest,
    rules: &[RuleDefinition],
    env: &E,
    applications: &A,
    context: PlanBuildContext<'_>,
    mut progress: F,
) -> Result<CleanupPlan>
where
    E: Environment,
    A: ApplicationDiscovery + ?Sized,
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    if request.workflow == CleanupWorkflow::AppLeftovers {
        return build_app_leftover_plan_with_context(request, env, applications, context, progress);
    }
    if request.workflow == CleanupWorkflow::ProjectArtifacts {
        return build_project_artifact_plan_with_context(request, context, progress);
    }

    let selection = request.selection();
    selection.validate_against_rules(rules)?;

    let mut candidates = Vec::new();
    let mut seen_paths = BTreeSet::new();

    for rule in rules {
        if rule.platform != request.platform {
            continue;
        }

        if !selection.matches_rule(rule) {
            continue;
        }

        if !request.allows_safety_level(rule.safety_level) {
            for spec in &rule.path_templates {
                let reason = match rule.safety_level.opt_in_flag() {
                    Some(flag) => format!("{} rule requires {}", rule.safety_level.label(), flag),
                    None => format!(
                        "{} rule requires explicit opt-in",
                        rule.safety_level.label()
                    ),
                };
                candidates.push(with_rule_restore_hint(
                    CleanupTarget::skipped_with_reason_code(
                        rule.id.clone(),
                        spec.placeholder_path(),
                        request.mode,
                        CleanupTargetIssueReason::SafetyOptInRequired,
                        reason,
                    ),
                    rule,
                ));
            }
            continue;
        }

        for spec in &rule.path_templates {
            let expanded_paths =
                match resolve_rule_target_with_applications(spec, env, applications) {
                    Ok(TargetResolution::Paths(paths)) => paths,
                    Ok(TargetResolution::Skipped(reason)) => {
                        candidates.push(with_rule_restore_hint(
                            CleanupTarget::skipped_with_reason_code(
                                rule.id.clone(),
                                spec.placeholder_path(),
                                request.mode,
                                CleanupTargetIssueReason::TargetDiscoverySkipped,
                                reason,
                            ),
                            rule,
                        ));
                        continue;
                    }
                    Err(err) => {
                        candidates.push(with_rule_restore_hint(
                            CleanupTarget::blocked_with_reason_code(
                                rule.id.clone(),
                                spec.placeholder_path(),
                                request.mode,
                                CleanupTargetIssueReason::TargetDiscoveryFailed,
                                err.to_string(),
                            ),
                            rule,
                        ));
                        continue;
                    }
                };

            for expanded in expanded_paths {
                let path_key = dedupe_key(&expanded, request.platform);
                if !seen_paths.insert(path_key) {
                    let target = with_rule_restore_hint(
                        CleanupTarget::skipped_with_reason_code(
                            rule.id.clone(),
                            expanded,
                            request.mode,
                            CleanupTargetIssueReason::DuplicateTargetPath,
                            "duplicate target path already covered",
                        ),
                        rule,
                    );
                    emit_target_finished(&mut progress, &target);
                    candidates.push(target);
                    continue;
                }

                progress(PlanProgressEvent::TargetScanning {
                    rule_id: &rule.id,
                    path: &expanded,
                });

                match assess_existing_path_with_policy(&expanded, context.protection_policy()) {
                    PathDisposition::Allowed => {
                        match measure_path_with_optional_scan_cache(&expanded, context, |event| {
                            match event {
                                PathMeasureProgressEvent::Scan(
                                    crate::scan::ScanProgressEvent::FileMeasured {
                                        path,
                                        file_size,
                                        files_scanned,
                                        bytes_scanned,
                                    },
                                ) => {
                                    progress(PlanProgressEvent::FileMeasured {
                                        rule_id: &rule.id,
                                        target_path: &expanded,
                                        path,
                                        file_size,
                                        files_scanned,
                                        bytes_scanned,
                                    });
                                }
                                PathMeasureProgressEvent::ScanCacheHit { report } => {
                                    progress(PlanProgressEvent::ScanCacheHit {
                                        rule_id: &rule.id,
                                        path: &expanded,
                                        estimated_bytes: report.bytes_scanned,
                                    });
                                }
                                PathMeasureProgressEvent::ScanCacheMiss { reason, pruned } => {
                                    progress(PlanProgressEvent::ScanCacheMiss {
                                        rule_id: &rule.id,
                                        path: &expanded,
                                        reason,
                                        pruned,
                                    });
                                }
                                PathMeasureProgressEvent::ScanCacheWriteSkipped => {
                                    progress(PlanProgressEvent::ScanCacheWriteSkipped {
                                        rule_id: &rule.id,
                                        path: &expanded,
                                    });
                                }
                            }
                        }) {
                            Ok(report) => {
                                let size = report.bytes_scanned;
                                let target = CleanupTarget::allowed(
                                    rule.id.clone(),
                                    expanded,
                                    size,
                                    request.mode,
                                );
                                let target = with_rule_restore_hint(target, rule);
                                emit_target_finished(&mut progress, &target);
                                candidates.push(target);
                            }
                            Err(err @ RebeccaError::OperationCancelled(_)) => return Err(err),
                            Err(err) => {
                                let target = CleanupTarget::failed_with_reason_code(
                                    rule.id.clone(),
                                    expanded,
                                    request.mode,
                                    0,
                                    CleanupTargetIssueReason::ScanFailed,
                                    err.to_string(),
                                );
                                let target = with_rule_restore_hint(target, rule);
                                emit_target_finished(&mut progress, &target);
                                candidates.push(target);
                            }
                        }
                    }
                    PathDisposition::Missing => {
                        let target = with_rule_restore_hint(
                            CleanupTarget::skipped_with_reason_code(
                                rule.id.clone(),
                                expanded,
                                request.mode,
                                CleanupTargetIssueReason::SafetyPolicySkipped,
                                PATH_DOES_NOT_EXIST_REASON,
                            ),
                            rule,
                        );
                        emit_target_finished(&mut progress, &target);
                        candidates.push(target);
                    }
                    PathDisposition::Skipped(reason) => {
                        let target = with_rule_restore_hint(
                            CleanupTarget::skipped_with_reason_code(
                                rule.id.clone(),
                                expanded,
                                request.mode,
                                CleanupTargetIssueReason::SafetyPolicySkipped,
                                reason,
                            ),
                            rule,
                        );
                        emit_target_finished(&mut progress, &target);
                        candidates.push(target);
                    }
                    PathDisposition::Blocked(reason) => {
                        let target = with_rule_restore_hint(
                            CleanupTarget::blocked_with_reason_code(
                                rule.id.clone(),
                                expanded,
                                request.mode,
                                CleanupTargetIssueReason::SafetyPolicyBlocked,
                                reason,
                            ),
                            rule,
                        );
                        emit_target_finished(&mut progress, &target);
                        candidates.push(target);
                    }
                }
            }
        }
    }

    prune_scan_cache(context, &mut progress);
    Ok(finalize_plan(request.clone(), candidates))
}

pub fn build_project_artifact_plan_with_context<F>(
    request: &PlanRequest,
    context: PlanBuildContext<'_>,
    mut progress: F,
) -> Result<CleanupPlan>
where
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    validate_project_artifact_selectors(&request.project_artifact_selectors)?;
    let scan_options = ProjectArtifactScanOptions::new(request.project_artifact_roots.clone())
        .with_max_depth(request.project_artifact_max_depth);
    let artifacts = discover_project_artifacts(&scan_options, context.cancellation())?;
    let filtered_artifacts = artifacts
        .into_iter()
        .filter(|artifact| {
            project_artifact_matches_selectors(
                artifact.definition,
                &request.project_artifact_selectors,
            )
        })
        .collect::<Vec<_>>();

    for artifact in &filtered_artifacts {
        progress(PlanProgressEvent::TargetScanning {
            rule_id: artifact.definition.rule_id,
            path: &artifact.path,
        });
    }

    let candidates = run_scoped_scan(|| {
        filtered_artifacts
            .into_par_iter()
            .map(|artifact| {
                measure_project_artifact_candidate(
                    artifact,
                    request.mode,
                    request.project_artifact_min_age_days,
                    context,
                )
            })
            .collect::<Vec<_>>()
    });

    let mut plan_candidates = Vec::with_capacity(candidates.len());
    for measured in candidates {
        let measured = measured?;
        emit_measured_target_progress(&mut progress, &measured);
        emit_target_finished(&mut progress, &measured.target);
        plan_candidates.push(measured.target);
    }

    prune_scan_cache(context, &mut progress);
    Ok(finalize_plan(request.clone(), plan_candidates))
}

pub fn build_app_leftover_plan_with_context<E, A, F>(
    request: &PlanRequest,
    env: &E,
    applications: &A,
    context: PlanBuildContext<'_>,
    mut progress: F,
) -> Result<CleanupPlan>
where
    E: Environment,
    A: ApplicationDiscovery + ?Sized,
    F: for<'a> FnMut(PlanProgressEvent<'a>),
{
    let installed = applications.installed_applications()?;
    let leftovers = derive_app_leftover_candidates(&installed, env);
    let mut seen_paths = BTreeSet::new();
    let mut duplicate_targets = Vec::new();
    let mut unique_leftovers = Vec::new();

    for leftover in leftovers {
        let path_key = dedupe_key(&leftover.path, request.platform);
        if !seen_paths.insert(path_key) {
            let target = app_leftover_skipped_target(
                &leftover,
                request.mode,
                CleanupTargetIssueReason::DuplicateTargetPath,
                "duplicate target path already covered",
            );
            emit_target_finished(&mut progress, &target);
            duplicate_targets.push(target);
            continue;
        }

        progress(PlanProgressEvent::TargetScanning {
            rule_id: app_leftover_rule_id(&leftover),
            path: &leftover.path,
        });
        unique_leftovers.push(leftover);
    }

    let measured_targets = run_scoped_scan(|| {
        unique_leftovers
            .into_par_iter()
            .map(|leftover| measure_app_leftover_candidate(leftover, request.mode, context))
            .collect::<Vec<_>>()
    });

    let mut candidates = duplicate_targets;
    for measured in measured_targets {
        let measured = measured?;
        emit_measured_target_progress(&mut progress, &measured);
        emit_target_finished(&mut progress, &measured.target);
        candidates.push(measured.target);
    }

    prune_scan_cache(context, &mut progress);
    Ok(finalize_plan(request.clone(), candidates))
}

fn with_rule_restore_hint(target: CleanupTarget, rule: &RuleDefinition) -> CleanupTarget {
    target.with_restore_hint(rule.restore_hint.clone())
}

pub fn validate_rule_catalog(rules: &[RuleDefinition]) -> Result<()> {
    let mut ids = BTreeSet::new();
    let mut target_specs = BTreeMap::<String, String>::new();

    for rule in rules {
        if rule.id.trim().is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(
                "rule id cannot be empty".to_string(),
            ));
        }

        if !ids.insert(rule.id.clone()) {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "duplicate rule id: {}",
                rule.id
            )));
        }

        if rule.category.trim().is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "rule {} must define a category",
                rule.id
            )));
        }

        if rule.name.trim().is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "rule {} must define a name",
                rule.id
            )));
        }

        if rule.provenance.license.trim().is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "rule {} is missing provenance license",
                rule.id
            )));
        }

        if rule.provenance.notes.trim().is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "rule {} is missing provenance notes",
                rule.id
            )));
        }

        if rule.path_templates.is_empty() {
            return Err(RebeccaError::RuleCatalogInvalid(format!(
                "rule {} must define at least one path template",
                rule.id
            )));
        }

        for spec in &rule.path_templates {
            if spec
                .placeholder_path()
                .as_os_str()
                .to_string_lossy()
                .trim()
                .is_empty()
            {
                return Err(RebeccaError::RuleCatalogInvalid(format!(
                    "rule {} contains an empty target path",
                    rule.id
                )));
            }

            let key = spec.dedupe_key(rule.platform);
            if let Some(previous_rule) = target_specs.insert(key.clone(), rule.id.clone()) {
                return Err(RebeccaError::RuleCatalogInvalid(format!(
                    "duplicate target spec {key} used by rules {previous_rule} and {}",
                    rule.id
                )));
            }
        }
    }

    Ok(())
}
