use serde::{Deserialize, Serialize};

// ── Core (DAG)
// ────────────────────────────────────────────────────────────────

/// Node polarity in the ontology DAG.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Pole {
    Yang,
    Yin,
    Spiral,
}

/// Abstraction level of a node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AbstractionLevel {
    Axiom,
    Tension,
    Practice,
    Project,
    Moment,
}

/// Edge type between nodes. Reconciled 2026-07-09 (G-2) to the vocabulary
/// actually used in core.ncl, mirroring
/// `code/ontology/schemas/core.ncl::edge_type`; the prior enum listed four
/// unused kinds and omitted four in active use.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeType {
    Complements,
    Contains,
    Contradicts,
    DependsOn,
    Implies,
    ManifestsIn,
    Resolves,
    SpiralsWith,
    TensionWith,
}

/// Edge weight — qualitative salience, mirroring the schema's symbolic
/// `weight_type` ('High/'Medium/'Low). Reconciled 2026-07-09 (G-2) from `f64`,
/// which never matched the symbolic values the data has always carried.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Weight {
    High,
    Medium,
    Low,
}

/// An ontology node (axiom, tension, practice, etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
    pub id: String,
    pub name: String,
    pub pole: Pole,
    pub level: AbstractionLevel,
    pub description: String,
    #[serde(default)]
    pub invariant: bool,
    #[serde(default)]
    pub artifact_paths: Vec<String>,
    #[serde(default)]
    pub adrs: Vec<String>,
}

/// A directed edge between two nodes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
    pub from: String,
    pub to: String,
    pub kind: EdgeType,
    #[serde(default = "Edge::default_weight")]
    pub weight: Weight,
    #[serde(default)]
    pub note: String,
}

impl Edge {
    fn default_weight() -> Weight {
        Weight::Medium
    }
}

/// Deserialization root for core.ncl exports.
#[derive(Debug, Deserialize)]
pub struct CoreConfig {
    pub nodes: Vec<Node>,
    pub edges: Vec<Edge>,
}

// ── State (FSM)
// ───────────────────────────────────────────────────────────────

/// Tension level of a state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TensionLevel {
    High,
    Medium,
    Low,
    Ignored,
}

/// Time horizon for a dimension or transition.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Horizon {
    Weeks,
    Months,
    Years,
    Continuous,
}

/// A discrete state within a dimension.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DimensionState {
    pub id: String,
    pub name: String,
    pub description: String,
    pub tension: TensionLevel,
}

/// A valid transition between two states.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transition {
    pub from: String,
    pub to: String,
    pub condition: String,
    pub catalyst: String,
    pub blocker: String,
    pub horizon: Horizon,
}

/// Coupling relationship between dimensions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Coupling {
    pub origin: String,
    pub destination: String,
    pub kind: String,
    pub note: String,
}

/// A tracked dimension of the project's state (FSM).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dimension {
    pub id: String,
    pub name: String,
    pub description: String,
    pub current_state: String,
    pub desired_state: String,
    pub horizon: Horizon,
    pub states: Vec<DimensionState>,
    pub transitions: Vec<Transition>,
    #[serde(default)]
    pub coupled_with: Vec<String>,
}

/// Deserialization root for state.ncl exports.
#[derive(Debug, Deserialize)]
pub struct StateConfig {
    pub dimensions: Vec<Dimension>,
}

// ── Gate (membranes)
// ──────────────────────────────────────────────────────────

/// Membrane permeability level.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Permeability {
    High,
    Medium,
    Low,
    Closed,
}

/// Signal processing protocol.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Protocol {
    Observe,
    Absorb,
    Challenge,
    Reject,
}

/// Maximum duration a membrane can remain open.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Duration {
    Moment,
    Days,
    Weeks,
    Indefinite,
}

/// Known signal types that membranes can accept.
/// Uses `#[serde(other)]` so project-specific extensions deserialize as
/// `Unknown`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SignalType {
    FrameBreakingQuestion,
    UsageFriction,
    ProductiveMisunderstanding,
    HardBug,
    ContextlessInsight,
    SignificantSilence,
    ConnectsToPractice,
    EcosystemRelevance,
    DepthDemonstrated,
    IdentityReinforcement,
    OpportunityAlignment,
    #[serde(other)]
    Unknown,
}

