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

use rebecca::core::plan::{CleanupPlan, CleanupSummary, CleanupTarget, CleanupTargetIssueReason};
use rebecca::core::project_artifacts::ProjectArtifactDiscoveryDiagnostic;
use rebecca::core::{EstimateProvenance, EstimateSource, TargetStatus};
use serde::Serialize;

use crate::text::format_count;

const LARGEST_ARTIFACT_LIMIT: usize = 5;
const INSIGHT_TOP_TARGET_LIMIT: usize = 10;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProjectArtifactPlanProjection<'a> {
    artifact_summaries: Vec<ProjectArtifactSummaryRow>,
    largest_targets: Vec<ProjectArtifactRow<'a>>,
    project_groups: Vec<ProjectArtifactGroup<'a>>,
    recently_modified: Vec<ProjectArtifactRow<'a>>,
    discovery_diagnostics: Vec<ProjectArtifactDiscoveryDiagnosticRow<'a>>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProjectArtifactSummaryRow {
    pub(crate) artifact_type: String,
    pub(crate) targets_label: String,
    pub(crate) estimated_bytes: u64,
    pub(crate) status_summary_label: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProjectArtifactGroup<'a> {
    pub(crate) project_path: PathBuf,
    pub(crate) targets_label: String,
    pub(crate) estimated_bytes: u64,
    pub(crate) targets: Vec<ProjectArtifactRow<'a>>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProjectArtifactRow<'a> {
    pub(crate) artifact_type: String,
    pub(crate) status: TargetStatus,
    pub(crate) status_label: &'static str,
    pub(crate) path: &'a Path,
    pub(crate) estimated_bytes: u64,
    pub(crate) estimate_source: EstimateSource,
    pub(crate) estimate_provenance: &'a EstimateProvenance,
    pub(crate) modified_at_unix_seconds: Option<u64>,
    pub(crate) reason: Option<&'a str>,
    pub(crate) restore_hint: Option<&'a str>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProjectArtifactDiscoveryDiagnosticRow<'a> {
    pub(crate) kind_label: &'static str,
    pub(crate) path: &'a Path,
    pub(crate) detail: &'a str,
}

#[derive(Debug, Serialize)]
pub(crate) struct ProjectArtifactInsightReport {
    pub(crate) roots: Vec<PathBuf>,
    pub(crate) summary: CleanupSummary,
    pub(crate) totals_by_root: Vec<ProjectArtifactInsightTotal>,
    pub(crate) totals_by_project: Vec<ProjectArtifactInsightTotal>,
    pub(crate) totals_by_artifact: Vec<ProjectArtifactInsightTotal>,
    pub(crate) top_targets: Vec<ProjectArtifactInsightTarget>,
    pub(crate) discovery_diagnostics: Vec<ProjectArtifactDiscoveryDiagnostic>,
}

#[derive(Debug, Serialize)]
pub(crate) struct ProjectArtifactInsightTotal {
    pub(crate) key: String,
    pub(crate) label: String,
    pub(crate) targets: usize,
    pub(crate) estimated_bytes: u64,
}

#[derive(Debug, Serialize)]
pub(crate) struct ProjectArtifactInsightTarget {
    pub(crate) rule_id: String,
    pub(crate) artifact: String,
    pub(crate) path: PathBuf,
    pub(crate) project_root: Option<PathBuf>,
    pub(crate) status: TargetStatus,
    pub(crate) estimated_bytes: u64,
    pub(crate) estimate_source: EstimateSource,
    #[serde(flatten)]
    pub(crate) estimate_provenance: EstimateProvenance,
    pub(crate) reason: Option<String>,
}

impl<'a> ProjectArtifactPlanProjection<'a> {
    pub(crate) fn new(plan: &'a CleanupPlan) -> Self {
        let mut summaries = BTreeMap::<String, ProjectArtifactSummaryAccumulator>::new();
        let mut grouped = BTreeMap::<PathBuf, Vec<ProjectArtifactRow<'a>>>::new();
        let mut recently_modified = Vec::new();

        for target in &plan.targets {
            let row = ProjectArtifactRow::from(target);
            summaries
                .entry(row.artifact_type.clone())
                .or_default()
                .record(&row);

            if target.reason_code == Some(CleanupTargetIssueReason::ProjectArtifactRecentlyModified)
            {
                recently_modified.push(ProjectArtifactRow::from(target));
            }

            grouped
                .entry(project_path_for(target.path.as_path()))
                .or_default()
                .push(row);
        }

        recently_modified.sort_by(|left, right| {
            left.path
                .cmp(right.path)
                .then_with(|| left.artifact_type.cmp(&right.artifact_type))
        });

        let project_groups = grouped
            .into_iter()
            .map(|(project_path, mut targets)| {
                targets.sort_by(|left, right| {
                    status_order(left.status)
                        .cmp(&status_order(right.status))
                        .then_with(|| left.artifact_type.cmp(&right.artifact_type))
                        .then_with(|| left.path.cmp(right.path))
                });

                let estimated_bytes = targets
                    .iter()
                    .map(|target| target.estimated_bytes)
                    .sum::<u64>();
                let targets_label = format_count(targets.len() as u64, "target", "targets");

                ProjectArtifactGroup {
                    project_path,
                    targets_label,
                    estimated_bytes,
                    targets,
                }
            })
            .collect();

        Self {
            artifact_summaries: artifact_summary_rows(summaries),
            largest_targets: largest_project_artifact_rows(plan),
            project_groups,
            recently_modified,
            discovery_diagnostics: discovery_diagnostic_rows(plan),
        }
    }

    pub(crate) fn artifact_summaries(&self) -> &[ProjectArtifactSummaryRow] {
        &self.artifact_summaries
    }

    pub(crate) fn largest_targets(&self) -> &[ProjectArtifactRow<'a>] {
        &self.largest_targets
    }

    pub(crate) fn project_groups(&self) -> &[ProjectArtifactGroup<'a>] {
        &self.project_groups
    }

    pub(crate) fn recently_modified(&self) -> &[ProjectArtifactRow<'a>] {
        &self.recently_modified
    }

    pub(crate) fn discovery_diagnostics(&self) -> &[ProjectArtifactDiscoveryDiagnosticRow<'a>] {
        &self.discovery_diagnostics
    }
}

impl ProjectArtifactInsightReport {
    pub(crate) fn from_plan(plan: &CleanupPlan) -> Self {
        Self {
            roots: plan.request.project_artifact_roots.clone(),
            summary: plan.summary.clone(),
            totals_by_root: insight_totals(plan, RootGrouping::Root),
            totals_by_project: insight_totals(plan, RootGrouping::Project),
            totals_by_artifact: insight_totals(plan, RootGrouping::Artifact),
            top_targets: insight_top_targets(plan),
            discovery_diagnostics: plan.discovery_diagnostics.clone(),
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ProjectArtifactSummaryAccumulator {
    targets: u64,
    estimated_bytes: u64,
    allowed_targets: u64,
    completed_targets: u64,
    failed_targets: u64,
    blocked_targets: u64,
    skipped_targets: u64,
}

#[derive(Debug, Clone, Copy)]
enum RootGrouping {
    Root,
    Project,
    Artifact,
}

#[derive(Debug, Default)]
struct InsightTotalAccumulator {
    targets: usize,
    estimated_bytes: u64,
}

impl ProjectArtifactSummaryAccumulator {
    fn record(&mut self, row: &ProjectArtifactRow<'_>) {
        self.targets = self.targets.saturating_add(1);
        self.estimated_bytes = self.estimated_bytes.saturating_add(row.estimated_bytes);

        match row.status {
            TargetStatus::Allowed => self.allowed_targets = self.allowed_targets.saturating_add(1),
            TargetStatus::Completed => {
                self.completed_targets = self.completed_targets.saturating_add(1)
            }
            TargetStatus::Failed => self.failed_targets = self.failed_targets.saturating_add(1),
            TargetStatus::Blocked => self.blocked_targets = self.blocked_targets.saturating_add(1),
            TargetStatus::Skipped => self.skipped_targets = self.skipped_targets.saturating_add(1),
        }
    }

    fn status_summary_label(&self) -> String {
        [
            (self.allowed_targets, "allowed"),
            (self.completed_targets, "completed"),
            (self.failed_targets, "failed"),
            (self.blocked_targets, "blocked"),
            (self.skipped_targets, "skipped"),
        ]
        .into_iter()
        .filter(|(count, _)| *count > 0)
        .map(|(count, label)| format!("{count} {label}"))
        .collect::<Vec<_>>()
        .join(", ")
    }
}

impl<'a> From<&'a CleanupTarget> for ProjectArtifactRow<'a> {
    fn from(target: &'a CleanupTarget) -> Self {
        Self {
            artifact_type: artifact_type_label(&target.rule_id, target.path.as_path()),
            status: target.status,
            status_label: target.status.label(),
            path: target.path.as_path(),
            estimated_bytes: target.estimated_bytes,
            estimate_source: target.estimate_source,
            estimate_provenance: &target.estimate_provenance,
            modified_at_unix_seconds: target.modified_at_unix_seconds,
            reason: target.reason.as_deref(),
            restore_hint: target.restore_hint.as_deref(),
        }
    }
}

impl<'a> From<&'a ProjectArtifactDiscoveryDiagnostic>
    for ProjectArtifactDiscoveryDiagnosticRow<'a>
{
    fn from(diagnostic: &'a ProjectArtifactDiscoveryDiagnostic) -> Self {
        Self {
            kind_label: diagnostic.kind.label(),
            path: diagnostic.path.as_path(),
            detail: diagnostic.detail.as_str(),
        }
    }
}

fn discovery_diagnostic_rows(plan: &CleanupPlan) -> Vec<ProjectArtifactDiscoveryDiagnosticRow<'_>> {
    plan.discovery_diagnostics
        .iter()
        .map(ProjectArtifactDiscoveryDiagnosticRow::from)
        .collect()
}

fn artifact_summary_rows(
    summaries: BTreeMap<String, ProjectArtifactSummaryAccumulator>,
) -> Vec<ProjectArtifactSummaryRow> {
    let mut rows = summaries
        .into_iter()
        .map(|(artifact_type, summary)| ProjectArtifactSummaryRow {
            artifact_type,
            targets_label: format_count(summary.targets, "target", "targets"),
            estimated_bytes: summary.estimated_bytes,
            status_summary_label: summary.status_summary_label(),
        })
        .collect::<Vec<_>>();

    rows.sort_by(|left, right| {
        right
            .estimated_bytes
            .cmp(&left.estimated_bytes)
            .then_with(|| left.artifact_type.cmp(&right.artifact_type))
    });

    rows
}

fn largest_project_artifact_rows(plan: &CleanupPlan) -> Vec<ProjectArtifactRow<'_>> {
    let mut targets = plan
        .targets
        .iter()
        .filter(|target| target.estimated_bytes > 0)
        .map(ProjectArtifactRow::from)
        .collect::<Vec<_>>();

    targets.sort_by(|left, right| {
        right
            .estimated_bytes
            .cmp(&left.estimated_bytes)
            .then_with(|| left.artifact_type.cmp(&right.artifact_type))
            .then_with(|| left.path.cmp(right.path))
    });

    targets.truncate(LARGEST_ARTIFACT_LIMIT);
    targets
}

fn insight_totals(plan: &CleanupPlan, grouping: RootGrouping) -> Vec<ProjectArtifactInsightTotal> {
    let mut totals = BTreeMap::<String, InsightTotalAccumulator>::new();

    for target in &plan.targets {
        let key = match grouping {
            RootGrouping::Root => root_for_target(plan, target.path.as_path())
                .unwrap_or_else(|| PathBuf::from("(outside configured roots)"))
                .display()
                .to_string(),
            RootGrouping::Project => target
                .project_artifact
                .as_ref()
                .map(|context| context.project_root.display().to_string())
                .unwrap_or_else(|| {
                    project_path_for(target.path.as_path())
                        .display()
                        .to_string()
                }),
            RootGrouping::Artifact => artifact_type_label(&target.rule_id, target.path.as_path()),
        };
        let entry = totals.entry(key).or_default();
        entry.targets = entry.targets.saturating_add(1);
        entry.estimated_bytes = entry.estimated_bytes.saturating_add(target.estimated_bytes);
    }

    totals
        .into_iter()
        .map(|(key, total)| ProjectArtifactInsightTotal {
            label: key.clone(),
            key,
            targets: total.targets,
            estimated_bytes: total.estimated_bytes,
        })
        .collect()
}

fn insight_top_targets(plan: &CleanupPlan) -> Vec<ProjectArtifactInsightTarget> {
    let mut targets = plan
        .targets
        .iter()
        .map(|target| ProjectArtifactInsightTarget {
            rule_id: target.rule_id.clone(),
            artifact: artifact_type_label(&target.rule_id, target.path.as_path()),
            path: target.path.clone(),
            project_root: target
                .project_artifact
                .as_ref()
                .map(|context| context.project_root.clone()),
            status: target.status,
            estimated_bytes: target.estimated_bytes,
            estimate_source: target.estimate_source,
            estimate_provenance: target.estimate_provenance.clone(),
            reason: target.reason.clone(),
        })
        .collect::<Vec<_>>();

    targets.sort_by(|left, right| {
        right
            .estimated_bytes
            .cmp(&left.estimated_bytes)
            .then_with(|| left.artifact.cmp(&right.artifact))
            .then_with(|| left.path.cmp(&right.path))
    });
    targets.truncate(INSIGHT_TOP_TARGET_LIMIT);
    targets
}

fn root_for_target(plan: &CleanupPlan, path: &Path) -> Option<PathBuf> {
    plan.request
        .project_artifact_roots
        .iter()
        .filter(|root| path.starts_with(root))
        .max_by_key(|root| root.components().count())
        .cloned()
}

fn artifact_type_label(rule_id: &str, path: &Path) -> String {
    match rule_id {
        "portable.project-artifact-cachedir-tag" => "CACHEDIR.TAG".to_string(),
        "portable.project-artifact-composer-vendor" => "vendor (Composer)".to_string(),
        "portable.project-artifact-dotnet-bin" => "bin (.NET)".to_string(),
        "portable.project-artifact-dotnet-obj" => "obj (.NET)".to_string(),
        _ => path
            .file_name()
            .and_then(|name| name.to_str())
            .map(str::to_string)
            .unwrap_or_else(|| {
                rule_id
                    .strip_prefix("portable.project-artifact-")
                    .unwrap_or(rule_id)
                    .replace('-', "_")
            }),
    }
}

fn project_path_for(path: &Path) -> PathBuf {
    path.parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| path.to_path_buf())
}

