use serde::{Deserialize, Serialize};
use super::types::NormalizedEntity;
use cannon_common::spec::{
    IdentitySpec, ReferenceIdentifierSpec, RelationSpec, RuleSpec,
};

use cannon_common::ir::{
    BlockingKey, BlockingTransformation, CompiledBlocking, CompiledReferenceIdentifier,
    CompiledRelation, CompiledRule, CompiledRuleType, DecisionConfig, FieldSurvivorshipPolicy,
    IdentityPlan, MatchGraph, ResolvedSource, SurvivorshipPolicy, TemporalConfig,
    CompiledExclusion, CompiledCompliance, CompiledFreshness, CompiledHierarchy,
    CompiledAudit, PlanMetadata, ScoringMethod,
    CompiledFieldSchema, CompiledSampling, CompiledGovernance,
    CompiledMlEnsembleConfig, CompiledCustomScoringConfig,
    CompiledFellegiSunterPlan, CompiledFsField, FsComparatorType, CompiledEmTraining,
    CompiledTieBreaker,
};

use super::observability::{
    PlanSummary, SourcesSummary, SourceDetail, MatchingSummary, RulesSummary, RuleDetail,
    BlockingSummary, SurvivorshipFieldSummary, ExecutionGraph, ScaleEstimate, RiskAnalysis,
    RiskFlag, FsPlanSummary, FsFieldWeight,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImpactAnalysis {
    pub total_entities: usize,
    pub distinct_blocks: usize,
    pub max_block_size: usize,
    pub potential_matches: usize,
    pub coverage_percent: f64,
}

#[derive(Debug, Clone)]
pub struct CompilationError {
    pub message: String,
}

impl std::fmt::Display for CompilationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for CompilationError {}

pub struct SpecCompiler;

impl SpecCompiler {
    pub fn compile(spec: &IdentitySpec) -> Result<IdentityPlan, CompilationError> {
        let sources = spec.sources
            .iter()
            .map(|s| {
                let adapter = s.resolved_adapter();
                let location = s.resolved_location();
                let id_field = s.resolved_primary_key();
                let schema = s.schema.as_ref().map(|sch| {
                    sch.iter().map(|(k, v)| (k.clone(), CompiledFieldSchema {
                        field_type: v.field_type.clone(),
                        pii: v.pii,
                        nullable: v.nullable,
                    })).collect()
                });
                let sampling = s.sampling.as_ref().map(|samp| CompiledSampling {
                    strategy: format!("{:?}", samp.strategy).to_lowercase(),
                    rate: samp.rate,
                });
                let pii_fields = s.pii_fields();

                ResolvedSource {
                    name: s.name.clone(),
                    adapter,
                    location,
                    id_field,
                    field_mappings: s.attributes.clone(),
                    temporal: s.temporal.as_ref().map(|t| TemporalConfig {
                        valid_from_field: t.valid_from.clone(),
                        valid_to_field: t.valid_to.clone(),
                        strategy: t.strategy.clone(),
                    }),
                    freshness: s.freshness.as_ref().map(|f| {
                        CompiledFreshness {
                            max_age_seconds: Self::parse_duration(&f.max_age),
                            warn_after_seconds: f.warn_after.as_ref().map(|w| Self::parse_duration(w)),
                        }
                    }),
                    schema,
                    sampling,
                    tags: s.tags.clone(),
                    pii_fields,
                }
            })
            .collect();

        let blocking = match &spec.blocking {
            Some(b) => {
                let keys = b.keys
                    .iter()
                    .map(|key_fields: &Vec<String>| BlockingKey {
                        fields: key_fields.clone(),
                        transformations: Self::infer_transformations(key_fields),
                    })
                    .collect();

                CompiledBlocking {
                    strategy: b.strategy.clone(),
                    keys,
                    fallback_sample_rate: b.fallback.as_ref().and_then(|f| f.sample_rate),
                    lsh_bands: None,
                    lsh_rows: None,
                    neighborhood_window: Some(50),
                    sort_fields: vec![],
                }
            }
            None => CompiledBlocking {
                strategy: cannon_common::ir::BlockingStrategy::Composite,
                keys: vec![],
                fallback_sample_rate: None,
                lsh_bands: None,
                lsh_rows: None,
                neighborhood_window: None,
                sort_fields: vec![],
            },
        };

        // Early scoring spec reference for FS detection
        let scoring_spec = spec.decision.scoring.as_ref();

        // Detect Fellegi-Sunter strategy
        let is_fellegi_sunter = scoring_spec
            .and_then(|s| s.strategy.as_ref())
            .map(|s| *s == cannon_common::spec::ScoringStrategy::FellegiSunter)
            .unwrap_or(false);

        // Generate match rules: from FS fields (implicit) + explicit rules
        let mut match_rules: Vec<CompiledRule> = spec.rules
            .iter()
            .map(Self::compile_rule)
            .collect();

        // When FS is active, generate implicit rules from FS field definitions
        if is_fellegi_sunter {
            if let Some(fs_fields) = scoring_spec.and_then(|s| s.fields.as_ref()) {
                let implicit_rules = Self::compile_fs_implicit_rules(fs_fields);
                // Only add implicit rules for fields not already covered by explicit rules
                let existing_names: std::collections::HashSet<String> =
                    match_rules.iter().map(|r| r.name.clone()).collect();
                for rule in implicit_rules {
                    if !existing_names.contains(&rule.name) {
                        match_rules.push(rule);
                    }
                }
            }
        }

        let leaf_count = match_rules.iter().map(Self::count_leaves).sum();

        let survivorship = match &spec.survivorship {
            Some(s) => {
                let mut field_policies = std::collections::HashMap::new();
                for override_spec in &s.overrides {
                    field_policies.insert(
                        override_spec.field.clone(),
                        FieldSurvivorshipPolicy {
                            strategy: override_spec.strategy.clone(),
                            source_priority: if override_spec.priority.is_empty() {
                                None
                            } else {
                                Some(override_spec.priority.clone())
                            },
                            aggregate_fn: override_spec.function.as_ref().map(|f| format!("{:?}", f)),
                        },
                    );
                }

                SurvivorshipPolicy {
                    default_strategy: s.default.clone(),
                    field_policies,
                }
            }
            None => SurvivorshipPolicy {
                default_strategy: cannon_common::ir::SurvivorshipStrategy::MostRecent,
                field_policies: std::collections::HashMap::new(),
            },
        };

        let scoring_method = scoring_spec
            .map(|s| s.method.clone())
            .unwrap_or_default();

        // Compile ML ensemble config if present
        let ml_ensemble_config = scoring_spec
            .and_then(|s| s.ml_ensemble.as_ref())
            .map(|cfg| CompiledMlEnsembleConfig {
                coefficients: cfg.coefficients.clone(),
                bias: cfg.bias,
            });

        // Compile custom scoring config if present
        let custom_scoring_config = scoring_spec
            .and_then(|s| s.custom.as_ref())
            .filter(|cfg| !cfg.expression.is_empty())
            .map(|cfg| {
                // Validate expression at compile time
                if let Err(e) = super::expression::compile_expression(&cfg.expression) {
                    tracing::error!("Invalid custom scoring expression: {}", e);
                }
                CompiledCustomScoringConfig {
                    expression: cfg.expression.clone(),
                }
            });

        // Compile Fellegi-Sunter plan if strategy is set
        let fellegi_sunter = if is_fellegi_sunter {
            Self::compile_fs_plan(scoring_spec.unwrap())
        } else {
            None
        };

        // Determine scoring method: FS strategy overrides `method`
        let resolved_scoring_method = if is_fellegi_sunter {
            ScoringMethod::FellegiSunter
        } else {
            match scoring_method {
                cannon_common::spec::ScoringMethod::WeightedSum => ScoringMethod::WeightedSum,
                cannon_common::spec::ScoringMethod::MlEnsemble => ScoringMethod::MlEnsemble,
                cannon_common::spec::ScoringMethod::Custom => ScoringMethod::Custom,
                cannon_common::spec::ScoringMethod::FellegiSunter => ScoringMethod::FellegiSunter,
            }
        };

        // Compile tie-breaking rules
        let tie_breaking = spec.decision.tie_breaking.as_ref()
            .map(|tbs| tbs.iter().map(|tb| match tb {
                cannon_common::spec::TieBreaker::ExactMatchPriority => CompiledTieBreaker::ExactMatchPriority,
                cannon_common::spec::TieBreaker::LowestEntityId => CompiledTieBreaker::LowestEntityId,
                cannon_common::spec::TieBreaker::EarliestTimestamp => CompiledTieBreaker::EarliestTimestamp,
            }).collect())
            .unwrap_or_else(|| vec![
                // Default: deterministic ordering even without explicit config
                CompiledTieBreaker::LowestEntityId,
            ]);

        let decision = DecisionConfig {
            match_threshold: spec.decision.thresholds.match_threshold,
            review_threshold: spec.decision.thresholds.review,
            reject_threshold: spec.decision.thresholds.reject,
            conflict_strategy: spec.decision.conflict_strategy.clone(),
            review_webhook: spec.decision.review_queue.as_ref().and_then(|r| r.webhook.clone()),
            scoring_method: resolved_scoring_method,
            ml_ensemble_config,
            custom_scoring_config,
            fellegi_sunter,
            tie_breaking,
        };

        let reference_identifiers = spec.reference_identifiers
            .as_ref()
            .map(|ids| {
                ids.iter()
                    .map(|rid: &ReferenceIdentifierSpec| CompiledReferenceIdentifier {
                        name: rid.name.clone(),
                        id_type: rid.id_type.clone(),
                        authority: rid.authority.clone(),
                        field: rid.field.clone(),
                        match_weight: rid.match_weight,
                    })
                    .collect()
            })
            .unwrap_or_default();

        let relations = spec.relations
            .as_ref()
            .map(|rels| {
                rels.iter()
                    .map(|rel: &RelationSpec| CompiledRelation {
                        name: rel.name.clone(),
                        from_entity: rel.from_entity.clone(),
                        to_entity: rel.to_entity.clone(),
                        join_key: rel.join_key.clone(),
                        cardinality: rel.cardinality.clone(),
                        propagate_match: rel.propagate_match,
                    })
                    .collect()
            })
            .unwrap_or_default();

        // Compile exclusions
        let exclusions = spec.exclusions
            .as_ref()
            .map(|excls| {
                excls.iter().map(|e| match e {
                    cannon_common::spec::ExclusionSpec::FieldValue { field, operator, value } => {
                        CompiledExclusion::FieldValue {
                            field: field.clone(),
                            operator: operator.clone(),
                            value: value.clone(),
                        }
                    }
                    cannon_common::spec::ExclusionSpec::ExplicitPair { source: _, left_id_field, right_id_field } => {
                        CompiledExclusion::ExplicitPair {
                            left_id: left_id_field.clone(),
                            right_id: right_id_field.clone(),
                        }
                    }
                    cannon_common::spec::ExclusionSpec::CrossTenant { enabled } => {
                        CompiledExclusion::CrossTenant { enabled: *enabled }
                    }
                }).collect()
            })
            .unwrap_or_default();

        // Compile compliance
        let compliance = spec.entity.compliance.as_ref().map(|c| {
            CompiledCompliance {
                frameworks: c.frameworks.clone(),
                pii_fields: c.pii_fields.clone(),
                audit_required: c.audit_required,
                retention_days: c.retention_days,
            }
        });

        // Compile hierarchy
        let hierarchy = spec.entity.hierarchy.as_ref().map(|h| {
            CompiledHierarchy {
                parent_field: h.parent_field.clone(),
                max_depth: h.depth,
                inherited_fields: h.inheritance.clone(),
            }
        });

        // Compile audit config
        let audit = spec.decision.audit.as_ref().map(|a| {
            CompiledAudit {
                log_all_comparisons: a.log_all_comparisons,
                log_decisions: a.log_decisions,
                log_conflicts: a.log_conflicts,
            }
        });

        // Compile metadata
        let metadata = spec.metadata.as_ref().map(|m| {
            PlanMetadata {
                name: m.name.clone(),
                description: m.description.clone(),
                owner: m.owner.clone(),
                tags: m.tags.clone(),
            }
        });

        // Compile governance
        let governance = spec.governance.as_ref().map(|g| {
            CompiledGovernance {
                require_freshness: g.require_freshness,
                require_schema: g.require_schema,
                required_tags: g.required_tags.clone(),
                log_probabilities: g.log_probabilities,
                require_shadow_on_threshold_change: g.require_shadow_on_threshold_change,
            }
        });

        let mut plan = IdentityPlan {
            entity: spec.entity.name.clone(),
            plan_hash: String::new(),
            identity_version: spec.identity_version.clone(),
            sources,
            blocking,
            match_graph: MatchGraph { rules: match_rules, leaf_count },
            survivorship,
            decision,
            reference_identifiers,
            relations,
            exclusions,
            compliance,
            hierarchy,
            audit,
            metadata,
            governance,
        };

        plan.plan_hash = plan.compute_hash();
        Ok(plan)
    }

    /// Parse duration strings like "24h", "12h", "30m", "7d" into seconds.
    fn parse_duration(s: &str) -> u64 {
        let s = s.trim();
        if s.is_empty() { return 0; }

        let (num_str, unit) = s.split_at(s.len().saturating_sub(1));
        let num: u64 = num_str.parse().unwrap_or(0);

        match unit {
            "s" => num,
            "m" => num * 60,
            "h" => num * 3600,
            "d" => num * 86400,
            _ => s.parse().unwrap_or(0),
        }
    }

    fn infer_transformations(fields: &[String]) -> Vec<BlockingTransformation> {
        let mut transforms = Vec::new();
        for field in fields {
            let field_lower = field.to_lowercase();
            if field_lower.contains("initial") {
                transforms.push(BlockingTransformation::FirstInitial);
            } else if field_lower.contains("soundex") {
                transforms.push(BlockingTransformation::Soundex);
            } else if field_lower.contains("area_code") {
                transforms.push(BlockingTransformation::AreaCode);
            } else if field_lower.contains("last4") || field_lower.contains("last_4") {
                transforms.push(BlockingTransformation::Last4);
            } else if field_lower.contains("trigram") {
                transforms.push(BlockingTransformation::Trigrams);
            } else if field_lower == "phone"
                || field_lower.contains("phone")
                || field_lower.contains("mobile")
                || field_lower.contains("cell")
                || field_lower.contains("tel")
            {
                transforms.push(BlockingTransformation::NormalizePhone);
            } else if field_lower == "email" || field_lower.contains("email") {
                transforms.push(BlockingTransformation::NormalizeEmail);
            } else if field_lower == "name"
                || field_lower.contains("name")
            {
                transforms.push(BlockingTransformation::NormalizeName);
            } else if field_lower == "website"
                || field_lower == "domain"
                || field_lower == "homepage"
                || field_lower.contains("website")
                || field_lower.contains("domain")
                || field_lower.contains("url")
            {
                transforms.push(BlockingTransformation::NormalizeDomain);
            }
        }
        if transforms.is_empty() {
            transforms.push(BlockingTransformation::Lowercase);
        }
        transforms
    }

    fn compile_rule(rule: &RuleSpec) -> CompiledRule {
        let rule_type = match rule {
            RuleSpec::Exact {
                field,
                weight,
                options,
                ..
            } => {
                let opts = options.clone().unwrap_or_default();
                CompiledRuleType::Exact {
                    field: field.clone(),
                    weight: *weight,
                    case_insensitive: opts.case_insensitive,
                    normalize: opts.normalize,
                    normalizer: opts.normalizer.clone(),
                }
            }
            RuleSpec::Similarity {
                field,
                algorithm,
                threshold,
                weight,
                normalizer,
                ..
            } => CompiledRuleType::Similarity {
                field: field.clone(),
                algorithm: algorithm.clone(),
                threshold: *threshold,
                weight: *weight,
                normalizer: normalizer.clone(),
            },
            RuleSpec::Range {
                field,
                tolerance,
                weight,
                normalizer,
                ..
            } => CompiledRuleType::Range {
                field: field.clone(),
                tolerance: *tolerance,
                weight: *weight,
                normalizer: normalizer.clone(),
            },
            RuleSpec::Composite {
                operator, children, ..
            } => {
                let compiled_children = children
                    .iter()
                    .map(Self::compile_rule)
                    .collect();
                CompiledRuleType::Composite {
                    operator: operator.clone(),
                    children: compiled_children,
                }
            }
            RuleSpec::Ml {
                model,
                features,
                threshold,
                weight,
                ..
            } => CompiledRuleType::Ml {
                model_id: model.clone(),
                features: features.clone(),
                threshold: *threshold,
                weight: *weight,
            },
        };

        CompiledRule {
            name: Self::get_rule_name(rule),
            rule_type,
        }
    }

    fn get_rule_name(rule: &RuleSpec) -> String {
        match rule {
            RuleSpec::Exact { name, .. } => name.clone(),
            RuleSpec::Similarity { name, .. } => name.clone(),
            RuleSpec::Range { name, .. } => name.clone(),
            RuleSpec::Composite { name, .. } => name.clone(),
            RuleSpec::Ml { name, .. } => name.clone(),
        }
    }

    /// Compile a Fellegi-Sunter plan from spec-level scoring configuration.
    fn compile_fs_plan(scoring: &cannon_common::spec::ScoringSpec) -> Option<CompiledFellegiSunterPlan> {
        let fs_fields = scoring.fields.as_ref()?;
        let fs_thresholds = scoring.thresholds.as_ref()?;

        let compiled_fields: Vec<CompiledFsField> = fs_fields.iter().map(|f| {
            let m = f.m_probability;
            let u = f.u_probability;
            let w_agree = if u > 0.0 { (m / u).ln() * f.weight } else { f64::MAX * f.weight };
            let w_disagree = if (1.0 - u) > 0.0 {
                ((1.0 - m) / (1.0 - u)).ln() * f.weight
            } else {
                f64::MIN * f.weight
            };
            CompiledFsField {
                name: f.name.clone(),
                comparator: Self::map_fs_comparator(&f.comparator),
                weight: f.weight,
                m_probability: m,
                u_probability: u,
                w_agree,
                w_disagree,
            }
        }).collect();

        let max_composite: f64 = compiled_fields.iter().map(|f| f.w_agree).sum();
        let min_composite: f64 = compiled_fields.iter().map(|f| f.w_disagree).sum();

        let em_training = scoring.training.as_ref()
            .filter(|t| t.method == "em")
            .map(|t| CompiledEmTraining {
                max_iterations: t.max_iterations,
                convergence_threshold: t.convergence_threshold,
            });

        Some(CompiledFellegiSunterPlan {
            fields: compiled_fields,
            match_threshold: fs_thresholds.match_threshold,
            possible_threshold: fs_thresholds.possible,
            non_match_threshold: fs_thresholds.non_match,
            max_composite,
            min_composite,
            em_training,
        })
    }

    /// Map spec-level FsComparator to IR-level FsComparatorType.
    fn map_fs_comparator(comparator: &cannon_common::spec::FsComparator) -> FsComparatorType {
        match comparator {
            cannon_common::spec::FsComparator::Exact => FsComparatorType::Exact,
            cannon_common::spec::FsComparator::JaroWinkler => FsComparatorType::JaroWinkler,
            cannon_common::spec::FsComparator::Levenshtein => FsComparatorType::Levenshtein,
            cannon_common::spec::FsComparator::Soundex => FsComparatorType::Soundex,
            cannon_common::spec::FsComparator::Metaphone => FsComparatorType::Metaphone,
            cannon_common::spec::FsComparator::Cosine => FsComparatorType::Cosine,
        }
    }

    /// Generate implicit CompiledRule entries from FS field definitions so the
    /// existing rule evaluation pipeline produces RuleResult scores for each field.
    fn compile_fs_implicit_rules(fs_fields: &[cannon_common::spec::FsFieldSpec]) -> Vec<CompiledRule> {
        fs_fields.iter().map(|f| {
            let rule_type = match f.comparator {
                cannon_common::spec::FsComparator::Exact => {
                    CompiledRuleType::Exact {
                        field: f.name.clone(),
                        weight: f.weight,
                        case_insensitive: false,
                        normalize: false,
                        normalizer: None,
                    }
                }
                cannon_common::spec::FsComparator::JaroWinkler => {
                    CompiledRuleType::Similarity {
                        field: f.name.clone(),
                        algorithm: cannon_common::ir::SimilarityAlgorithm::JaroWinkler,
                        threshold: 0.0, // FS uses continuous scores, no hard threshold
                        weight: f.weight,
                        normalizer: None,
                    }
                }
                cannon_common::spec::FsComparator::Levenshtein => {
                    CompiledRuleType::Similarity {
                        field: f.name.clone(),
                        algorithm: cannon_common::ir::SimilarityAlgorithm::Levenshtein,
                        threshold: 0.0,
                        weight: f.weight,
                        normalizer: None,
                    }
                }
                cannon_common::spec::FsComparator::Soundex => {
                    CompiledRuleType::Similarity {
                        field: f.name.clone(),
                        algorithm: cannon_common::ir::SimilarityAlgorithm::Soundex,
                        threshold: 0.0,
                        weight: f.weight,
                        normalizer: None,
                    }
                }
                cannon_common::spec::FsComparator::Metaphone => {
                    CompiledRuleType::Similarity {
                        field: f.name.clone(),
                        algorithm: cannon_common::ir::SimilarityAlgorithm::Metaphone,
                        threshold: 0.0,
                        weight: f.weight,
                        normalizer: None,
                    }
                }
                cannon_common::spec::FsComparator::Cosine => {
                    CompiledRuleType::Similarity {
                        field: f.name.clone(),
                        algorithm: cannon_common::ir::SimilarityAlgorithm::Cosine,
                        threshold: 0.0,
                        weight: f.weight,
                        normalizer: None,
                    }
                }
            };
            CompiledRule {
                name: f.name.clone(),
                rule_type,
            }
        }).collect()
    }

    fn count_leaves(rule: &CompiledRule) -> usize {
        match &rule.rule_type {
            CompiledRuleType::Composite { children, .. } => {
                children.iter().map(Self::count_leaves).sum()
            }
            _ => 1,
        }
    }

    pub fn summarize(spec: &IdentitySpec, plan: &IdentityPlan) -> PlanSummary {
        let sources = SourcesSummary {
            count: spec.sources.len(),
            list: spec.sources.iter().map(|s| SourceDetail {
                name: s.name.clone(),
                table: s.resolved_location(),
                key: s.resolved_primary_key(),
                fields: s.attributes.keys().cloned().collect(),
            }).collect(),
        };

        let mut by_type: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
        let mut rules_detail = Vec::new();
        for rule in &plan.match_graph.rules {
            Self::collect_rule_details(rule, &mut by_type, &mut rules_detail);
        }
        // Build FS probability model summary if present
        let probability_model = plan.decision.fellegi_sunter.as_ref().map(|fs| {
            let max_ll = fs.max_composite;
            let field_weights: Vec<FsFieldWeight> = fs.fields.iter().map(|f| {
                FsFieldWeight {
                    name: f.name.clone(),
                    w_agree: f.w_agree,
                    w_disagree: f.w_disagree,
                    dominance_pct: if max_ll.abs() > 1e-10 { (f.w_agree / max_ll) * 100.0 } else { 0.0 },
                }
            }).collect();

            FsPlanSummary {
                fields_configured: fs.fields.len(),
                independence_assumption: true,
                max_possible_log_likelihood: fs.max_composite,
                min_possible_log_likelihood: fs.min_composite,
                match_threshold: fs.match_threshold,
                possible_threshold: fs.possible_threshold,
                non_match_threshold: fs.non_match_threshold,
                field_weights,
            }
        });

        let matching = MatchingSummary {
            rules: RulesSummary {
                total: rules_detail.len(),
                by_type,
            },
            rules_detail,
            probability_model,
        };

        let blocking_enabled = spec.blocking.is_some() && !plan.blocking.keys.is_empty();
        let blocking = BlockingSummary {
            enabled: blocking_enabled,
            strategy: format!("{:?}", plan.blocking.strategy).to_lowercase(),
            keys: plan.blocking.keys.iter().map(|k| k.fields.clone()).collect(),
            estimated_reduction_factor: if blocking_enabled { 0.9 } else { 0.0 },
        };

        let mut survivorship_fields = Vec::new();
        survivorship_fields.push(SurvivorshipFieldSummary {
            field: "*".to_string(),
            strategy: format!("{:?}", plan.survivorship.default_strategy).to_lowercase(),
            source_priority: None,
        });
        for (field, policy) in &plan.survivorship.field_policies {
            survivorship_fields.push(SurvivorshipFieldSummary {
                field: field.clone(),
                strategy: format!("{:?}", policy.strategy).to_lowercase(),
                source_priority: policy.source_priority.clone(),
            });
        }

        let mut stages = vec!["ingest".to_string(), "block".to_string(), "match".to_string(), "score".to_string(), "resolve".to_string()];
        if !plan.survivorship.field_policies.is_empty() || plan.survivorship.default_strategy != cannon_common::ir::SurvivorshipStrategy::MostRecent {
            stages.push("survivorship".to_string());
        }
        let execution_graph = ExecutionGraph {
            dag_depth: stages.len(),
            parallelizable_stages: vec!["block".to_string(), "match".to_string()],
            stages,
        };

        let scale_estimate = if blocking_enabled {
            ScaleEstimate {
                estimated_comparisons: "depends on blocking distribution".to_string(),
                complexity_class: "O(n log n)".to_string(),
            }
        } else {
            ScaleEstimate {
                estimated_comparisons: "O(n²) — no blocking".to_string(),
                complexity_class: "O(n²)".to_string(),
            }
        };

        let mut flags = Vec::new();
        let mut risk_score: f64 = 0.0;

        if !blocking_enabled {
            flags.push(RiskFlag {
                code: "no_blocking".to_string(),
                severity: "high".to_string(),
                description: "No blocking strategy configured — all-pairs comparison is O(n²)".to_string(),
            });
            risk_score += 0.4;
        }

        let has_fuzzy = matching.rules_detail.iter().any(|r| r.rule_type == "similarity");
        if has_fuzzy && matching.rules_detail.iter().all(|r| r.rule_type == "similarity") {
            flags.push(RiskFlag {
                code: "all_fuzzy".to_string(),
                severity: "medium".to_string(),
                description: "All matching rules are fuzzy — results may be non-deterministic".to_string(),
            });
            risk_score += 0.2;
        }

        if spec.sources.len() == 1 {
            flags.push(RiskFlag {
                code: "single_source".to_string(),
                severity: "low".to_string(),
                description: "Only one data source — deduplication within a single system".to_string(),
            });
            risk_score += 0.05;
        }

        if plan.decision.match_threshold < 0.5 {
            flags.push(RiskFlag {
                code: "low_threshold".to_string(),
                severity: "high".to_string(),
                description: format!("Match threshold {:.2} is below 0.50 — high risk of over-merging", plan.decision.match_threshold),
            });
            risk_score += 0.3;
        }

        if plan.match_graph.rules.is_empty() && plan.decision.fellegi_sunter.is_none() {
            flags.push(RiskFlag {
                code: "no_rules".to_string(),
                severity: "critical".to_string(),
                description: "No matching rules defined — reconciliation will produce no matches".to_string(),
            });
            risk_score = 1.0;
        }

        // Fellegi-Sunter-specific risk flags (A5)
        if let Some(ref fs) = plan.decision.fellegi_sunter {
            // fs_field_dominance: single field contributes > 50% of max composite
            for field in &fs.fields {
                if fs.max_composite.abs() > 1e-10 {
                    let pct = (field.w_agree / fs.max_composite) * 100.0;
                    if pct > 50.0 {
                        flags.push(RiskFlag {
                            code: "fs_field_dominance".to_string(),
                            severity: "medium".to_string(),
                            description: format!(
                                "Field '{}' contributes {:.0}% of max composite score — single field dominates matching",
                                field.name, pct
                            ),
                        });
                        risk_score += 0.15;
                    }
                }
            }

            // fs_narrow_threshold_gap: match - possible < 1.0
            if (fs.match_threshold - fs.possible_threshold).abs() < 1.0 {
                flags.push(RiskFlag {
                    code: "fs_narrow_threshold_gap".to_string(),
                    severity: "high".to_string(),
                    description: format!(
                        "Match threshold ({:.2}) and possible threshold ({:.2}) are < 1.0 apart — unstable classification",
                        fs.match_threshold, fs.possible_threshold
                    ),
                });
                risk_score += 0.3;
            }

            // fs_threshold_outside_range: thresholds unreachable
            if fs.match_threshold > fs.max_composite {
                flags.push(RiskFlag {
                    code: "fs_threshold_outside_range".to_string(),
                    severity: "critical".to_string(),
                    description: format!(
                        "Match threshold ({:.2}) exceeds maximum possible composite ({:.2}) — no pairs can ever match",
                        fs.match_threshold, fs.max_composite
                    ),
                });
                risk_score = 1.0;
            }
            if fs.non_match_threshold < fs.min_composite {
                flags.push(RiskFlag {
                    code: "fs_threshold_outside_range".to_string(),
                    severity: "critical".to_string(),
                    description: format!(
                        "Non-match threshold ({:.2}) is below minimum possible composite ({:.2}) — no pairs can be rejected",
                        fs.non_match_threshold, fs.min_composite
                    ),
                });
                risk_score += 0.3;
            }

            // fs_m_near_u: weak discriminator
            for field in &fs.fields {
                if (field.m_probability - field.u_probability).abs() < 0.1 {
                    flags.push(RiskFlag {
                        code: "fs_m_near_u".to_string(),
                        severity: "medium".to_string(),
                        description: format!(
                            "Field '{}' has m={:.3} and u={:.3} — weak discriminator (difference < 0.1)",
                            field.name, field.m_probability, field.u_probability
                        ),
                    });
                    risk_score += 0.1;
                }
            }

            // fs_extreme_probability: overconfident parameters
            for field in &fs.fields {
                if field.m_probability > 0.999 || field.u_probability < 0.0001 {
                    flags.push(RiskFlag {
                        code: "fs_extreme_probability".to_string(),
                        severity: "low".to_string(),
                        description: format!(
                            "Field '{}' has extreme probability (m={:.4}, u={:.4}) — may be overconfident",
                            field.name, field.m_probability, field.u_probability
                        ),
                    });
                    risk_score += 0.05;
                }
            }
        }

        let risk_analysis = RiskAnalysis {
            flags,
            overall_risk_score: risk_score.min(1.0),
        };

        PlanSummary {
            plan_hash: plan.plan_hash.clone(),
            api_version: spec.api_version.clone(),
            entity: plan.entity.clone(),
            spec_version: plan.identity_version.clone(),
            sources,
            matching,
            blocking,
            survivorship: survivorship_fields,
            execution_graph,
            scale_estimate,
            risk_analysis,
        }
    }

    fn collect_rule_details(
        rule: &CompiledRule,
        by_type: &mut std::collections::HashMap<String, usize>,
        details: &mut Vec<RuleDetail>,
    ) {
        match &rule.rule_type {
            CompiledRuleType::Exact { field, weight, .. } => {
                *by_type.entry("exact".to_string()).or_insert(0) += 1;
                details.push(RuleDetail {
                    name: rule.name.clone(),
                    rule_type: "exact".to_string(),
                    field: Some(field.clone()),
                    algorithm: None,
                    threshold: None,
                    weight: Some(*weight),
                });
            }
            CompiledRuleType::Similarity { field, algorithm, threshold, weight, .. } => {
                *by_type.entry("similarity".to_string()).or_insert(0) += 1;
                details.push(RuleDetail {
                    name: rule.name.clone(),
                    rule_type: "similarity".to_string(),
                    field: Some(field.clone()),
                    algorithm: Some(format!("{:?}", algorithm).to_lowercase()),
                    threshold: Some(*threshold),
                    weight: Some(*weight),
                });
            }
            CompiledRuleType::Range { field, tolerance, weight, .. } => {
                *by_type.entry("range".to_string()).or_insert(0) += 1;
                details.push(RuleDetail {
                    name: rule.name.clone(),
                    rule_type: "range".to_string(),
                    field: Some(field.clone()),
                    algorithm: None,
                    threshold: Some(*tolerance),
                    weight: Some(*weight),
                });
            }
            CompiledRuleType::Composite { children, .. } => {
                *by_type.entry("composite".to_string()).or_insert(0) += 1;
                details.push(RuleDetail {
                    name: rule.name.clone(),
                    rule_type: "composite".to_string(),
                    field: None,
                    algorithm: None,
                    threshold: None,
                    weight: None,
                });
                for child in children {
                    Self::collect_rule_details(child, by_type, details);
                }
            }
            CompiledRuleType::Ml { model_id, threshold, weight, .. } => {
                *by_type.entry("ml".to_string()).or_insert(0) += 1;
                details.push(RuleDetail {
                    name: rule.name.clone(),
                    rule_type: "ml".to_string(),
                    field: None,
                    algorithm: Some(model_id.clone()),
                    threshold: Some(*threshold),
                    weight: Some(*weight),
                });
            }
        }
    }

    pub fn estimate_impact(plan: &IdentityPlan, entities: &[NormalizedEntity]) -> ImpactAnalysis {
        use std::collections::HashMap;
        let engine = super::ReconciliationEngine::from_plan(plan);

        let mut block_counts = HashMap::new();
        for entity in entities {
            let keys = engine.generate_block_keys(entity);
            for key in keys {
                *block_counts.entry(key).or_insert(0) += 1;
            }
        }

        let distinct_blocks = block_counts.len();
        let max_block_size = block_counts.values().cloned().max().unwrap_or(0);
        let mut potential_matches = 0;
        for count in block_counts.values() {
            if *count > 1 {
                potential_matches += (count * (count - 1)) / 2;
            }
        }

        let entities_with_blocks = entities.iter()
            .filter(|e| !engine.generate_block_keys(e).is_empty())
            .count();

        ImpactAnalysis {
            total_entities: entities.len(),
            distinct_blocks,
            max_block_size,
            potential_matches,
            coverage_percent: if entities.is_empty() { 0.0 } else { (entities_with_blocks as f64 / entities.len() as f64) * 100.0 },
        }
    }
}