/// Conditions under which a membrane opens.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpeningCondition {
    pub max_tension_dimensions: u32,
    pub pending_transitions: u32,
    pub core_stable: bool,
    pub description: String,
}

/// A gate membrane — filters incoming signals to protect ontology invariants.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Membrane {
    pub id: String,
    pub name: String,
    pub description: String,
    pub permeability: Permeability,
    pub accepts: Vec<SignalType>,
    pub protects: Vec<String>,
    pub opening_condition: OpeningCondition,
    pub closing_condition: String,
    pub protocol: Protocol,
    pub max_duration: Duration,
    #[serde(default)]
    pub active: bool,
}

/// Deserialization root for gate.ncl exports.
#[derive(Debug, Deserialize)]
pub struct GateConfig {
    pub membranes: Vec<Membrane>,
}

// ── Positioning (marketing as queryable protocol surface — ADR-035)
// ────────

/// Lifecycle status of a positioning artefact.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PositioningStatus {
    Draft,
    Validated,
    Live,
    Retired,
}

/// Difusión mechanism kind — the CÓMO axis (ADR-043). Persistent surfaces
/// (Web, DocsSite, McpServer, Repo) and event vehicles (Content, Talk, Event).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MechanismKind {
    Web,
    DocsSite,
    McpServer,
    Repo,
    Content,
    Talk,
    Event,
}

/// Proof kind — how a success case is grounded (ADR-043).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProofKind {
    Dogfood,
    PersonalInstance,
    ReferenceAdopter,
    Benchmark,
}

/// Audience validation state (ADR-043) — Hypothesis until a Proof references
/// it.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AudienceStatus {
    #[default]
    Hypothesis,
    Validated,
}

/// Launch-readiness check declared on a value-prop or campaign.
///
/// Structurally identical to the ADR `ConstraintCheck` discriminated record;
/// the two will be unified by a future `ontology/schemas/check.ncl` lift-out
/// (ADR-035 soft constraint `shared-check-lift-out-pending`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "tag")]
pub enum LaunchCheck {
    Grep {
        pattern: String,
        paths: Vec<String>,
        must_be_empty: bool,
    },
    NuCmd {
        cmd: String,
        expect_exit: i64,
    },
    ApiCall {
        endpoint: String,
        json_path: String,
        expected: serde_json::Value,
    },
    FileExists {
        path: String,
        present: bool,
    },
}

/// Audience segment — one file per audience under
/// `.ontoref/positioning/audiences/`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Audience {
    pub id: String,
    pub name: String,
    pub description: String,
    pub primary_pain: String,
    #[serde(default)]
    pub context: String,
    #[serde(default)]
    pub channels: Vec<String>,
    #[serde(default)]
    pub evidence: Vec<String>,
    #[serde(default)]
    pub linked_nodes: Vec<String>,
    #[serde(default)]
    pub status: AudienceStatus,
}

/// Value proposition — one file per claim under
/// `.ontoref/positioning/value-props/`.
///
/// `evidence_adrs` is the witness-seam (ADR-031) anchoring an act-axis
/// claim to substance-axis architectural decisions. The
/// `live_requires_evidence_adrs` Nickel contract makes the field non-empty
/// when `status = Live`; deserialization does not re-enforce it (trust the
/// source export).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValueProp {
    pub id: String,
    pub claim: String,
    pub audience_ids: Vec<String>,
    #[serde(default)]
    pub differentiator_ids: Vec<String>,
    pub problem_solved: String,
    #[serde(default)]
    pub alternatives: Vec<String>,
    pub unique_angle: String,
    #[serde(default)]
    pub evidence_adrs: Vec<String>,
    #[serde(default)]
    pub proof_points: Vec<String>,
    #[serde(default)]
    pub narrative_path: Option<String>,
    #[serde(default)]
    pub launch_criteria: Vec<LaunchCheck>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub retired_at: Option<String>,
    #[serde(default)]
    pub superseded_by: Option<String>,
}

