use nana_domain::{
    CheckRecord, ItemAcquisition, PromiseStatus, RelationshipAxes, RelationshipDimension,
    RuntimeState, StateDelta, StateOp,
};
use thiserror::Error;

const MAX_RELATIONSHIP_ADJUSTMENT: u16 = 8;
const MIN_RELATIONSHIP_VALUE: i16 = 0;
const MAX_RELATIONSHIP_VALUE: i16 = 100;

#[derive(Debug, Error, PartialEq, Eq)]
pub enum ReduceError {
    #[error("relationship adjustment {delta} exceeds the per-operation limit of 8")]
    RelationshipAdjustmentOutOfRange { delta: i16 },

    #[error("promise id already exists: {id}")]
    DuplicatePromiseId { id: String },

    #[error("promise must be created as proposed or accepted: {id}")]
    InvalidInitialPromiseStatus { id: String },

    #[error("promise not found: {id}")]
    PromiseNotFound { id: String },

    #[error("invalid promise transition for {id}: {from:?} -> {to:?}")]
    InvalidPromiseTransition {
        id: String,
        from: PromiseStatus,
        to: PromiseStatus,
    },

    #[error("knowledge id already exists: {id}")]
    DuplicateKnowledgeId { id: String },

    #[error("item id already exists: {id}")]
    DuplicateItemId { id: String },

    #[error("item not found: {id}")]
    ItemNotFound { id: String },

    #[error("item {id} is held by {actual}, not {expected}")]
    ItemHolderMismatch {
        id: String,
        expected: String,
        actual: String,
    },

    #[error("check id already exists: {id}")]
    DuplicateCheckId { id: String },

    #[error("action id already has a different check record: {action_id}")]
    CheckActionConflict { action_id: String },

    #[error("clock not found: {id}")]
    ClockNotFound { id: String },
}

/// Applies a complete delta to a cloned state.
///
/// The input is never mutated. If any operation fails, the partially reduced clone
/// is discarded so callers observe all-or-nothing behavior.
pub fn apply_delta(state: &RuntimeState, delta: &StateDelta) -> Result<RuntimeState, ReduceError> {
    let mut next = state.clone();
    for op in &delta.ops {
        apply_op(&mut next, op)?;
    }
    Ok(next)
}

fn apply_op(state: &mut RuntimeState, op: &StateOp) -> Result<(), ReduceError> {
    match op {
        StateOp::SetWorldFlag { key, value } => {
            state.world_flags.insert(key.clone(), *value);
        }
        StateOp::AdjustRelationship {
            from,
            to,
            adjustment,
        } => {
            if adjustment.delta.unsigned_abs() > MAX_RELATIONSHIP_ADJUSTMENT {
                return Err(ReduceError::RelationshipAdjustmentOutOfRange {
                    delta: adjustment.delta,
                });
            }

            let axes = state
                .relationships
                .entry(relationship_key(from, to))
                .or_insert_with(RelationshipAxes::neutral);
            let value = relationship_dimension_mut(axes, adjustment.dimension);
            *value = value
                .saturating_add(adjustment.delta)
                .clamp(MIN_RELATIONSHIP_VALUE, MAX_RELATIONSHIP_VALUE);
        }
        StateOp::CreatePromise { promise } => {
            if state.promises.iter().any(|item| item.id == promise.id) {
                return Err(ReduceError::DuplicatePromiseId {
                    id: promise.id.clone(),
                });
            }
            if !matches!(
                promise.status,
                PromiseStatus::Proposed | PromiseStatus::Accepted
            ) {
                return Err(ReduceError::InvalidInitialPromiseStatus {
                    id: promise.id.clone(),
                });
            }
            state.promises.push(promise.clone());
        }
        StateOp::UpdatePromise {
            promise_id,
            status,
            resolved_at,
        } => {
            let promise = state
                .promises
                .iter_mut()
                .find(|promise| promise.id == *promise_id)
                .ok_or_else(|| ReduceError::PromiseNotFound {
                    id: promise_id.clone(),
                })?;

            if !valid_promise_transition(promise.status, *status) {
                return Err(ReduceError::InvalidPromiseTransition {
                    id: promise_id.clone(),
                    from: promise.status,
                    to: *status,
                });
            }

            if matches!(*status, PromiseStatus::Accepted) && promise.accepted_at.is_none() {
                promise.accepted_at = Some(state.current_node.clone());
            }
            promise.status = *status;
            promise.resolved_at.clone_from(resolved_at);
        }
        StateOp::AddKnowledge { record } => {
            if state.knowledge.iter().any(|item| item.id == record.id) {
                return Err(ReduceError::DuplicateKnowledgeId {
                    id: record.id.clone(),
                });
            }
            state.knowledge.push(record.clone());
        }
        StateOp::AddItem { item } => {
            if state.items.iter().any(|existing| existing.id == item.id) {
                return Err(ReduceError::DuplicateItemId {
                    id: item.id.clone(),
                });
            }
            state.items.push(item.clone());
        }
        StateOp::TransferItem {
            item_id,
            from,
            to,
            placement,
            mode,
        } => {
            let item = state
                .items
                .iter_mut()
                .find(|item| item.id == *item_id)
                .ok_or_else(|| ReduceError::ItemNotFound {
                    id: item_id.clone(),
                })?;
            if item.holder != *from {
                return Err(ReduceError::ItemHolderMismatch {
                    id: item_id.clone(),
                    expected: from.clone(),
                    actual: item.holder.clone(),
                });
            }

            item.holder.clone_from(to);
            item.placement = *placement;
            item.acquisition = ItemAcquisition {
                mode: *mode,
                from: Some(from.clone()),
                at_node: state.current_node.clone(),
            };
        }
        StateOp::RecordCheck { check } => {
            record_check(state, check)?;
        }
        StateOp::AdvanceClock { clock_id, delta } => {
            let clock = state
                .clocks
                .iter_mut()
                .find(|clock| clock.id == *clock_id)
                .ok_or_else(|| ReduceError::ClockNotFound {
                    id: clock_id.clone(),
                })?;
            clock.value = clock.value.saturating_add(*delta).min(clock.max);
        }
    }
    Ok(())
}

