use std::{
    env, fs,
    path::{Path, PathBuf},
};

use nana_domain::{
    AcquisitionMode, ActionSuggestion, AppInfo, BeatKind, CharacterCard, CharacterJudgmentRule,
    CharacterStyle, CheckDifficulty, CheckRecord, CheckResult, ClockState, DemoPackSummary,
    HistoryNodeView, ItemAcquisition, ItemInstance, ItemMechanics, ItemPlacement, ItemSpec,
    KnowledgeCertainty, KnowledgeRecord, Persona, PlayerItemView, PlayerKnowledgeView,
    PlayerPromiseView, PlayerView, PlotEvent, PlotModule, PlotOutcome, PlotPressure, Promise,
    PromiseStatus, PromiseWeight, RelationshipAdjustment, RelationshipAxes, RelationshipBand,
    RelationshipDimension, RelationshipState, RelationshipView, ResourceBundle, ResourceHeader,
    ResourceId, ResourceKind, ResourceRef, RuntimeState, SkillValue, StateDelta, StateOp, Story,
    StoryBinding, StoryNode, TurnFailure, TurnFailureCode, TurnIntent, TurnRequest, TurnResult,
    ValidationCode, ValidationIssue, ValidationReport, VisualDirective, WorldBook, WorldBookEntry,
};
use schemars::{JsonSchema, schema_for};
use serde::Serialize;
use sha2::{Digest, Sha256};
use ts_rs::TS;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let check = env::args().any(|argument| argument == "--check");
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let outputs = generated_outputs(&root)?;

    if check {
        let mut stale = Vec::new();
        for (path, expected) in outputs {
            if fs::read(&path).ok().as_deref() != Some(expected.as_slice()) {
                stale.push(path);
            }
        }
        if stale.is_empty() {
            return Ok(());
        }
        for path in stale {
            eprintln!("stale generated contract: {}", path.display());
        }
        return Err("generated contracts are stale".into());
    }

    for (path, bytes) in outputs {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(path, bytes)?;
    }
    Ok(())
}

fn generated_outputs(
    root: &Path,
) -> Result<Vec<(PathBuf, Vec<u8>)>, Box<dyn std::error::Error>> {
    let mut outputs = Vec::new();
    let schema_dir = root.join("contracts/schema");

    add_schema::<ResourceHeader>(&mut outputs, &schema_dir, "resource-header")?;
    add_schema::<CharacterCard>(&mut outputs, &schema_dir, "character-card")?;
    add_schema::<WorldBook>(&mut outputs, &schema_dir, "world-book")?;
    add_schema::<Persona>(&mut outputs, &schema_dir, "persona")?;
    add_schema::<PlotModule>(&mut outputs, &schema_dir, "plot-module")?;
    add_schema::<ItemSpec>(&mut outputs, &schema_dir, "item-spec")?;
    add_schema::<ResourceBundle>(&mut outputs, &schema_dir, "resource-bundle")?;
    add_schema::<StoryNode>(&mut outputs, &schema_dir, "story-node")?;
    add_schema::<RuntimeState>(&mut outputs, &schema_dir, "runtime-state")?;
    add_schema::<PlayerView>(&mut outputs, &schema_dir, "player-view")?;
    add_schema::<TurnRequest>(&mut outputs, &schema_dir, "turn-request")?;
    add_schema::<TurnResult>(&mut outputs, &schema_dir, "turn-result")?;
    add_schema::<TurnFailure>(&mut outputs, &schema_dir, "turn-failure")?;
    add_schema::<AppInfo>(&mut outputs, &schema_dir, "app-info")?;
    add_schema::<DemoPackSummary>(&mut outputs, &schema_dir, "demo-pack-summary")?;

    let declarations = [
        ResourceId::decl(),
        ResourceKind::decl(),
        ResourceRef::decl(),
        ResourceHeader::decl(),
        CharacterStyle::decl(),
        CharacterJudgmentRule::decl(),
        SkillValue::decl(),
        CharacterCard::decl(),
        WorldBookEntry::decl(),
        WorldBook::decl(),
        Persona::decl(),
        PlotPressure::decl(),
        PlotOutcome::decl(),
        PlotEvent::decl(),
        PlotModule::decl(),
        ItemMechanics::decl(),
        ItemSpec::decl(),
        ResourceBundle::decl(),
        RelationshipAxes::decl(),
        RelationshipAdjustment::decl(),
        RelationshipDimension::decl(),
        RelationshipBand::decl(),
        RelationshipView::decl(),
        PromiseStatus::decl(),
        PromiseWeight::decl(),
        Promise::decl(),
        RelationshipState::decl(),
        KnowledgeCertainty::decl(),
        KnowledgeRecord::decl(),
        ItemPlacement::decl(),
        AcquisitionMode::decl(),
        ItemAcquisition::decl(),
        ItemInstance::decl(),
        CheckDifficulty::decl(),
        CheckResult::decl(),
        CheckRecord::decl(),
        ClockState::decl(),
        StoryBinding::decl(),
        Story::decl(),
        StateDelta::decl(),
        StateOp::decl(),
        StoryNode::decl(),
        RuntimeState::decl(),
        BeatKind::decl(),
        VisualDirective::decl(),
        PresentationBeat::decl(),
        ActionSuggestion::decl(),
        PlayerItemView::decl(),
        PlayerKnowledgeView::decl(),
        PlayerPromiseView::decl(),
        HistoryNodeView::decl(),
        PlayerView::decl(),
        TurnIntent::decl(),
        TurnRequest::decl(),
        TurnResult::decl(),
        TurnFailureCode::decl(),
        TurnFailure::decl(),
        AppInfo::decl(),
        DemoPackSummary::decl(),
        ValidationCode::decl(),
        ValidationIssue::decl(),
        ValidationReport::decl(),
    ]
    .join("\n\n");
    let ts = format!(
        "// @generated by crates/nana-contracts; do not edit.\n\n{declarations}\n"
    );
    outputs.push((
        root.join("contracts/ts/index.ts"),
        ts.into_bytes(),
    ));

    let domain_source = fs::read(root.join("crates/nana-domain/src/lib.rs"))?;
    let source_hash = format!("{:x}\n", Sha256::digest(domain_source));
    outputs.push((
        root.join("contracts/.source.sha256"),
        source_hash.into_bytes(),
    ));

    Ok(outputs)
}

fn add_schema<T: JsonSchema + Serialize>(
    outputs: &mut Vec<(PathBuf, Vec<u8>)>,
    schema_dir: &Path,
    name: &str,
) -> Result<(), serde_json::Error> {
    let mut bytes = serde_json::to_vec_pretty(&schema_for!(T))?;
    bytes.push(b'\n');
    outputs.push((schema_dir.join(format!("{name}.schema.json")), bytes));
    Ok(())
}
