//! Deterministic, GPU-free content registry and runtime-catalog processor.

use content_pipeline::{
    scan_project, serialize_registry, serialize_runtime_catalog, write_if_changed, REGISTRY_PATH,
    RUNTIME_CATALOG_PATH,
};
use serde::Serialize;
use shared::{parse_asset_registry, AssetRegistryDocument};
use std::path::{Path, PathBuf};

#[derive(Serialize)]
struct CommandReport {
    project: String,
    check: bool,
    registry_changed: bool,
    runtime_catalog_changed: bool,
    model_artifacts_changed: usize,
    texture_artifacts_changed: usize,
    material_artifacts_changed: usize,
    processing: content_pipeline::ProcessingReport,
}

fn main() {
    if let Err(error) = run() {
        eprintln!("process-assets failed: {error}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let mut project = PathBuf::from(".");
    let mut check = false;
    let mut json = false;
    let mut arguments = std::env::args().skip(1);
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--project" => {
                project = PathBuf::from(
                    arguments
                        .next()
                        .ok_or_else(|| "--project requires a path".to_string())?,
                );
            }
            "--check" => check = true,
            "--json" => json = true,
            other => return Err(format!("unknown argument `{other}`")),
        }
    }

    let project_display = project.to_string_lossy().into_owned();
    let project = std::fs::canonicalize(&project)
        .map_err(|error| format!("could not resolve project {project_display}: {error}"))?;
    std::env::set_current_dir(&project)
        .map_err(|error| format!("could not enter project {}: {error}", project.display()))?;
    let project = PathBuf::from(".");
    let registry_path = project.join(REGISTRY_PATH);
    let previous = load_registry(&registry_path)?;
    let mut processed = scan_project(&project, &previous)?;
    let mut model_plans = Vec::new();
    for record in processed
        .registry
        .records
        .iter_mut()
        .filter(|record| record.kind == shared::AssetKind::Model)
    {
        let plan = content_pipeline::plan_model_artifacts(record)
            .map_err(|error| format!("could not process model {}: {error}", record.path))?;
        *record = plan.record.clone();
        model_plans.push(plan);
    }
    processed.runtime_catalog = shared::RuntimeContentCatalog::from(&processed.registry);
    let mut texture_plans = Vec::new();
    for record in processed
        .registry
        .records
        .iter()
        .filter(|record| record.kind == shared::AssetKind::Texture)
    {
        let plan = content_pipeline::plan_texture_artifact(&project, record)
            .map_err(|error| format!("could not process Texture {}: {error}", record.path))?;
        content_pipeline::apply_texture_plan_to_catalog(
            &mut processed.runtime_catalog,
            &record.id,
            &plan,
        )?;
        texture_plans.push(plan);
    }
    let mut material_plans = Vec::new();
    for record in processed.registry.records.iter().filter(|record| {
        matches!(
            record.kind,
            shared::AssetKind::Material | shared::AssetKind::MaterialInstance
        )
    }) {
        if let Some(plan) =
            content_pipeline::plan_material_artifact(&project, record, &processed.registry)?
        {
            content_pipeline::apply_material_plan_to_catalog(
                &mut processed.runtime_catalog,
                &record.id,
                &plan,
            )?;
            material_plans.push(plan);
        }
    }
    processed.report.changed = processed.registry != previous;
    let model_artifacts_changed = model_plans
        .iter()
        .filter(|plan| {
            file_differs(&plan.static_path, &plan.static_bytes)
                || file_differs(&plan.animation_path, &plan.animation_bytes)
        })
        .count();
    let texture_artifacts_changed = texture_plans
        .iter()
        .filter(|plan| !plan.reused && file_differs(&plan.output_path, &plan.output_bytes))
        .count();
    let material_artifacts_changed = material_plans
        .iter()
        .filter(|plan| !plan.reused && file_differs(&plan.output_path, &plan.output_bytes))
        .count();
    let registry_bytes = serialize_registry(&processed.registry)?;
    let runtime_bytes = serialize_runtime_catalog(&processed.runtime_catalog)?;
    let runtime_path = project.join(RUNTIME_CATALOG_PATH);
    let registry_changed = file_differs(&registry_path, &registry_bytes);
    let runtime_catalog_changed = file_differs(&runtime_path, &runtime_bytes);
    let report = CommandReport {
        project: project_display,
        check,
        registry_changed,
        runtime_catalog_changed,
        model_artifacts_changed,
        texture_artifacts_changed,
        material_artifacts_changed,
        processing: processed.report,
    };

    if !check {
        let mut publication_paths = model_plans
            .iter()
            .flat_map(|plan| [plan.static_path.clone(), plan.animation_path.clone()])
            .collect::<Vec<_>>();
        publication_paths.extend(texture_plans.iter().map(|plan| plan.output_path.clone()));
        publication_paths.extend(material_plans.iter().map(|plan| plan.output_path.clone()));
        publication_paths.push(registry_path.clone());
        publication_paths.push(runtime_path.clone());
        publication_paths.sort();
        publication_paths.dedup();
        let backups = publication_paths
            .iter()
            .map(|path| (path.clone(), std::fs::read(path).ok()))
            .collect::<Vec<_>>();
        let publication = (|| {
            for plan in &model_plans {
                content_pipeline::publish_model_artifacts(plan)?;
            }
            for plan in &texture_plans {
                content_pipeline::publish_texture_artifact(plan)?;
            }
            for plan in &material_plans {
                content_pipeline::publish_material_artifact(plan)?;
            }
            write_if_changed(&registry_path, &registry_bytes)?;
            write_if_changed(&runtime_path, &runtime_bytes)?;
            Ok::<(), String>(())
        })();
        if let Err(error) = publication {
            for (path, bytes) in &backups {
                restore_file(path, bytes.as_deref());
            }
            return Err(format!("headless content publication rolled back: {error}"));
        }
    }
    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&report)
                .map_err(|error| format!("could not serialize report: {error}"))?
        );
    } else {
        println!(
            "process-assets: discovered={}, added={}, moved={}, removed={}, registry_changed={}, runtime_catalog_changed={}, model_artifacts_changed={}, texture_artifacts_changed={}, material_artifacts_changed={}, mode={}",
            report.processing.discovered,
            report.processing.added,
            report.processing.moved,
            report.processing.removed,
            registry_changed,
            runtime_catalog_changed,
            model_artifacts_changed,
            texture_artifacts_changed,
            material_artifacts_changed,
            if check { "check" } else { "write" }
        );
        for diagnostic in &report.processing.diagnostics {
            eprintln!("  {}: {}", diagnostic.path, diagnostic.message);
        }
    }
    if check
        && (registry_changed
            || runtime_catalog_changed
            || model_artifacts_changed > 0
            || texture_artifacts_changed > 0
            || material_artifacts_changed > 0)
    {
        return Err(
            "generated content state is stale; run `cargo process-assets --project <root>`".into(),
        );
    }
    Ok(())
}

fn load_registry(path: &Path) -> Result<AssetRegistryDocument, String> {
    let source = match std::fs::read_to_string(path) {
        Ok(source) => source,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return Ok(AssetRegistryDocument::default());
        }
        Err(error) => return Err(format!("could not read {}: {error}", path.display())),
    };
    let loaded = parse_asset_registry(&source)?;
    if loaded.migration_required {
        return Err(
            "asset registry v1 requires `cargo upgrade-project --project <root> --apply`".into(),
        );
    }
    Ok(loaded.document)
}

fn file_differs(path: &Path, expected: &[u8]) -> bool {
    std::fs::read(path).map_or(true, |current| current != expected)
}

fn restore_file(path: &Path, bytes: Option<&[u8]>) {
    if let Some(bytes) = bytes {
        let _ = std::fs::write(path, bytes);
    } else {
        let _ = std::fs::remove_file(path);
    }
}