fn relationship_key(from: &str, to: &str) -> String {
    format!("{from}->{to}")
}

fn relationship_dimension_mut(
    axes: &mut RelationshipAxes,
    dimension: RelationshipDimension,
) -> &mut i16 {
    match dimension {
        RelationshipDimension::Affinity => &mut axes.affinity,
        RelationshipDimension::Trust => &mut axes.trust,
        RelationshipDimension::Hope => &mut axes.hope,
        RelationshipDimension::Respect => &mut axes.respect,
        RelationshipDimension::Intimacy => &mut axes.intimacy,
        RelationshipDimension::Attachment => &mut axes.attachment,
    }
}

fn valid_promise_transition(from: PromiseStatus, to: PromiseStatus) -> bool {
    match from {
        PromiseStatus::Proposed => matches!(to, PromiseStatus::Accepted),
        PromiseStatus::Accepted => matches!(
            to,
            PromiseStatus::Fulfilled
                | PromiseStatus::Broken
                | PromiseStatus::Released
                | PromiseStatus::Impossible
        ),
        PromiseStatus::Fulfilled
        | PromiseStatus::Broken
        | PromiseStatus::Released
        | PromiseStatus::Impossible => false,
    }
}

fn record_check(state: &mut RuntimeState, check: &CheckRecord) -> Result<(), ReduceError> {
    if let Some(existing) = state
        .checks
        .iter()
        .find(|existing| existing.action_id == check.action_id)
    {
        if existing == check {
            return Ok(());
        }
        return Err(ReduceError::CheckActionConflict {
            action_id: check.action_id.clone(),
        });
    }

    if state.checks.iter().any(|existing| existing.id == check.id) {
        return Err(ReduceError::DuplicateCheckId {
            id: check.id.clone(),
        });
    }

    state.checks.push(check.clone());
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use nana_domain::{
        AcquisitionMode, CheckDifficulty, CheckRecord, CheckResult, ClockState, ItemAcquisition,
        ItemInstance, ItemPlacement, KnowledgeCertainty, KnowledgeRecord, Promise, PromiseStatus,
        PromiseWeight, RelationshipAdjustment, RelationshipAxes, RelationshipDimension, ResourceId,
        RuntimeState, StateDelta, StateOp,
    };

    use super::{apply_delta, ReduceError};

    fn state() -> RuntimeState {
        RuntimeState {
            story_id: "story_demo".to_owned(),
            current_node: "node_001".to_owned(),
            current_branch: "branch_main".to_owned(),
            world_flags: BTreeMap::new(),
            relationships: BTreeMap::new(),
            relationship_states: Vec::new(),
            promises: Vec::new(),
            knowledge: Vec::new(),
            items: Vec::new(),
            clocks: vec![ClockState {
                id: "dawn".to_owned(),
                label: "Dawn".to_owned(),
                value: 2,
                max: 4,
            }],
            checks: Vec::new(),
        }
    }

    fn adjustment(dimension: RelationshipDimension, delta: i16) -> RelationshipAdjustment {
        RelationshipAdjustment {
            dimension,
            delta,
            cause: "test".to_owned(),
            judgment_rule: None,
        }
    }

    fn promise(id: &str, status: PromiseStatus) -> Promise {
        Promise {
            id: id.to_owned(),
            promiser: "player".to_owned(),
            promisee: "nana".to_owned(),
            content: "Return before dawn".to_owned(),
            status,
            weight: PromiseWeight::Major,
            created_at: "node_001".to_owned(),
            accepted_at: (status == PromiseStatus::Accepted).then(|| "node_001".to_owned()),
            resolved_at: None,
        }
    }

    fn item(id: &str, owner: &str, holder: &str) -> ItemInstance {
        ItemInstance {
            id: id.to_owned(),
            spec_ref: ResourceId("item.hairpin".to_owned()),
            owner: owner.to_owned(),
            holder: holder.to_owned(),
            placement: ItemPlacement::Bag,
            quantity: 1,
            condition: "intact".to_owned(),
            state_tags: Vec::new(),
            acquisition: ItemAcquisition {
                mode: AcquisitionMode::Initial,
                from: None,
                at_node: "node_000".to_owned(),
            },
        }
    }

    fn knowledge(id: &str) -> KnowledgeRecord {
        KnowledgeRecord {
            id: id.to_owned(),
            observer: "player".to_owned(),
            subject: Some("nana".to_owned()),
            fact: "Nana has a hairpin".to_owned(),
            certainty: KnowledgeCertainty::Confirmed,
            source: "seen".to_owned(),
            learned_at: "node_001".to_owned(),
            last_verified_at: Some("node_001".to_owned()),
        }
    }

    fn check(id: &str, action_id: &str, roll: u8) -> CheckRecord {
        CheckRecord {
            id: id.to_owned(),
            action_id: action_id.to_owned(),
            actor: "player".to_owned(),
            skill: "persuade".to_owned(),
            target: 55,
            difficulty: CheckDifficulty::Regular,
            bonus_dice: 0,
            roll,
            result: CheckResult::Success,
            pushed_from: None,
            node_id: "node_001".to_owned(),
        }
    }

    #[test]
    fn empty_delta_is_identity() {
        let original = state();
        assert_eq!(
            apply_delta(&original, &StateDelta { ops: Vec::new() }),
            Ok(original)
        );
    }

    #[test]
    fn applies_world_flags_directed_relationships_and_keeps_cursor() {
        let original = state();
        let next = apply_delta(
            &original,
            &StateDelta {
                ops: vec![
                    StateOp::SetWorldFlag {
                        key: "truth_known".to_owned(),
                        value: true,
                    },
                    StateOp::AdjustRelationship {
                        from: "nana".to_owned(),
                        to: "player".to_owned(),
                        adjustment: adjustment(RelationshipDimension::Hope, 4),
                    },
                    StateOp::AdjustRelationship {
                        from: "player".to_owned(),
                        to: "nana".to_owned(),
                        adjustment: adjustment(RelationshipDimension::Trust, -3),
                    },
                ],
            },
        )
        .expect("delta should apply");

        assert_eq!(next.world_flags.get("truth_known"), Some(&true));
        assert_eq!(next.relationships["nana->player"].hope, 54);
        assert_eq!(next.relationships["player->nana"].trust, 47);
        assert_eq!(next.current_node, original.current_node);
        assert_eq!(next.current_branch, original.current_branch);
        assert!(original.world_flags.is_empty());
        assert!(original.relationships.is_empty());
    }

    #[test]
    fn clamps_relationship_values_but_rejects_excessive_single_adjustments() {
        let mut original = state();
        original.relationships.insert(
            "nana->player".to_owned(),
            RelationshipAxes {
                affinity: 99,
                trust: 1,
                ..RelationshipAxes::neutral()
            },
        );
        let clamped = apply_delta(
            &original,
            &StateDelta {
                ops: vec![
                    StateOp::AdjustRelationship {
                        from: "nana".to_owned(),
                        to: "player".to_owned(),
                        adjustment: adjustment(RelationshipDimension::Affinity, 8),
                    },
                    StateOp::AdjustRelationship {
                        from: "nana".to_owned(),
                        to: "player".to_owned(),
                        adjustment: adjustment(RelationshipDimension::Trust, -8),
                    },
                ],
            },
        )
        .expect("bounded adjustments should apply");
        assert_eq!(clamped.relationships["nana->player"].affinity, 100);
        assert_eq!(clamped.relationships["nana->player"].trust, 0);

        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::AdjustRelationship {
                        from: "nana".to_owned(),
                        to: "player".to_owned(),
                        adjustment: adjustment(RelationshipDimension::Hope, i16::MIN),
                    }],
                },
            ),
            Err(ReduceError::RelationshipAdjustmentOutOfRange { delta: i16::MIN })
        );
    }

    #[test]
    fn rejects_duplicate_ids_for_promises_knowledge_and_items() {
        let mut original = state();
        original.promises.push(promise("promise_1", PromiseStatus::Proposed));
        original.knowledge.push(knowledge("knowledge_1"));
        original.items.push(item("item_1", "nana", "nana"));

        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::CreatePromise {
                        promise: promise("promise_1", PromiseStatus::Proposed),
                    }],
                },
            ),
            Err(ReduceError::DuplicatePromiseId {
                id: "promise_1".to_owned()
            })
        );
        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::AddKnowledge {
                        record: knowledge("knowledge_1"),
                    }],
                },
            ),
            Err(ReduceError::DuplicateKnowledgeId {
                id: "knowledge_1".to_owned()
            })
        );
        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::AddItem {
                        item: item("item_1", "nana", "nana"),
                    }],
                },
            ),
            Err(ReduceError::DuplicateItemId {
                id: "item_1".to_owned()
            })
        );
    }

    #[test]
    fn creates_supported_records_in_operation_order() {
        let original = state();
        let accepted = promise("promise_1", PromiseStatus::Accepted);
        let known = knowledge("knowledge_1");
        let carried = item("item_1", "player", "player");
        let rolled = check("check_1", "action_1", 24);

        let next = apply_delta(
            &original,
            &StateDelta {
                ops: vec![
                    StateOp::CreatePromise {
                        promise: accepted.clone(),
                    },
                    StateOp::AddKnowledge {
                        record: known.clone(),
                    },
                    StateOp::AddItem {
                        item: carried.clone(),
                    },
                    StateOp::RecordCheck {
                        check: rolled.clone(),
                    },
                ],
            },
        )
        .expect("unique records should be appended");

        assert_eq!(next.promises, vec![accepted]);
        assert_eq!(next.knowledge, vec![known]);
        assert_eq!(next.items, vec![carried]);
        assert_eq!(next.checks, vec![rolled]);
        assert!(original.promises.is_empty());
        assert!(original.knowledge.is_empty());
        assert!(original.items.is_empty());
        assert!(original.checks.is_empty());
    }

    #[test]
    fn enforces_promise_lifecycle_and_terminal_immutability() {
        let mut original = state();
        original
            .promises
            .push(promise("promise_1", PromiseStatus::Proposed));
        let accepted = apply_delta(
            &original,
            &StateDelta {
                ops: vec![StateOp::UpdatePromise {
                    promise_id: "promise_1".to_owned(),
                    status: PromiseStatus::Accepted,
                    resolved_at: None,
                }],
            },
        )
        .expect("proposed should become accepted");
        assert_eq!(accepted.promises[0].status, PromiseStatus::Accepted);
        assert_eq!(
            accepted.promises[0].accepted_at.as_deref(),
            Some("node_001")
        );

        let fulfilled = apply_delta(
            &accepted,
            &StateDelta {
                ops: vec![StateOp::UpdatePromise {
                    promise_id: "promise_1".to_owned(),
                    status: PromiseStatus::Fulfilled,
                    resolved_at: Some("node_002".to_owned()),
                }],
            },
        )
        .expect("accepted should become terminal");
        assert_eq!(fulfilled.promises[0].status, PromiseStatus::Fulfilled);
        assert_eq!(
            fulfilled.promises[0].resolved_at.as_deref(),
            Some("node_002")
        );

        assert_eq!(
            apply_delta(
                &fulfilled,
                &StateDelta {
                    ops: vec![StateOp::UpdatePromise {
                        promise_id: "promise_1".to_owned(),
                        status: PromiseStatus::Broken,
                        resolved_at: Some("node_003".to_owned()),
                    }],
                },
            ),
            Err(ReduceError::InvalidPromiseTransition {
                id: "promise_1".to_owned(),
                from: PromiseStatus::Fulfilled,
                to: PromiseStatus::Broken,
            })
        );
    }

    #[test]
    fn rejects_skipped_and_invalid_initial_promise_states() {
        let mut original = state();
        original
            .promises
            .push(promise("promise_1", PromiseStatus::Proposed));
        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::UpdatePromise {
                        promise_id: "promise_1".to_owned(),
                        status: PromiseStatus::Fulfilled,
                        resolved_at: Some("node_001".to_owned()),
                    }],
                },
            ),
            Err(ReduceError::InvalidPromiseTransition {
                id: "promise_1".to_owned(),
                from: PromiseStatus::Proposed,
                to: PromiseStatus::Fulfilled,
            })
        );
        assert_eq!(
            apply_delta(
                &state(),
                &StateDelta {
                    ops: vec![StateOp::CreatePromise {
                        promise: promise("promise_terminal", PromiseStatus::Broken),
                    }],
                },
            ),
            Err(ReduceError::InvalidInitialPromiseStatus {
                id: "promise_terminal".to_owned()
            })
        );
    }

    #[test]
    fn transfers_holder_without_changing_owner_and_updates_acquisition() {
        let mut original = state();
        original.items.push(item("hairpin", "nana", "nana"));
        let next = apply_delta(
            &original,
            &StateDelta {
                ops: vec![StateOp::TransferItem {
                    item_id: "hairpin".to_owned(),
                    from: "nana".to_owned(),
                    to: "player".to_owned(),
                    placement: ItemPlacement::Hidden,
                    mode: AcquisitionMode::Stolen,
                }],
            },
        )
        .expect("holder should match");

        let transferred = &next.items[0];
        assert_eq!(transferred.owner, "nana");
        assert_eq!(transferred.holder, "player");
        assert_eq!(transferred.placement, ItemPlacement::Hidden);
        assert_eq!(transferred.acquisition.mode, AcquisitionMode::Stolen);
        assert_eq!(transferred.acquisition.from.as_deref(), Some("nana"));
        assert_eq!(transferred.acquisition.at_node, "node_001");
    }

    #[test]
    fn rejects_transfer_from_non_holder() {
        let mut original = state();
        original.items.push(item("hairpin", "nana", "nana"));
        assert_eq!(
            apply_delta(
                &original,
                &StateDelta {
                    ops: vec![StateOp::TransferItem {
                        item_id: "hairpin".to_owned(),
                        from: "player".to_owned(),
                        to: "nana".to_owned(),
                        placement: ItemPlacement::Hand,
                        mode: AcquisitionMode::Returned,
                    }],
                },
            ),
            Err(ReduceError::ItemHolderMismatch {
                id: "hairpin".to_owned(),
                expected: "player".to_owned(),
                actual: "nana".to_owned(),
            })
        );
    }

    #[test]
    fn record_check_is_idempotent_by_action_and_rejects_conflicts() {
        let original = state();
        let first = check("check_1", "action_1", 24);
        let once = apply_delta(
            &original,
            &StateDelta {
                ops: vec![StateOp::RecordCheck {
                    check: first.clone(),
                }],
            },
        )
        .expect("first check should be recorded");
        let twice = apply_delta(
            &once,
            &StateDelta {
                ops: vec![StateOp::RecordCheck {
                    check: first.clone(),
                }],
            },
        )
        .expect("identical check should be idempotent");
        assert_eq!(twice.checks, vec![first]);

        assert_eq!(
            apply_delta(
                &once,
                &StateDelta {
                    ops: vec![StateOp::RecordCheck {
                        check: check("check_2", "action_1", 25),
                    }],
                },
            ),
            Err(ReduceError::CheckActionConflict {
                action_id: "action_1".to_owned()
            })
        );
        assert_eq!(
            apply_delta(
                &once,
                &StateDelta {
                    ops: vec![StateOp::RecordCheck {
                        check: check("check_1", "action_2", 24),
                    }],
                },
            ),
            Err(ReduceError::DuplicateCheckId {
                id: "check_1".to_owned()
            })
        );
    }

    #[test]
    fn advances_clock_without_exceeding_max() {
        let next = apply_delta(
            &state(),
            &StateDelta {
                ops: vec![StateOp::AdvanceClock {
                    clock_id: "dawn".to_owned(),
                    delta: u16::MAX,
                }],
            },
        )
        .expect("known clock should advance");
        assert_eq!(next.clocks[0].value, 4);
    }

    #[test]
    fn a_failed_late_operation_rolls_back_the_entire_delta() {
        let original = state();
        let result = apply_delta(
            &original,
            &StateDelta {
                ops: vec![
                    StateOp::SetWorldFlag {
                        key: "must_not_commit".to_owned(),
                        value: true,
                    },
                    StateOp::AdvanceClock {
                        clock_id: "missing".to_owned(),
                        delta: 1,
                    },
                ],
            },
        );

        assert_eq!(
            result,
            Err(ReduceError::ClockNotFound {
                id: "missing".to_owned()
            })
        );
        assert!(original.world_flags.is_empty());
        assert_eq!(original.clocks[0].value, 2);
    }
}
