//! Intermediate Representation (IR) for compiled Identity Plans.
//!
//! These types are the single source of truth for compiled plan structures.
//! Both cannon-core's engine and all downstream consumers import from here.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityPlan {
    pub entity: String,
    pub plan_hash: String,
    pub identity_version: String,
    pub sources: Vec<ResolvedSource>,
    pub blocking: CompiledBlocking,
    pub match_graph: MatchGraph,
    pub survivorship: SurvivorshipPolicy,
    pub decision: DecisionConfig,
    pub reference_identifiers: Vec<CompiledReferenceIdentifier>,
    pub relations: Vec<CompiledRelation>,
    #[serde(default)]
    pub exclusions: Vec<CompiledExclusion>,
    #[serde(default)]
    pub compliance: Option<CompiledCompliance>,
    #[serde(default)]
    pub hierarchy: Option<CompiledHierarchy>,
    #[serde(default)]
    pub audit: Option<CompiledAudit>,
    #[serde(default)]
    pub metadata: Option<PlanMetadata>,
    #[serde(default)]
    pub governance: Option<CompiledGovernance>,
    /// Compiled subtype inference rules for the semantic resolution layer.
    #[serde(default)]
    pub subtype_rules: Vec<CompiledSubtypeRule>,
    /// Compiled constraint matrix controlling merge vs relate vs block.
    #[serde(default)]
    pub constraint_matrix: Vec<CompiledConstraint>,
    /// Minimum confidence for subtype inference to be used in constraints.
    #[serde(default = "default_subtype_confidence_threshold")]
    pub subtype_confidence_threshold: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedSource {
    pub name: String,
    #[serde(alias = "system")]
    pub adapter: String,
    #[serde(alias = "table")]
    pub location: String,
    pub id_field: String,
    pub field_mappings: HashMap<String, String>,
    pub temporal: Option<TemporalConfig>,
    #[serde(default)]
    pub freshness: Option<CompiledFreshness>,
    #[serde(default)]
    pub schema: Option<HashMap<String, CompiledFieldSchema>>,
    #[serde(default)]
    pub sampling: Option<CompiledSampling>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub pii_fields: Vec<String>,
    /// Source reliability score (0.0 to 1.0, default 1.0)
    #[serde(default = "default_reliability")]
    pub reliability: f64,
}

