use std::collections::{BTreeMap, BTreeSet};

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use ts_rs::TS;

pub const DOMAIN_SCHEMA_VERSION: u32 = 1;

#[derive(
    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS,
)]
#[serde(transparent)]
pub struct ResourceId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ResourceKind {
    Character,
    WorldBook,
    Persona,
    PlotModule,
    ItemSpec,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ResourceRef {
    pub id: ResourceId,
    pub kind: ResourceKind,
    pub revision: String,
    pub content_hash: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ResourceHeader {
    pub id: ResourceId,
    pub kind: ResourceKind,
    pub schema_version: u32,
    pub revision: String,
    pub content_hash: String,
    #[serde(default)]
    pub dependencies: Vec<ResourceRef>,
}

pub trait Resource {
    fn header(&self) -> &ResourceHeader;
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct CharacterStyle {
    pub speech: String,
    #[serde(default)]
    pub mannerisms: Vec<String>,
    #[serde(default)]
    pub forbidden_player_assumptions: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct CharacterJudgmentRule {
    pub tags: Vec<String>,
    pub meaning: String,
    #[serde(default)]
    pub relationship_effects: Vec<RelationshipAdjustment>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct SkillValue {
    pub skill: String,
    pub value: u8,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct CharacterCard {
    pub header: ResourceHeader,
    pub name: String,
    pub identity: String,
    pub personality: Vec<String>,
    pub values: Vec<String>,
    pub boundaries: Vec<String>,
    pub style: CharacterStyle,
    pub initial_relationship: RelationshipAxes,
    #[serde(default)]
    pub judgment_rules: Vec<CharacterJudgmentRule>,
    #[serde(default)]
    pub skills: Vec<SkillValue>,
    pub default_expression: String,
}

impl Resource for CharacterCard {
    fn header(&self) -> &ResourceHeader {
        &self.header
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct WorldBookEntry {
    pub id: String,
    pub title: String,
    pub content: String,
    #[serde(default)]
    pub keywords: Vec<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub required_flags: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct WorldBook {
    pub header: ResourceHeader,
    pub name: String,
    pub entries: Vec<WorldBookEntry>,
}

impl Resource for WorldBook {
    fn header(&self) -> &ResourceHeader {
        &self.header
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct Persona {
    pub header: ResourceHeader,
    pub name: String,
    pub identity: String,
    #[serde(default)]
    pub traits: Vec<String>,
    #[serde(default)]
    pub skills: Vec<SkillValue>,
    #[serde(default)]
    pub initial_items: Vec<ResourceId>,
}

impl Resource for Persona {
    fn header(&self) -> &ResourceHeader {
        &self.header
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct PlotPressure {
    pub id: String,
    pub description: String,
    pub clock_id: Option<String>,
    pub clock_delta: Option<u16>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct PlotOutcome {
    pub id: String,
    pub description: String,
    #[serde(default)]
    pub effects: Vec<StateOp>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct PlotEvent {
    pub id: String,
    pub title: String,
    pub tags: Vec<String>,
    pub situation: Vec<String>,
    #[serde(default)]
    pub required_flags: Vec<String>,
    #[serde(default)]
    pub pressures: Vec<PlotPressure>,
    pub outcomes: Vec<PlotOutcome>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct PlotModule {
    pub header: ResourceHeader,
    pub name: String,
    pub opening_event: String,
    pub events: Vec<PlotEvent>,
}

impl Resource for PlotModule {
    fn header(&self) -> &ResourceHeader {
        &self.header
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ItemMechanics {
    pub usable: bool,
    #[serde(default)]
    pub grants_tags: Vec<String>,
    pub check_modifier: Option<i8>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ItemSpec {
    pub header: ResourceHeader,
    pub name: String,
    pub description: String,
    pub tags: Vec<String>,
    #[serde(default)]
    pub lore_refs: Vec<String>,
    pub mechanics: ItemMechanics,
    #[serde(default)]
    pub hidden_facts: Vec<String>,
}

impl Resource for ItemSpec {
    fn header(&self) -> &ResourceHeader {
        &self.header
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ResourceBundle {
    pub id: String,
    pub schema_version: u32,
    pub revision: String,
    pub display_name: String,
    pub entry_character: ResourceId,
    pub entry_persona: ResourceId,
    pub entry_plot_module: Option<ResourceId>,
    #[serde(default)]
    pub characters: Vec<CharacterCard>,
    #[serde(default)]
    pub world_books: Vec<WorldBook>,
    #[serde(default)]
    pub personas: Vec<Persona>,
    #[serde(default)]
    pub plot_modules: Vec<PlotModule>,
    #[serde(default)]
    pub item_specs: Vec<ItemSpec>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct RelationshipAxes {
    pub affinity: i16,
    pub trust: i16,
    pub hope: i16,
    pub respect: i16,
    pub intimacy: i16,
    pub attachment: i16,
}

impl RelationshipAxes {
    pub const fn neutral() -> Self {
        Self {
            affinity: 50,
            trust: 50,
            hope: 50,
            respect: 50,
            intimacy: 50,
            attachment: 50,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct RelationshipAdjustment {
    pub dimension: RelationshipDimension,
    pub delta: i16,
    pub cause: String,
    pub judgment_rule: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipDimension {
    Affinity,
    Trust,
    Hope,
    Respect,
    Intimacy,
    Attachment,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipBand {
    Distant,
    Guarded,
    Warming,
    Close,
    Bonded,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct RelationshipView {
    pub affinity: RelationshipBand,
    pub trust: RelationshipBand,
    pub hope: RelationshipBand,
    pub respect: RelationshipBand,
    pub intimacy: RelationshipBand,
    pub attachment: RelationshipBand,
    pub updated_at_node: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum PromiseStatus {
    Proposed,
    Accepted,
    Fulfilled,
    Broken,
    Released,
    Impossible,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum PromiseWeight {
    Minor,
    Major,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct Promise {
    pub id: String,
    pub promiser: String,
    pub promisee: String,
    pub content: String,
    pub status: PromiseStatus,
    pub weight: PromiseWeight,
    pub created_at: String,
    pub accepted_at: Option<String>,
    pub resolved_at: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct RelationshipState {
    pub id: String,
    pub tag: String,
    pub description: String,
    pub source_node: String,
    pub active: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum KnowledgeCertainty {
    Suspected,
    Reported,
    Confirmed,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct KnowledgeRecord {
    pub id: String,
    pub observer: String,
    pub subject: Option<String>,
    pub fact: String,
    pub certainty: KnowledgeCertainty,
    pub source: String,
    pub learned_at: String,
    pub last_verified_at: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ItemPlacement {
    Bag,
    Worn,
    Hand,
    Scene,
    Hidden,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum AcquisitionMode {
    Initial,
    Found,
    Given,
    Borrowed,
    Stolen,
    Returned,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ItemAcquisition {
    pub mode: AcquisitionMode,
    pub from: Option<String>,
    pub at_node: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ItemInstance {
    pub id: String,
    pub spec_ref: ResourceId,
    pub owner: String,
    pub holder: String,
    pub placement: ItemPlacement,
    pub quantity: u32,
    pub condition: String,
    #[serde(default)]
    pub state_tags: Vec<String>,
    pub acquisition: ItemAcquisition,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum CheckDifficulty {
    Regular,
    Hard,
    Extreme,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum CheckResult {
    CriticalSuccess,
    ExtremeSuccess,
    HardSuccess,
    Success,
    Failure,
    Fumble,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct CheckRecord {
    pub id: String,
    pub action_id: String,
    pub actor: String,
    pub skill: String,
    pub target: u8,
    pub difficulty: CheckDifficulty,
    pub bonus_dice: i8,
    pub roll: u8,
    pub result: CheckResult,
    pub pushed_from: Option<String>,
    pub node_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ClockState {
    pub id: String,
    pub label: String,
    pub value: u16,
    pub max: u16,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct StoryBinding {
    pub persona: ResourceRef,
    pub characters: Vec<ResourceRef>,
    #[serde(default)]
    pub world_books: Vec<ResourceRef>,
    #[serde(default)]
    pub plot_modules: Vec<ResourceRef>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct Story {
    pub id: String,
    pub title: String,
    pub ruleset: String,
    pub bindings: StoryBinding,
    pub root_node: String,
    pub active_branch: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct StateDelta {
    #[serde(default)]
    pub ops: Vec<StateOp>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum StateOp {
    SetWorldFlag {
        key: String,
        value: bool,
    },
    AdjustRelationship {
        from: String,
        to: String,
        adjustment: RelationshipAdjustment,
    },
    CreatePromise {
        promise: Promise,
    },
    UpdatePromise {
        promise_id: String,
        status: PromiseStatus,
        resolved_at: Option<String>,
    },
    AddKnowledge {
        record: KnowledgeRecord,
    },
    AddItem {
        item: ItemInstance,
    },
    TransferItem {
        item_id: String,
        from: String,
        to: String,
        placement: ItemPlacement,
        mode: AcquisitionMode,
    },
    RecordCheck {
        check: CheckRecord,
    },
    AdvanceClock {
        clock_id: String,
        delta: u16,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct RuntimeState {
    pub story_id: String,
    pub current_node: String,
    pub current_branch: String,
    #[serde(default)]
    pub world_flags: BTreeMap<String, bool>,
    #[serde(default)]
    pub relationships: BTreeMap<String, RelationshipAxes>,
    #[serde(default)]
    pub relationship_states: Vec<RelationshipState>,
    #[serde(default)]
    pub promises: Vec<Promise>,
    #[serde(default)]
    pub knowledge: Vec<KnowledgeRecord>,
    #[serde(default)]
    pub items: Vec<ItemInstance>,
    #[serde(default)]
    pub clocks: Vec<ClockState>,
    #[serde(default)]
    pub checks: Vec<CheckRecord>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum BeatKind {
    Narration,
    Dialogue,
    Action,
    System,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct VisualDirective {
    pub character: Option<String>,
    pub expression: Option<String>,
    pub pose: Option<String>,
    pub scene: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct PresentationBeat {
    pub id: String,
    pub kind: BeatKind,
    pub speaker: Option<String>,
    pub text: String,
    pub visual: Option<VisualDirective>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct ActionSuggestion {
    pub id: String,
    pub label: String,
    pub draft: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct PlayerItemView {
    pub instance_id: String,
    pub name: String,
    pub description: String,
    pub quantity: u32,
    pub placement: ItemPlacement,
    pub condition: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct PlayerKnowledgeView {
    pub id: String,
    pub title: String,
    pub summary: String,
    pub certainty: KnowledgeCertainty,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct PlayerPromiseView {
    pub id: String,
    pub content: String,
    pub status: PromiseStatus,
    pub weight: PromiseWeight,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct HistoryNodeView {
    pub id: String,
    pub parent_id: Option<String>,
    pub branch_id: String,
    pub label: String,
    pub is_current: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct PlayerView {
    pub story_id: String,
    pub node_id: String,
    pub branch_id: String,
    pub scene_id: String,
    pub scene_title: String,
    pub character_name: String,
    pub beats: Vec<PresentationBeat>,
    #[serde(default)]
    pub suggestions: Vec<ActionSuggestion>,
    #[serde(default)]
    pub inventory: Vec<PlayerItemView>,
    #[serde(default)]
    pub knowledge: Vec<PlayerKnowledgeView>,
    #[serde(default)]
    pub promises: Vec<PlayerPromiseView>,
    pub relationship: RelationshipView,
    #[serde(default)]
    pub history: Vec<HistoryNodeView>,
    pub can_continue: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum TurnIntent {
    SpeakOrAct,
    Continue,
    Regenerate,
    PushCheck,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct TurnRequest {
    pub story_id: String,
    pub branch_id: String,
    pub expected_node_id: String,
    pub action_id: String,
    pub intent: TurnIntent,
    pub input: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct TurnResult {
    pub committed_node_id: String,
    pub player_view: PlayerView,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum TurnFailureCode {
    StaleNode,
    InvalidInput,
    InvalidModelOutput,
    ProviderUnavailable,
    Cancelled,
    TimedOut,
    Internal,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct TurnFailure {
    pub code: TurnFailureCode,
    pub message: String,
    pub retryable: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct StoryNode {
    pub id: String,
    pub story_id: String,
    pub branch_id: String,
    pub parent_id: Option<String>,
    pub action_id: String,
    pub user_input: String,
    pub beats: Vec<PresentationBeat>,
    pub delta: StateDelta,
    pub state_hash: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct AppInfo {
    pub name: String,
    pub version: String,
    pub contract_version: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct DemoPackSummary {
    pub id: String,
    pub display_name: String,
    pub revision: String,
    pub characters: usize,
    pub world_books: usize,
    pub personas: usize,
    pub plot_modules: usize,
    pub item_specs: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ValidationCode {
    InvalidId,
    InvalidSchemaVersion,
    InvalidRevision,
    InvalidContentHash,
    WrongResourceKind,
    DuplicateResource,
    MissingReference,
    InvalidRelationshipValue,
    InvalidSkillValue,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ValidationIssue {
    pub code: ValidationCode,
    pub path: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
pub struct ValidationReport {
    pub issues: Vec<ValidationIssue>,
}

impl ValidationReport {
    #[must_use]
    pub fn is_valid(&self) -> bool {
        self.issues.is_empty()
    }
}

#[derive(Debug, Error)]
pub enum BundleError {
    #[error("content bundle is invalid")]
    Invalid(ValidationReport),
}

pub fn validate_bundle(bundle: &ResourceBundle) -> ValidationReport {
    let mut issues = Vec::new();
    if bundle.schema_version != DOMAIN_SCHEMA_VERSION {
        issues.push(issue(
            ValidationCode::InvalidSchemaVersion,
            "schema_version",
            format!(
                "expected schema version {DOMAIN_SCHEMA_VERSION}, got {}",
                bundle.schema_version
            ),
        ));
    }
    if !is_resource_id(&bundle.id) {
        issues.push(issue(
            ValidationCode::InvalidId,
            "id",
            "bundle id must use lowercase dotted segments",
        ));
    }
    if !is_revision(&bundle.revision) {
        issues.push(issue(
            ValidationCode::InvalidRevision,
            "revision",
            "revision must be a three-part semantic version",
        ));
    }

    let mut ids = BTreeSet::new();
    for (path, resource) in bundle_resources(bundle) {
        validate_header(&path, resource.header(), &mut issues);
        if !ids.insert(resource.header().id.clone()) {
            issues.push(issue(
                ValidationCode::DuplicateResource,
                format!("{path}.header.id"),
                "resource id must be unique inside a bundle",
            ));
        }
    }

    let available = ids;
    validate_entry_ref(
        &bundle.entry_character,
        "entry_character",
        &available,
        &mut issues,
    );
    validate_entry_ref(
        &bundle.entry_persona,
        "entry_persona",
        &available,
        &mut issues,
    );
    if let Some(plot) = &bundle.entry_plot_module {
        validate_entry_ref(plot, "entry_plot_module", &available, &mut issues);
    }

    for (path, resource) in bundle_resources(bundle) {
        for (index, dependency) in resource.header().dependencies.iter().enumerate() {
            if !available.contains(&dependency.id) {
                issues.push(issue(
                    ValidationCode::MissingReference,
                    format!("{path}.header.dependencies[{index}].id"),
                    format!("missing resource dependency {}", dependency.id.0),
                ));
            }
        }
    }

    for (index, character) in bundle.characters.iter().enumerate() {
        validate_expected_kind(
            &character.header,
            ResourceKind::Character,
            format!("characters[{index}].header.kind"),
            &mut issues,
        );
        validate_axes(
            character.initial_relationship,
            format!("characters[{index}].initial_relationship"),
            &mut issues,
        );
        validate_skills(
            &character.skills,
            format!("characters[{index}].skills"),
            &mut issues,
        );
    }
    for (index, world_book) in bundle.world_books.iter().enumerate() {
        validate_expected_kind(
            &world_book.header,
            ResourceKind::WorldBook,
            format!("world_books[{index}].header.kind"),
            &mut issues,
        );
    }
    for (index, persona) in bundle.personas.iter().enumerate() {
        validate_expected_kind(
            &persona.header,
            ResourceKind::Persona,
            format!("personas[{index}].header.kind"),
            &mut issues,
        );
        validate_skills(
            &persona.skills,
            format!("personas[{index}].skills"),
            &mut issues,
        );
    }
    for (index, plot) in bundle.plot_modules.iter().enumerate() {
        validate_expected_kind(
            &plot.header,
            ResourceKind::PlotModule,
            format!("plot_modules[{index}].header.kind"),
            &mut issues,
        );
    }
    for (index, item) in bundle.item_specs.iter().enumerate() {
        validate_expected_kind(
            &item.header,
            ResourceKind::ItemSpec,
            format!("item_specs[{index}].header.kind"),
            &mut issues,
        );
    }

    ValidationReport { issues }
}

pub fn validate_bundle_or_error(bundle: &ResourceBundle) -> Result<(), BundleError> {
    let report = validate_bundle(bundle);
    if report.is_valid() {
        Ok(())
    } else {
        Err(BundleError::Invalid(report))
    }
}

#[must_use]
pub fn stable_json_hash(bytes: &[u8]) -> String {
    format!("sha256:{:x}", Sha256::digest(bytes))
}

fn bundle_resources(bundle: &ResourceBundle) -> Vec<(String, &dyn Resource)> {
    let mut resources: Vec<(String, &dyn Resource)> = Vec::new();
    resources.extend(
        bundle
            .characters
            .iter()
            .enumerate()
            .map(|(index, value)| (format!("characters[{index}]"), value as &dyn Resource)),
    );
    resources.extend(
        bundle
            .world_books
            .iter()
            .enumerate()
            .map(|(index, value)| (format!("world_books[{index}]"), value as &dyn Resource)),
    );
    resources.extend(bundle.personas.iter().enumerate().map(|(index, value)| {
        (format!("personas[{index}]"), value as &dyn Resource)
    }));
    resources.extend(
        bundle
            .plot_modules
            .iter()
            .enumerate()
            .map(|(index, value)| (format!("plot_modules[{index}]"), value as &dyn Resource)),
    );
    resources.extend(
        bundle
            .item_specs
            .iter()
            .enumerate()
            .map(|(index, value)| (format!("item_specs[{index}]"), value as &dyn Resource)),
    );
    resources
}

fn validate_header(path: &str, header: &ResourceHeader, issues: &mut Vec<ValidationIssue>) {
    if !is_resource_id(&header.id.0) {
        issues.push(issue(
            ValidationCode::InvalidId,
            format!("{path}.header.id"),
            "resource id must use lowercase dotted segments",
        ));
    }
    if header.schema_version != DOMAIN_SCHEMA_VERSION {
        issues.push(issue(
            ValidationCode::InvalidSchemaVersion,
            format!("{path}.header.schema_version"),
            format!("expected schema version {DOMAIN_SCHEMA_VERSION}"),
        ));
    }
    if !is_revision(&header.revision) {
        issues.push(issue(
            ValidationCode::InvalidRevision,
            format!("{path}.header.revision"),
            "revision must be a three-part semantic version",
        ));
    }
    if !is_content_hash(&header.content_hash) {
        issues.push(issue(
            ValidationCode::InvalidContentHash,
            format!("{path}.header.content_hash"),
            "content hash must be sha256 followed by 64 lowercase hex characters",
        ));
    }
}

fn validate_expected_kind(
    header: &ResourceHeader,
    expected: ResourceKind,
    path: String,
    issues: &mut Vec<ValidationIssue>,
) {
    if header.kind != expected {
        issues.push(issue(
            ValidationCode::WrongResourceKind,
            path,
            "resource kind does not match its collection",
        ));
    }
}

fn validate_axes(
    axes: RelationshipAxes,
    path: String,
    issues: &mut Vec<ValidationIssue>,
) {
    let values = [
        ("affinity", axes.affinity),
        ("trust", axes.trust),
        ("hope", axes.hope),
        ("respect", axes.respect),
        ("intimacy", axes.intimacy),
        ("attachment", axes.attachment),
    ];
    for (name, value) in values {
        if !(0..=100).contains(&value) {
            issues.push(issue(
                ValidationCode::InvalidRelationshipValue,
                format!("{path}.{name}"),
                "relationship values must be between 0 and 100",
            ));
        }
    }
}

fn validate_skills(skills: &[SkillValue], path: String, issues: &mut Vec<ValidationIssue>) {
    for (index, skill) in skills.iter().enumerate() {
        if skill.value > 100 {
            issues.push(issue(
                ValidationCode::InvalidSkillValue,
                format!("{path}[{index}].value"),
                "skill values must be between 0 and 100",
            ));
        }
    }
}

fn validate_entry_ref(
    id: &ResourceId,
    path: &str,
    available: &BTreeSet<ResourceId>,
    issues: &mut Vec<ValidationIssue>,
) {
    if !available.contains(id) {
        issues.push(issue(
            ValidationCode::MissingReference,
            path,
            format!("missing entry resource {}", id.0),
        ));
    }
}

fn issue(
    code: ValidationCode,
    path: impl Into<String>,
    message: impl Into<String>,
) -> ValidationIssue {
    ValidationIssue {
        code,
        path: path.into(),
        message: message.into(),
    }
}

fn is_resource_id(value: &str) -> bool {
    !value.is_empty()
        && value.split('.').all(|segment| {
            !segment.is_empty()
                && segment
                    .chars()
                    .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
        })
}

fn is_revision(value: &str) -> bool {
    let parts: Vec<_> = value.split('.').collect();
    parts.len() == 3
        && parts
            .iter()
            .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
}

fn is_content_hash(value: &str) -> bool {
    let Some(hex) = value.strip_prefix("sha256:") else {
        return false;
    };
    hex.len() == 64
        && hex
            .chars()
            .all(|ch| ch.is_ascii_digit() || ('a'..='f').contains(&ch))
}

#[cfg(test)]
mod tests {
    use super::{ResourceId, is_content_hash, is_resource_id, is_revision};

    #[test]
    fn validates_resource_ids() {
        assert!(is_resource_id("nana.character.nana"));
        assert!(is_resource_id("nana.item.red_hairpin"));
        assert!(!is_resource_id("Nana.Character"));
        assert!(!is_resource_id("nana..item"));
        let _typed = ResourceId("nana.character.nana".to_owned());
    }

    #[test]
    fn validates_revisions_and_hashes() {
        assert!(is_revision("1.2.0"));
        assert!(!is_revision("1.2"));
        assert!(is_content_hash(&format!("sha256:{}", "a".repeat(64))));
        assert!(!is_content_hash("sha256:abc"));
    }
}
