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

use crate::documents::{
    RuntimeOverlayImportDocument, RuntimeSaveSliceDocument, RuntimeStateInput,
    RuntimeStateInputDocument, build_runtime_state_input_from_overlay,
    build_runtime_state_input_from_save_slice,
};
use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use crate::state::RuntimeState;
use crate::validation::{
    validate_runtime_overlay_import_document, validate_runtime_save_slice_document,
    validate_runtime_state_input_document,
};

pub fn load_runtime_save_slice_document(
    path: &Path,
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?;
    Ok(document)
}

pub fn load_runtime_overlay_import_document(
    path: &Path,
) -> Result<RuntimeOverlayImportDocument, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    let document: RuntimeOverlayImportDocument = serde_json::from_str(&text)?;
    Ok(document)
}

pub fn save_runtime_save_slice_document(
    path: &Path,
    document: &RuntimeSaveSliceDocument,
) -> Result<(), Box<dyn std::error::Error>> {
    validate_runtime_save_slice_document(document)
        .map_err(|err| format!("invalid runtime save slice document: {err}"))?;
    let bytes = serde_json::to_vec_pretty(document)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, bytes)?;
    Ok(())
}

pub fn save_runtime_overlay_import_document(
    path: &Path,
    document: &RuntimeOverlayImportDocument,
) -> Result<(), Box<dyn std::error::Error>> {
    validate_runtime_overlay_import_document(document)
        .map_err(|err| format!("invalid runtime overlay import document: {err}"))?;
    let bytes = serde_json::to_vec_pretty(document)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, bytes)?;
    Ok(())
}

pub fn load_runtime_state_input(
    path: &Path,
) -> Result<RuntimeStateInput, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    load_runtime_state_input_from_str_with_base(
        &text,
        path.file_stem()
            .and_then(|stem| stem.to_str())
            .unwrap_or("runtime-state"),
        path.parent().unwrap_or_else(|| Path::new(".")),
    )
}

pub fn load_runtime_state_input_from_str(
    text: &str,
    fallback_id: &str,
) -> Result<RuntimeStateInput, Box<dyn std::error::Error>> {
    load_runtime_state_input_from_str_with_base(text, fallback_id, Path::new("."))
}

pub(super) fn load_runtime_state_input_from_str_with_base(
    text: &str,
    fallback_id: &str,
    base_dir: &Path,
) -> Result<RuntimeStateInput, Box<dyn std::error::Error>> {
    if let Ok(document) = serde_json::from_str::<RuntimeStateInputDocument>(text) {
        validate_runtime_state_input_document(&document)
            .map_err(|err| format!("invalid runtime state input document: {err}"))?;
        return Ok(RuntimeStateInput {
            input_id: document.input_id,
            description: document.source.description,
            state: document.state,
        });
    }

    if let Ok(document) = serde_json::from_str::<RuntimeSaveSliceDocument>(text) {
        validate_runtime_save_slice_document(&document)
            .map_err(|err| format!("invalid runtime save slice document: {err}"))?;
        let mut description_parts = Vec::new();
        if let Some(description) = document.source.description {
            description_parts.push(description);
        }
        if let Some(filename) = document.source.original_save_filename {
            description_parts.push(format!("source save {filename}"));
        }
        let input = build_runtime_state_input_from_save_slice(
            &document.save_slice,
            &document.save_slice_id,
            if description_parts.is_empty() {
                None
            } else {
                Some(description_parts.join(" | "))
            },
        )?;
        return Ok(input);
    }

    if let Ok(document) = serde_json::from_str::<RuntimeOverlayImportDocument>(text) {
        validate_runtime_overlay_import_document(&document)
            .map_err(|err| format!("invalid runtime overlay import document: {err}"))?;
        let base_snapshot_path = resolve_document_path(base_dir, &document.base_snapshot_path);
        let save_slice_path = resolve_document_path(base_dir, &document.save_slice_path);

        let snapshot = load_runtime_snapshot_document(&base_snapshot_path)?;
        validate_runtime_snapshot_document(&snapshot).map_err(|err| {
            format!(
                "invalid runtime snapshot {}: {err}",
                base_snapshot_path.display()
            )
        })?;
        let save_slice_document = load_runtime_save_slice_document(&save_slice_path)?;
        validate_runtime_save_slice_document(&save_slice_document).map_err(|err| {
            format!(
                "invalid runtime save slice document {}: {err}",
                save_slice_path.display()
            )
        })?;

        let mut description_parts = Vec::new();
        if let Some(description) = document.source.description {
            description_parts.push(description);
        }
        if let Some(description) = snapshot.source.description {
            description_parts.push(format!("base snapshot {description}"));
        }
        if let Some(description) = save_slice_document.source.description {
            description_parts.push(format!("save slice {description}"));
        }

        return build_runtime_state_input_from_overlay(
            &snapshot.state,
            &save_slice_document.save_slice,
            &document.import_id,
            if description_parts.is_empty() {
                None
            } else {
                Some(description_parts.join(" | "))
            },
        )
        .map_err(Into::into);
    }

    let state: RuntimeState = serde_json::from_str(text)?;
    state
        .validate()
        .map_err(|err| format!("invalid runtime state: {err}"))?;
    Ok(RuntimeStateInput {
        input_id: fallback_id.to_string(),
        description: None,
        state,
    })
}

pub(super) fn resolve_document_path(base_dir: &Path, path: &str) -> PathBuf {
    let candidate = PathBuf::from(path);
    if candidate.is_absolute() {
        candidate
    } else {
        base_dir.join(candidate)
    }
}