/// GTM campaign — bundles value-props for a specific moment and channels.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Campaign {
    pub id: String,
    pub title: String,
    pub audience_ids: Vec<String>,
    pub value_prop_ids: Vec<String>,
    #[serde(default)]
    pub mechanism_ids: Vec<String>,
    pub goal: String,
    #[serde(default)]
    pub channels: Vec<String>,
    #[serde(default)]
    pub launch_criteria: Vec<LaunchCheck>,
    #[serde(default)]
    pub narrative_path: Option<String>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub starts_at: Option<String>,
    #[serde(default)]
    pub ends_at: Option<String>,
}

/// Differentiator — one file per area of differential value (the QUÉ axis,
/// ADR-043).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Differentiator {
    pub id: String,
    pub area: String,
    pub value_claim: String,
    pub vs: Vec<String>,
    #[serde(default)]
    pub competitor_ids: Vec<String>,
    #[serde(default)]
    pub linked_nodes: Vec<String>,
    #[serde(default)]
    pub evidence_adrs: Vec<String>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub retired_at: Option<String>,
}

/// Competitor — supports the QUÉ axis: what they do and how ontoref differs
/// (ADR-043).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Competitor {
    pub id: String,
    pub name: String,
    pub category: String,
    pub what_they_do: String,
    #[serde(default)]
    pub differs_by: Vec<String>,
    #[serde(default)]
    pub url: Option<String>,
    #[serde(default)]
    pub linked_nodes: Vec<String>,
}

/// Difusión mechanism — one vehicle of difusión (the CÓMO axis, ADR-043).
/// Difusión is the category; a [`Campaign`] is one time-boxed coordination
/// within it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DifusionMechanism {
    pub id: String,
    pub kind: MechanismKind,
    pub name: String,
    #[serde(default)]
    pub description: String,
    pub persistent: bool,
    #[serde(default)]
    pub channels: Vec<String>,
    #[serde(default)]
    pub value_prop_ids: Vec<String>,
    #[serde(default)]
    pub differentiator_ids: Vec<String>,
    #[serde(default)]
    pub launch_criteria: Vec<LaunchCheck>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub retired_at: Option<String>,
}

/// The three axes a [`Proof`] converges (ADR-043).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProofConverges {
    #[serde(default)]
    pub differentiator_ids: Vec<String>,
    #[serde(default)]
    pub audience_ids: Vec<String>,
    #[serde(default)]
    pub mechanism_ids: Vec<String>,
}

/// Proof — the validation seam (ADR-043). A success case is the convergence of
/// a differentiator, an audience and a difusión mechanism, signed by a real
/// outcome. It references the other three axes; nothing references it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Proof {
    pub id: String,
    pub title: String,
    pub kind: ProofKind,
    pub converges: ProofConverges,
    pub outcome: String,
    #[serde(default)]
    pub evidence: Vec<String>,
    #[serde(default)]
    pub evidence_adrs: Vec<String>,
    #[serde(default)]
    pub launch_criteria: Vec<LaunchCheck>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub retired_at: Option<String>,
}

/// Directness of a [`ViabilityPath`] (ADR-055) — maps to the two poles of the
/// `openness-vs-sustainability` tension. `Indirect` is the Yin/open pole
/// (visibility, funnel, reputation); `Direct` is the Yang/governance pole
/// (metered payment for the witness/audit surface).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Directness {
    Direct,
    Indirect,
}

/// Compensation form of a [`ViabilityPath`] (ADR-055). Descriptive vocabulary,
/// NOT a per-claim business-model decision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FlowKind {
    HostedService,
    Support,
    Consulting,
    DualLicense,
    Sponsorship,
    Product,
    Funnel,
    Reputation,
}

/// The three axes a [`ViabilityPath`] converges (ADR-055): PARA QUIÉN × CÓMO ×
/// (QUÉ×QUIÉN).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ViabilityConverges {
    #[serde(default)]
    pub audience_ids: Vec<String>,
    #[serde(default)]
    pub mechanism_ids: Vec<String>,
    #[serde(default)]
    pub value_prop_ids: Vec<String>,
}

