use super::*;

#[derive(Resource, Default)]
pub struct ContentProcessingQueue {
    pub(super) pending: VecDeque<ProcessingRequest>,
    pub(super) active: Option<ActiveProcessingJob>,
}

#[derive(Debug, Clone)]
pub(super) enum ProcessingRequest {
    Material {
        key: AuthoredAssetDocumentKey,
        revision: u64,
        root: PathBuf,
        record: AssetRecord,
        registry: AssetRegistryDocument,
    },
    Texture {
        key: AuthoredAssetDocumentKey,
        revision: u64,
        root: PathBuf,
        record: AssetRecord,
        registry: AssetRegistryDocument,
    },
    Model {
        key: AuthoredAssetDocumentKey,
        revision: u64,
        root: PathBuf,
        record: AssetRecord,
        registry: AssetRegistryDocument,
    },
    Defaults {
        key: AuthoredAssetDocumentKey,
        revision: u64,
        root: PathBuf,
        registry: AssetRegistryDocument,
    },
}

impl ProcessingRequest {
    pub(super) fn key(&self) -> &AuthoredAssetDocumentKey {
        match self {
            Self::Material { key, .. }
            | Self::Texture { key, .. }
            | Self::Model { key, .. }
            | Self::Defaults { key, .. } => key,
        }
    }

    pub(super) fn revision(&self) -> u64 {
        match self {
            Self::Material { revision, .. }
            | Self::Texture { revision, .. }
            | Self::Model { revision, .. }
            | Self::Defaults { revision, .. } => *revision,
        }
    }
}

pub(super) struct ActiveProcessingJob {
    pub(super) key: AuthoredAssetDocumentKey,
    pub(super) revision: u64,
    pub(super) handle: JoinHandle<Result<ProcessingOutput, String>>,
}

pub(super) enum ProcessingOutput {
    RuntimeCatalog(RuntimeContentCatalog),
    Model {
        registry: AssetRegistryDocument,
        runtime: RuntimeContentCatalog,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct RecoveryEnvelope {
    pub(super) key: AuthoredAssetDocumentKey,
    pub(super) path: String,
    pub(super) baseline_revision: String,
    pub(super) value: AuthoredAssetValue,
}

pub struct AuthoredAssetDocumentsPlugin;

impl Plugin for AuthoredAssetDocumentsPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<AuthoredAssetDocumentStore>()
            .init_resource::<ContentProcessingQueue>()
            .add_systems(Startup, discover_asset_recovery)
            .add_systems(
                Update,
                (
                    drive_asset_save_requests,
                    drive_content_processing,
                    tick_asset_recovery,
                )
                    .chain(),
            );
    }
}

pub fn ensure_material_document(
    world: &mut World,
    path: &str,
    fallback_label: &str,
) -> AuthoredAssetDocumentKey {
    ensure_file_document(world, path, AuthoredAssetDocumentKind::Material, || {
        MaterialAsset::load_from_path(path)
            .map(AuthoredAssetValue::Material)
            .unwrap_or_else(|_| {
                AuthoredAssetValue::Material(MaterialAsset {
                    schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
                    label: fallback_label.to_string(),
                    shader: Default::default(),
                    shader_ref: None,
                    render_state: shared::MaterialRenderState::default(),
                    provenance: None,
                    inputs: shared::MaterialInputSet::default(),
                })
            })
    })
}

pub fn ensure_material_instance_document(
    world: &mut World,
    path: &str,
    fallback_label: &str,
) -> AuthoredAssetDocumentKey {
    ensure_file_document(
        world,
        path,
        AuthoredAssetDocumentKind::MaterialInstance,
        || {
            MaterialInstanceAsset::load_from_path(path)
                .map(AuthoredAssetValue::MaterialInstance)
                .unwrap_or_else(|_| {
                    AuthoredAssetValue::MaterialInstance(MaterialInstanceAsset {
                        schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
                        label: fallback_label.to_string(),
                        base: Default::default(),
                        overrides: Default::default(),
                    })
                })
        },
    )
}

pub(crate) fn reload_file_document_after_conflict(
    world: &mut World,
    path: &Path,
    kind: AuthoredAssetDocumentKind,
) -> Result<String, String> {
    let normalized = normalize_path(&path.to_string_lossy());
    let key = world
        .resource::<AuthoredAssetDocumentStore>()
        .path_index
        .get(&normalized)
        .filter(|key| key.kind == kind)
        .cloned()
        .ok_or_else(|| "the conflicted asset document is no longer open".to_string())?;
    discard_document_recovery(world, &key);
    {
        let mut store = world.resource_mut::<AuthoredAssetDocumentStore>();
        store.documents.remove(&key);
        store.path_index.remove(&normalized);
        if store.active.as_ref() == Some(&key) {
            store.active = None;
        }
    }
    if let Some(mut overlay) =
        world.get_resource_mut::<blacksite_surface::LiveMaterialDocumentOverlay>()
    {
        overlay.remove(&key.asset_id);
    }
    if let Some(mut cache) = world.get_resource_mut::<blacksite_surface::SurfaceMaterialCache>() {
        cache.invalidate_disk_documents();
    }
    Ok(format!("Reloaded {} from disk", path.display()))
}

