use super::{
    DecisionOrigin, DecisionOutcomeKind, FulfillmentDecisionPath, ReplayClassificationSource,
};
use serde::{Deserialize, Serialize};

pub const DECISION_CONSUMER_CONTRACT_VERSION_V1: &str = "wave41_v1";

const REQUIRED_CONSUMER_FIELDS_V1: &[&str] = &[
    "decision",
    "reason_code",
    "decision_outcome_kind",
    "decision_origin",
    "fulfillment_decision_path",
    "decision_basis_version",
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsumerReadPath {
    ConvergedDecision,
    CompatibilityMarkers,
    LegacyDecision,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsumerPayloadState {
    Converged,
    CompatibilityFallback,
    LegacyBase,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerContractProjection {
    pub read_path: ConsumerReadPath,
    pub fallback_applied: bool,
    pub payload_state: ConsumerPayloadState,
    pub required_consumer_fields: Vec<String>,
}

pub fn project_consumer_contract(
    decision_outcome_kind: Option<DecisionOutcomeKind>,
    decision_origin: Option<DecisionOrigin>,
    fulfillment_decision_path: Option<FulfillmentDecisionPath>,
    decision_basis_version: Option<&str>,
    compat_fallback_applied: Option<bool>,
    classification_source: Option<ReplayClassificationSource>,
    legacy_shape_detected: Option<bool>,
) -> ConsumerContractProjection {
    let converged_present = decision_outcome_kind.is_some()
        && decision_origin.is_some()
        && fulfillment_decision_path.is_some();
    let compatibility_present = decision_basis_version.is_some()
        || compat_fallback_applied.is_some()
        || classification_source.is_some()
        || legacy_shape_detected.is_some();

    let read_path = if converged_present {
        ConsumerReadPath::ConvergedDecision
    } else if compatibility_present {
        ConsumerReadPath::CompatibilityMarkers
    } else {
        ConsumerReadPath::LegacyDecision
    };

    let fallback_applied = read_path != ConsumerReadPath::ConvergedDecision;

    let payload_state = match read_path {
        ConsumerReadPath::ConvergedDecision => {
            if compat_fallback_applied.unwrap_or(false) || legacy_shape_detected.unwrap_or(false) {
                ConsumerPayloadState::CompatibilityFallback
            } else {
                ConsumerPayloadState::Converged
            }
        }
        ConsumerReadPath::CompatibilityMarkers => ConsumerPayloadState::CompatibilityFallback,
        ConsumerReadPath::LegacyDecision => ConsumerPayloadState::LegacyBase,
    };

    ConsumerContractProjection {
        read_path,
        fallback_applied,
        payload_state,
        required_consumer_fields: required_consumer_fields_v1(),
    }
}

pub fn required_consumer_fields_v1() -> Vec<String> {
    REQUIRED_CONSUMER_FIELDS_V1
        .iter()
        .map(|field| (*field).to_string())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prefers_converged_decision_fields() {
        let projection = project_consumer_contract(
            Some(DecisionOutcomeKind::ObligationApplied),
            Some(DecisionOrigin::ObligationExecutor),
            Some(FulfillmentDecisionPath::PolicyAllow),
            Some("wave39_v1"),
            Some(false),
            Some(ReplayClassificationSource::ConvergedOutcome),
            Some(false),
        );

        assert_eq!(projection.read_path, ConsumerReadPath::ConvergedDecision);
        assert!(!projection.fallback_applied);
        assert_eq!(projection.payload_state, ConsumerPayloadState::Converged);
        assert_eq!(
            projection.required_consumer_fields,
            required_consumer_fields_v1()
        );
    }

    #[test]
    fn falls_back_to_compatibility_markers() {
        let projection = project_consumer_contract(
            None,
            None,
            None,
            Some("wave39_v1"),
            Some(true),
            Some(ReplayClassificationSource::FulfillmentPath),
            Some(true),
        );

        assert_eq!(projection.read_path, ConsumerReadPath::CompatibilityMarkers);
        assert!(projection.fallback_applied);
        assert_eq!(
            projection.payload_state,
            ConsumerPayloadState::CompatibilityFallback
        );
    }

    #[test]
    fn falls_back_to_legacy_decision_when_no_markers_exist() {
        let projection = project_consumer_contract(None, None, None, None, None, None, None);

        assert_eq!(projection.read_path, ConsumerReadPath::LegacyDecision);
        assert!(projection.fallback_applied);
        assert_eq!(projection.payload_state, ConsumerPayloadState::LegacyBase);
    }

    #[test]
    fn partial_markers_still_count_as_consumer_fallback() {
        let projection = project_consumer_contract(
            Some(DecisionOutcomeKind::ObligationApplied),
            None,
            None,
            Some("wave39_v1"),
            Some(false),
            Some(ReplayClassificationSource::ConvergedOutcome),
            Some(false),
        );

        assert_eq!(projection.read_path, ConsumerReadPath::CompatibilityMarkers);
        assert!(projection.fallback_applied);
        assert_eq!(
            projection.payload_state,
            ConsumerPayloadState::CompatibilityFallback
        );
    }
}