/// ViabilityPath — the sustainability seam (ADR-055), a second costura parallel
/// to [`Proof`]. Where a Proof answers "is the claim real?", a ViabilityPath
/// answers "does the claim sustain its maker?". It converges PARA QUIÉN × CÓMO
/// × (QUÉ×QUIÉN); nothing references it. `realized_outcome` is present only
/// once a real measured compensation promotes the path past `Draft` (the
/// one-directional realized⇒outcome gate, enforced by the Nickel
/// `make_viability_path` contract).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViabilityPath {
    pub id: String,
    pub title: String,
    pub directness: Directness,
    pub flow_kind: FlowKind,
    pub converges: ViabilityConverges,
    pub hypothesis: String,
    #[serde(default)]
    pub realized_outcome: Option<String>,
    #[serde(default)]
    pub linked_nodes: Vec<String>,
    #[serde(default)]
    pub evidence_adrs: Vec<String>,
    #[serde(default)]
    pub launch_criteria: Vec<LaunchCheck>,
    pub status: PositioningStatus,
    #[serde(default)]
    pub retired_at: Option<String>,
}

/// Aggregate view of a project's positioning layer — populated by a filesystem
/// walk of `.ontoref/positioning/{audiences,value-props,campaigns,
/// differentiators,competitors,difusion,proofs,viability}/` (ADR-035 + ADR-043
/// + ADR-055).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PositioningConfig {
    #[serde(default)]
    pub audiences: Vec<Audience>,
    #[serde(default)]
    pub value_props: Vec<ValueProp>,
    #[serde(default)]
    pub campaigns: Vec<Campaign>,
    #[serde(default)]
    pub differentiators: Vec<Differentiator>,
    #[serde(default)]
    pub competitors: Vec<Competitor>,
    #[serde(default)]
    pub mechanisms: Vec<DifusionMechanism>,
    #[serde(default)]
    pub proofs: Vec<Proof>,
    #[serde(default)]
    pub viability_paths: Vec<ViabilityPath>,
}

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

    #[test]
    fn deserializes_value_prop_with_tagged_checks() {
        let json = r#"{
            "id": "vp-001-smoke",
            "claim": "Claim text",
            "audience_ids": ["audience-a"],
            "problem_solved": "Problem",
            "alternatives": [],
            "unique_angle": "Angle",
            "evidence_adrs": ["adr-035"],
            "proof_points": [],
            "launch_criteria": [
                { "tag": "FileExists", "path": "x", "present": true },
                { "tag": "Grep", "pattern": "p", "paths": ["a"], "must_be_empty": false }
            ],
            "status": "Draft"
        }"#;
        let vp: ValueProp = serde_json::from_str(json).expect("ValueProp parse");
        assert_eq!(vp.id, "vp-001-smoke");
        assert_eq!(vp.status, PositioningStatus::Draft);
        assert_eq!(vp.launch_criteria.len(), 2);
        match &vp.launch_criteria[0] {
            LaunchCheck::FileExists { path, present } => {
                assert_eq!(path, "x");
                assert!(*present);
            }
            other => panic!("expected FileExists, got {other:?}"),
        }
        match &vp.launch_criteria[1] {
            LaunchCheck::Grep {
                pattern,
                paths,
                must_be_empty,
            } => {
                assert_eq!(pattern, "p");
                assert_eq!(paths, &vec!["a".to_string()]);
                assert!(!*must_be_empty);
            }
            other => panic!("expected Grep, got {other:?}"),
        }
    }

    #[test]
    fn deserializes_audience_with_defaults() {
        let json = r#"{
            "id": "platform-engineer",
            "name": "Platform Engineer",
            "description": "desc",
            "primary_pain": "pain"
        }"#;
        let a: Audience = serde_json::from_str(json).expect("Audience parse");
        assert_eq!(a.id, "platform-engineer");
        assert!(a.context.is_empty());
        assert!(a.channels.is_empty());
        assert!(a.linked_nodes.is_empty());
    }

    #[test]
    fn round_trips_launch_check() {
        let check = LaunchCheck::NuCmd {
            cmd: "test".to_string(),
            expect_exit: 0,
        };
        let s = serde_json::to_string(&check).expect("serialize");
        assert!(s.contains("\"tag\":\"NuCmd\""));
        let back: LaunchCheck = serde_json::from_str(&s).expect("deserialize");
        match back {
            LaunchCheck::NuCmd { cmd, expect_exit } => {
                assert_eq!(cmd, "test");
                assert_eq!(expect_exit, 0);
            }
            other => panic!("expected NuCmd, got {other:?}"),
        }
    }
}