pub(crate) fn adopt_material_conflict_save_as(
    world: &mut World,
    original_path: &Path,
    catalog_path: &str,
    kind: AuthoredAssetDocumentKind,
) -> Result<String, String> {
    let original = normalize_path(&original_path.to_string_lossy());
    if let Some(key) = world
        .resource::<AuthoredAssetDocumentStore>()
        .path_index
        .get(&original)
        .filter(|key| key.kind == kind)
        .cloned()
    {
        discard_document_recovery(world, &key);
        let mut store = world.resource_mut::<AuthoredAssetDocumentStore>();
        store.documents.remove(&key);
        store.path_index.remove(&original);
        if store.active.as_ref() == Some(&key) {
            store.active = None;
        }
    }
    match kind {
        AuthoredAssetDocumentKind::Material => {
            ensure_material_document(world, catalog_path, "Material copy");
        }
        AuthoredAssetDocumentKind::MaterialInstance => {
            ensure_material_instance_document(world, catalog_path, "Material Instance copy");
        }
        _ => return Err("Save As adoption requires a Material document".into()),
    }
    Ok(format!("Saved material copy to {catalog_path}"))
}

pub(super) fn ensure_file_document(
    world: &mut World,
    path: &str,
    kind: AuthoredAssetDocumentKind,
    load: impl FnOnce() -> AuthoredAssetValue,
) -> AuthoredAssetDocumentKey {
    let normalized = normalize_path(path);
    if let Some(key) = world
        .resource::<AuthoredAssetDocumentStore>()
        .path_index
        .get(&normalized)
        .filter(|key| key.kind == kind)
        .cloned()
    {
        return key;
    }
    let asset_id = world
        .resource::<AssetRegistry>()
        .records
        .iter()
        .find(|record| normalize_path(&record.path) == normalized)
        .map(|record| record.id.as_string())
        .unwrap_or_else(|| format!("path:{}", blake3::hash(normalized.as_bytes()).to_hex()));
    let key = AuthoredAssetDocumentKey::new(asset_id, kind);
    let snapshot =
        FileSnapshot::capture(Path::new(path)).unwrap_or_else(|_| FileSnapshot::missing());
    let document = AuthoredAssetDocument::new(key.clone(), normalized.clone(), load(), snapshot);
    let mut store = world.resource_mut::<AuthoredAssetDocumentStore>();
    store.path_index.insert(normalized, key.clone());
    store.documents.insert(key.clone(), document);
    key
}

pub fn ensure_model_import_document(
    world: &mut World,
    record: &AssetRecord,
) -> AuthoredAssetDocumentKey {
    ensure_registry_document(
        world,
        record.id.clone(),
        &record.path,
        AuthoredAssetDocumentKind::ModelImportSettings,
        AuthoredAssetValue::ModelImportSettings(record.model_import().clone()),
    )
}

pub fn ensure_texture_import_document(
    world: &mut World,
    record: &AssetRecord,
) -> AuthoredAssetDocumentKey {
    ensure_registry_document(
        world,
        record.id.clone(),
        &record.path,
        AuthoredAssetDocumentKind::TextureImportSettings,
        AuthoredAssetValue::TextureImportSettings(
            record.texture_import().cloned().unwrap_or_default(),
        ),
    )
}

pub fn ensure_content_defaults_document(world: &mut World) -> AuthoredAssetDocumentKey {
    let defaults = world.resource::<AssetRegistry>().defaults.clone();
    ensure_registry_document(
        world,
        AssetId(uuid::Uuid::nil()),
        content_pipeline::REGISTRY_PATH,
        AuthoredAssetDocumentKind::ProjectContentDefaults,
        AuthoredAssetValue::ProjectContentDefaults(defaults),
    )
}

pub(super) fn ensure_registry_document(
    world: &mut World,
    asset_id: AssetId,
    logical_path: &str,
    kind: AuthoredAssetDocumentKind,
    value: AuthoredAssetValue,
) -> AuthoredAssetDocumentKey {
    let key = AuthoredAssetDocumentKey::new(asset_id.as_string(), kind);
    if world
        .resource::<AuthoredAssetDocumentStore>()
        .documents
        .contains_key(&key)
    {
        return key;
    }
    let root = project_root(world);
    let registry_path = root.join(content_pipeline::REGISTRY_PATH);
    let snapshot =
        FileSnapshot::capture(&registry_path).unwrap_or_else(|_| FileSnapshot::missing());
    let registry = world.resource::<AssetRegistry>().document();
    let document = AuthoredAssetDocument::new(
        key.clone(),
        normalize_path(logical_path),
        value,
        snapshot.clone(),
    );
    let mut store = world.resource_mut::<AuthoredAssetDocumentStore>();
    store.registry_clean.get_or_insert(registry);
    store.registry_snapshot.get_or_insert(snapshot);
    store
        .path_index
        .insert(normalize_path(logical_path), key.clone());
    store.documents.insert(key.clone(), document);
    key
}