fn default_reliability() -> f64 { 1.0 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalConfig {
    pub valid_from_field: String,
    pub valid_to_field: Option<String>,
    pub strategy: TemporalStrategy,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TemporalStrategy {
    #[default]
    LatestOnly,
    PointInTime,
    BiTemporal,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledBlocking {
    pub strategy: BlockingStrategy,
    pub keys: Vec<BlockingKey>,
    pub fallback_sample_rate: Option<f64>,
    pub lsh_bands: Option<usize>,
    pub lsh_rows: Option<usize>,
    pub neighborhood_window: Option<usize>,
    pub sort_fields: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BlockingStrategy {
    Single,
    #[default]
    Composite,
    Lsh,
    MlPredicted,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockingKey {
    pub fields: Vec<String>,
    pub transformations: Vec<BlockingTransformation>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockingTransformation {
    Lowercase,
    FirstInitial,
    Soundex,
    AreaCode,
    Last4,
    Trigrams,
    DoubleMetaphone,
    NormalizePhone,
    NormalizeEmail,
    NormalizeName,
    NormalizeNickname,
    NormalizeDomain,
    // Product-specific transformations
    NormalizeBarcode,
    NormalizeMpn,
    NormalizeBrand,
    /// Tokenize a product name: lowercase, split on separators, remove stopwords,
    /// sort alphabetically, join with "_". Makes "Samsung 55in QLED 4K" and
    /// "Samsung QLED 4K 55in" produce the same blocking key.
    TokenizeProduct,
    /// Encode a lat/lon pair as a geohash string for geospatial blocking.
    /// The field value must be "lat,lon" (comma-separated). Precision is 6
    /// characters (~1.2km x 0.6km cells), ensuring nearby observations share
    /// the same blocking key.
    Geohash,
    /// Snap a "lat,lon" pair to a coarse grid cell by truncating to 3 decimal
    /// places (~100m). Lightweight alternative to geohash for realtime spatial
    /// blocking. Produces keys like "51.507,-0.128".
    GridCell,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchGraph {
    pub rules: Vec<CompiledRule>,
    pub leaf_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledRule {
    pub name: String,
    pub rule_type: CompiledRuleType,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompiledRuleType {
    Exact {
        field: String,
        weight: f64,
        case_insensitive: bool,
        normalize: bool,
        #[serde(default)]
        normalizer: Option<String>,
    },
    Similarity {
        field: String,
        algorithm: SimilarityAlgorithm,
        threshold: f64,
        weight: f64,
        #[serde(default)]
        normalizer: Option<String>,
    },
    Range {
        field: String,
        tolerance: f64,
        weight: f64,
        #[serde(default)]
        normalizer: Option<String>,
    },
    Composite {
        operator: CompositeOperator,
        children: Vec<CompiledRule>,
    },
    Ml {
        model_id: String,
        features: Vec<String>,
        threshold: f64,
        weight: f64,
    },
    /// Jaccard token overlap for product names where word order varies.
    TokenOverlap {
        field: String,
        threshold: f64,
        weight: f64,
        #[serde(default)]
        normalizer: Option<String>,
    },
    /// Numeric alignment: extracts numeric-unit pairs and scores match fraction.
    /// E.g. "256GB 6.1in" vs "256GB 6.1-inch" -> 1.0
    NumericAlignment {
        field: String,
        threshold: f64,
        weight: f64,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SimilarityAlgorithm {
    JaroWinkler,
    Levenshtein,
    Soundex,
    Metaphone,
    Cosine,
    Haversine,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CompositeOperator {
    And,
    Or,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SurvivorshipPolicy {
    pub default_strategy: SurvivorshipStrategy,
    pub field_policies: HashMap<String, FieldSurvivorshipPolicy>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SurvivorshipStrategy {
    #[default]
    MostRecent,
    MostComplete,
    SourcePriority,
    Aggregate,
    Custom,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSurvivorshipPolicy {
    pub strategy: SurvivorshipStrategy,
    pub source_priority: Option<Vec<String>>,
    pub aggregate_fn: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionConfig {
    pub match_threshold: f64,
    pub review_threshold: Option<f64>,
    pub reject_threshold: Option<f64>,
    pub conflict_strategy: ConflictStrategy,
    pub review_webhook: Option<String>,
    #[serde(default)]
    pub scoring_method: ScoringMethod,
    #[serde(default)]
    pub ml_ensemble_config: Option<CompiledMlEnsembleConfig>,
    #[serde(default)]
    pub custom_scoring_config: Option<CompiledCustomScoringConfig>,
    #[serde(default)]
    pub fellegi_sunter: Option<CompiledFellegiSunterPlan>,
    /// Tie-breaking rules for deterministic ordering of equal-confidence pairs
    #[serde(default)]
    pub tie_breaking: Vec<CompiledTieBreaker>,
    /// Clustering configuration (default: simple union-find)
    #[serde(default)]
    pub clustering: Option<CompiledClusteringConfig>,
    /// Minimum total available weight to accept a match (0.0 = disabled)
    #[serde(default)]
    pub min_total_weight: f64,
    /// Rule names exempt from the min_total_weight guard
    #[serde(default)]
    pub allow_single_field: Vec<String>,
}

/// Compiled tie-breaking rule for deterministic pair ordering.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CompiledTieBreaker {
    ExactMatchPriority,
    LowestEntityId,
    EarliestTimestamp,
}

/// Compiled ML ensemble (logistic regression) scoring configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledMlEnsembleConfig {
    pub coefficients: HashMap<String, f64>,
    pub bias: f64,
}

/// Compiled custom expression-based scoring configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledCustomScoringConfig {
    pub expression: String,
}

// ============================================================================
// Fellegi-Sunter IR types
// ============================================================================

/// A single field in a compiled Fellegi-Sunter plan with precomputed log-likelihood weights.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledFsField {
    pub name: String,
    pub comparator: FsComparatorType,
    pub weight: f64,
    pub m_probability: f64,
    pub u_probability: f64,
    /// Precomputed: ln(m/u) * weight -- log-likelihood ratio when fields agree
    pub w_agree: f64,
    /// Precomputed: ln((1-m)/(1-u)) * weight -- log-likelihood ratio when fields disagree
    pub w_disagree: f64,
    /// Optional normalizer applied before comparison: "email", "phone", "name", "domain", "generic"
    #[serde(default)]
    pub normalizer: Option<String>,
}

/// Comparator type for Fellegi-Sunter fields, mapped from spec-level FsComparator.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FsComparatorType {
    Exact,
    JaroWinkler,
    Levenshtein,
    Soundex,
    Metaphone,
    Cosine,
    /// Geohash prefix comparison for geospatial proximity matching.
    /// Encodes lat/lon as geohash strings and compares prefix overlap.
    /// Longer shared prefix = closer proximity. Agreement score is
    /// shared_prefix_len / max(len_a, len_b).
    GeohashPrefix,
    /// Numeric distance comparison for continuous values (altitude, speed, heading).
    /// Parses both values as f64 and returns 1.0 - (|a - b| / max(|a|, |b|, 1.0)),
    /// clamped to [0.0, 1.0]. Identical values score 1.0, values far apart score 0.0.
    NumericDistance,
    /// Cosine similarity on numeric embedding vectors.
    /// Both values must be comma-separated floats (e.g. "0.12,0.45,-0.03,...").
    /// Returns the cosine similarity in [0.0, 1.0] (clamped, since embeddings
    /// can have negative components). This is the strongest single signal for
    /// re-identification across locations -- a 512-d OSNet embedding compared
    /// via cosine distance has a 42x match/non-match discriminative ratio.
    EmbeddingCosine,
    /// Speed-constrained plausibility check for moving-target re-identification.
    /// Both values must be "lat,lon,epoch_secs" triples. Computes haversine
    /// distance and elapsed time, then rejects (returns 0.0) if the implied
    /// speed exceeds a class-dependent ceiling (default 55 m/s / 200 km/h).
    /// Returns 1.0 - (speed / max_speed) when plausible, giving higher scores
    /// to slower (more likely) transitions.
    SpeedConstraint,
}

/// A compiled Fellegi-Sunter probabilistic matching plan.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledFellegiSunterPlan {
    pub fields: Vec<CompiledFsField>,
    pub match_threshold: f64,
    pub possible_threshold: f64,
    pub non_match_threshold: f64,
    /// Minimum score to merge two existing entities during real-time resolve.
    /// Higher than match_threshold because merging is destructive and hard to undo.
    /// Defaults to match_threshold * 1.5 if not set.
    #[serde(default)]
    pub merge_threshold: Option<f64>,
    /// Theoretical maximum composite score: sum of all w_agree
    pub max_composite: f64,
    /// Theoretical minimum composite score: sum of all w_disagree
    pub min_composite: f64,
    /// Whether m/u should be estimated via EM before scoring
    #[serde(default)]
    pub em_training: Option<CompiledEmTraining>,
}

/// EM training configuration compiled from spec.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledEmTraining {
    pub max_iterations: usize,
    pub convergence_threshold: f64,
    /// "random_sampling" to estimate u from random pairs before EM
    #[serde(default)]
    pub estimate_u: Option<String>,
    /// Maximum random pairs for u estimation
    #[serde(default = "default_max_random_pairs")]
    pub max_random_pairs: usize,
    /// Weight given to spec priors when blending with EM estimates (0.0-1.0).
    /// 0.0 = fully data-driven, 1.0 = ignore EM and keep spec priors.
    #[serde(default = "default_prior_weight")]
    pub prior_weight: f64,
}

fn default_max_random_pairs() -> usize { 1_000_000 }
fn default_prior_weight() -> f64 { 0.7 }

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ConflictStrategy {
    #[default]
    PreferHighConfidence,
    PreferRecent,
    ManualReview,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ScoringMethod {
    #[default]
    WeightedSum,
    MlEnsemble,
    Custom,
    FellegiSunter,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledReferenceIdentifier {
    pub name: String,
    pub id_type: ReferenceIdType,
    pub authority: Option<String>,
    pub field: String,
    pub match_weight: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceIdType {
    External,
    Internal,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledRelation {
    pub name: String,
    pub from_entity: String,
    pub to_entity: String,
    pub join_key: String,
    pub cardinality: Cardinality,
    pub propagate_match: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Cardinality {
    OneToOne,
    OneToMany,
    ManyToOne,
    ManyToMany,
}

// ============================================================================
// Enterprise IR types
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompiledExclusion {
    FieldValue { field: String, operator: String, value: serde_json::Value },
    ExplicitPair { left_id: String, right_id: String },
    CrossTenant { enabled: bool },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledCompliance {
    pub frameworks: Vec<String>,
    pub pii_fields: Vec<String>,
    pub audit_required: bool,
    pub retention_days: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledFreshness {
    pub max_age_seconds: u64,
    pub warn_after_seconds: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledFieldSchema {
    pub field_type: String,
    pub pii: bool,
    pub nullable: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledSampling {
    pub strategy: String,
    pub rate: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledGovernance {
    pub require_freshness: bool,
    pub require_schema: bool,
    pub required_tags: Vec<String>,
    #[serde(default)]
    pub log_probabilities: bool,
    #[serde(default)]
    pub require_shadow_on_threshold_change: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledHierarchy {
    pub parent_field: String,
    pub max_depth: u8,
    pub inherited_fields: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledAudit {
    pub log_all_comparisons: bool,
    pub log_decisions: bool,
    pub log_conflicts: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct PlanMetadata {
    pub name: Option<String>,
    pub description: Option<String>,
    pub owner: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
}

// ============================================================================
// Clustering IR types
// ============================================================================

/// Compiled clustering configuration for controlling how merge decisions form clusters.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledClusteringConfig {
    /// Clustering method: Simple (union-find) or Graph (coherence-aware)
    #[serde(default)]
    pub method: ClusteringMethod,
    /// Edges with confidence below this are dropped before clustering (default 0.0)
    #[serde(default)]
    pub min_edge_weight: f64,
    /// Clusters with avg internal edge weight below this are split (default 0.0)
    #[serde(default)]
    pub min_cluster_coherence: f64,
    /// Force coherence check on clusters larger than this (default usize::MAX)
    #[serde(default = "default_max_cluster_size")]
    pub max_cluster_size: usize,
}

fn default_max_cluster_size() -> usize { usize::MAX }

impl Default for CompiledClusteringConfig {
    fn default() -> Self {
        Self {
            method: ClusteringMethod::Simple,
            min_edge_weight: 0.0,
            min_cluster_coherence: 0.0,
            max_cluster_size: usize::MAX,
        }
    }
}

/// Clustering method for grouping merge decisions into entity clusters.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ClusteringMethod {
    /// Simple transitive closure via union-find (existing behavior)
    #[default]
    Simple,
    /// Graph-based clustering with coherence validation
    Graph,
}

// ============================================================================
// Active Learning / Feedback IR types
// ============================================================================

/// A user-provided label for a pair of records (active learning feedback).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackLabel {
    /// External ID of the first record
    pub entity_a_id: String,
    /// External ID of the second record
    pub entity_b_id: String,
    /// Source name of the first record
    pub source_a: String,
    /// Source name of the second record
    pub source_b: String,
    /// User's label: match or no_match
    pub label: FeedbackDecision,
}

/// User decision for a feedback label.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FeedbackDecision {
    Match,
    NoMatch,
}

fn default_subtype_confidence_threshold() -> f64 { 0.7 }

// ============================================================================
// Semantic Resolution IR types (typed resolution graph)
// ============================================================================

/// A compiled subtype inference rule. The engine evaluates these in priority
/// order (highest first) and the first matching rule determines the subtype.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledSubtypeRule {
    pub subtype: String,
    pub conditions: Vec<SubtypeCondition>,
    pub priority: u16,
}

/// A single condition in a subtype inference rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtypeCondition {
    pub field: String,
    pub operator: SubtypeOperator,
    pub value: Option<String>,
}

/// Operator for subtype condition evaluation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SubtypeOperator {
    /// Field exists and is non-empty
    Present,
    /// Field value equals the specified value (case-insensitive)
    Equals,
    /// Field value contains the specified substring (case-insensitive)
    Contains,
    /// Field value matches the specified regex
    Matches,
}

/// A compiled constraint entry in the constraint matrix.
/// Defines what happens when two entities of given subtypes are paired.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledConstraint {
    pub subtype_a: String,
    pub subtype_b: String,
    pub action: ConstraintAction,
}

/// The action to take when two entities of specific subtypes are paired.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ConstraintAction {
    /// Allow merging (standard behavior)
    MergeAllowed,
    /// Create a typed relationship instead of merging
    Relate { link_type: String, direction: RelateDirection },
    /// Block - produce NoMatch regardless of score
    Block,
}

/// Direction of a Relate constraint.
///
/// The direction indicates which entity's parent_entity_id gets set.
/// "XToY" means "X points to Y as its parent" (X is the child, Y is the parent).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RelateDirection {
    /// A's parent is B (A is child of B)
    AToB,
    /// B's parent is A (B is child of A)
    BToA,
    /// Undirected relationship
    Symmetric,
}

impl IdentityPlan {
    pub fn compute_hash(&self) -> String {
        let json = serde_json::to_string(self).expect("Failed to serialize plan");
        let mut hasher = Sha256::new();
        hasher.update(json.as_bytes());
        let result = hasher.finalize();
        hex::encode(result)
    }
}