fn status_order(status: TargetStatus) -> usize {
    match status {
        TargetStatus::Allowed => 0,
        TargetStatus::Completed => 1,
        TargetStatus::Failed => 2,
        TargetStatus::Blocked => 3,
        TargetStatus::Skipped => 4,
    }
}

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

    use rebecca::core::plan::{CleanupPlan, CleanupSummary, CleanupTargetIssueReason};
    use rebecca::core::{CleanupWorkflow, DeleteMode, PlanRequest, Platform};

    use super::*;

    fn plan_with_targets(targets: Vec<CleanupTarget>) -> CleanupPlan {
        let mut plan = CleanupPlan {
            request: PlanRequest::for_platform(Platform::Windows, DeleteMode::DryRun)
                .with_workflow(CleanupWorkflow::ProjectArtifacts),
            summary: CleanupSummary::default(),
            targets,
            execution_report: None,
            discovery_diagnostics: Vec::new(),
        };
        plan.recompute_summary();
        plan
    }

    #[test]
    fn projection_groups_artifacts_by_parent_project_path() {
        let plan = plan_with_targets(vec![
            CleanupTarget::allowed(
                "portable.project-artifact-target",
                PathBuf::from("workspace/app/target"),
                4,
                DeleteMode::DryRun,
            ),
            CleanupTarget::allowed(
                "portable.project-artifact-node-modules",
                PathBuf::from("workspace/app/node_modules"),
                3,
                DeleteMode::DryRun,
            ),
        ]);

        let projection = ProjectArtifactPlanProjection::new(&plan);

        assert_eq!(projection.project_groups().len(), 1);
        let group = &projection.project_groups()[0];
        assert_eq!(group.project_path, PathBuf::from("workspace/app"));
        assert_eq!(group.targets_label, "2 targets");
        assert_eq!(group.estimated_bytes, 7);
        assert_eq!(group.targets[0].artifact_type, "node_modules");
        assert_eq!(group.targets[1].artifact_type, "target");

        let summaries = projection.artifact_summaries();
        assert_eq!(summaries[0].artifact_type, "target");
        assert_eq!(summaries[0].targets_label, "1 target");
        assert_eq!(summaries[0].estimated_bytes, 4);
        assert_eq!(summaries[0].status_summary_label, "1 allowed");
        assert_eq!(summaries[1].artifact_type, "node_modules");
        assert_eq!(summaries[1].estimated_bytes, 3);

        let largest = projection
            .largest_targets()
            .iter()
            .map(|target| target.artifact_type.as_str())
            .collect::<Vec<_>>();
        assert_eq!(largest, ["target", "node_modules"]);
    }

    #[test]
    fn projection_extracts_recently_modified_artifacts() {
        let target = CleanupTarget::skipped_with_reason_code(
            "portable.project-artifact-node-modules",
            PathBuf::from("workspace/app/node_modules"),
            DeleteMode::DryRun,
            CleanupTargetIssueReason::ProjectArtifactRecentlyModified,
            "project artifact was modified within the last 7 days",
        );
        let plan = plan_with_targets(vec![target]);

        let projection = ProjectArtifactPlanProjection::new(&plan);

        assert_eq!(projection.recently_modified().len(), 1);
        assert_eq!(
            projection.recently_modified()[0].artifact_type,
            "node_modules"
        );
        assert_eq!(
            projection.recently_modified()[0].reason,
            Some("project artifact was modified within the last 7 days")
        );
    }

    #[test]
    fn projection_labels_context_sensitive_artifact_types() {
        let plan = plan_with_targets(vec![
            CleanupTarget::allowed(
                "portable.project-artifact-composer-vendor",
                PathBuf::from("workspace/php-app/vendor"),
                1,
                DeleteMode::DryRun,
            ),
            CleanupTarget::allowed(
                "portable.project-artifact-dotnet-bin",
                PathBuf::from("workspace/dotnet-app/bin"),
                1,
                DeleteMode::DryRun,
            ),
            CleanupTarget::allowed(
                "portable.project-artifact-cachedir-tag",
                PathBuf::from("workspace/app/tool-cache"),
                1,
                DeleteMode::DryRun,
            ),
        ]);

        let projection = ProjectArtifactPlanProjection::new(&plan);
        let artifact_types = projection
            .project_groups()
            .iter()
            .flat_map(|group| {
                group
                    .targets
                    .iter()
                    .map(|target| target.artifact_type.as_str())
            })
            .collect::<Vec<_>>();

        assert!(artifact_types.contains(&"vendor (Composer)"));
        assert!(artifact_types.contains(&"bin (.NET)"));
        assert!(artifact_types.contains(&"CACHEDIR.TAG"));
    }
}
