use super::*;

pub(super) fn plan_property_block_promotion(
    world: &World,
    entity: Entity,
    base: &MaterialRef,
    block: &MaterialPropertyBlock,
    slot_name: &str,
) -> Result<PendingPropertyBlockPromotion, String> {
    plan_material_instance_assignment(world, entity, base, block, slot_name, true)
}

pub(super) fn plan_material_instance_assignment(
    world: &World,
    entity: Entity,
    base: &MaterialRef,
    block: &MaterialPropertyBlock,
    slot_name: &str,
    include_property_block: bool,
) -> Result<PendingPropertyBlockPromotion, String> {
    let selected_path = base.0.source_path.as_deref().ok_or_else(|| {
        "select a project Material before promoting this property block".to_string()
    })?;
    let project_root = world
        .get_resource::<ProjectWorkspace>()
        .map(|workspace| PathBuf::from(&workspace.root))
        .unwrap_or_else(|| PathBuf::from("."));
    let selected_relative = project_relative_path(&project_root, Path::new(selected_path))?;
    let selected_absolute = project_root.join(&selected_relative);
    let (selected_text, selected_snapshot) = read_snapshotted_text(&selected_absolute)?;
    let mut source_snapshots = vec![PromotionSourceSnapshot {
        path: selected_absolute,
        snapshot: selected_snapshot,
    }];
    let (direct_base, mut parameters, mut textures, base_label, direct_base_asset) = if let Ok(
        material,
    ) =
        ron::from_str::<MaterialAsset>(&selected_text)
    {
        (
            base.clone(),
            Vec::new(),
            Vec::new(),
            material.label.clone(),
            material,
        )
    } else if let Ok(instance) = ron::from_str::<MaterialInstanceAsset>(&selected_text) {
        let base_path = instance.base.0.source_path.as_deref().ok_or_else(|| {
            "the selected Material Instance has no direct project base".to_string()
        })?;
        let base_relative = project_relative_path(&project_root, Path::new(base_path))?;
        let base_absolute = project_root.join(base_relative);
        let (base_text, base_snapshot) = read_snapshotted_text(&base_absolute)?;
        let direct_base_asset = ron::from_str::<MaterialAsset>(&base_text)
            .map_err(|_| "the selected Material Instance base is missing or invalid".to_string())?;
        source_snapshots.push(PromotionSourceSnapshot {
            path: base_absolute,
            snapshot: base_snapshot,
        });
        (
            instance.base,
            instance.overrides.values,
            instance.overrides.textures,
            instance.label,
            direct_base_asset,
        )
    } else {
        return Err(
                "Default Grid and imported source materials cannot be promotion bases; select a project Material first"
                    .to_string(),
            );
    };
    let schema_path = direct_base_asset
        .shader_ref
        .as_ref()
        .and_then(|reference| reference.source_path.as_deref())
        .or(direct_base_asset.shader.schema_path.as_deref());
    let schema = if let Some(schema_path) = schema_path {
        let schema_relative = project_relative_path(&project_root, Path::new(schema_path))?;
        let schema_absolute = project_root.join(schema_relative);
        let (schema_text, schema_snapshot) = read_snapshotted_text(&schema_absolute)?;
        source_snapshots.push(PromotionSourceSnapshot {
            path: schema_absolute,
            snapshot: schema_snapshot,
        });
        Some(
            ron::from_str::<shared::ShaderSchemaAsset>(&schema_text).map_err(|error| {
                format!("the selected Material shader schema is invalid: {error}")
            })?,
        )
    } else {
        None
    };
    let custom_surface = schema
        .as_ref()
        .and_then(|schema| schema.wgsl_path.as_deref())
        .is_some();
    if include_property_block {
        blacksite_surface::validate_property_block_for_promotion(
            &project_root,
            block,
            schema.as_ref(),
            custom_surface,
        )?;
    }
    let variation_kind = if include_property_block {
        "Override"
    } else {
        "Instance"
    };
    let mut promoted = MaterialInstanceAsset {
        schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
        label: format!("{base_label} {slot_name} {variation_kind}"),
        base: direct_base,
        overrides: shared::MaterialInputSet {
            values: std::mem::take(&mut parameters),
            textures: std::mem::take(&mut textures),
        },
    };
    if include_property_block {
        promoted.merge_overrides(block.parameters.clone(), block.textures.clone());
    }

    let current_folder = world
        .get_resource::<EditorAssets>()
        .map(|assets| assets.current_folder.clone())
        .unwrap_or_else(|| ASSETS_ROOT.to_string());
    let destination_folder =
        if current_folder.starts_with(ASSETS_ROOT) && current_folder != BUILTINS_FOLDER {
            PathBuf::from(current_folder)
        } else {
            selected_relative
                .parent()
                .map(Path::to_path_buf)
                .unwrap_or_else(|| PathBuf::from(ASSETS_ROOT))
        };
    let stem: String = promoted
        .label
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric() {
                character.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .collect();
    let stem = stem.trim_matches('_').to_string();
    let mut index = 0usize;
    let destination_path = loop {
        let suffix = if index == 0 {
            String::new()
        } else {
            format!("_{index}")
        };
        let candidate = destination_folder.join(format!("{stem}{suffix}.material-instance.ron"));
        if !project_root.join(&candidate).exists() {
            break candidate;
        }
        index += 1;
    };
    let bytes = ron::ser::to_string_pretty(&promoted, ron::ser::PrettyConfig::default())
        .map(String::into_bytes)
        .map_err(|error| format!("could not serialize promoted Material Instance: {error}"))?;
    let destination_absolute = project_root.join(&destination_path);
    let target_snapshot = FileSnapshot::capture(&destination_absolute)?;
    if target_snapshot != FileSnapshot::missing() {
        return Err(format!(
            "{} already exists; refresh the promotion review",
            destination_path.display()
        ));
    }
    Ok(PendingPropertyBlockPromotion {
        entity,
        slot_id: block.slot_id.clone(),
        slot_name: slot_name.to_string(),
        base: base.clone(),
        actor_assignment: actor_material_assignment_for_slot(world, entity, &block.slot_id),
        block: block.clone(),
        project_root,
        destination_path,
        destination_absolute,
        target_snapshot,
        source_snapshots,
        instance: promoted,
        bytes,
        clear_property_block: include_property_block,
    })
}

pub(super) fn read_snapshotted_text(path: &Path) -> Result<(String, FileSnapshot), String> {
    let bytes =
        fs::read(path).map_err(|error| format!("could not read {}: {error}", path.display()))?;
    let snapshot = FileSnapshot::from_loaded_bytes(path, &bytes);
    let text = String::from_utf8(bytes)
        .map_err(|error| format!("{} is not UTF-8: {error}", path.display()))?;
    Ok((text, snapshot))
}

pub(super) fn project_relative_path(project_root: &Path, path: &Path) -> Result<PathBuf, String> {
    let relative = if path.is_absolute() {
        path.strip_prefix(project_root).map_err(|_| {
            format!(
                "{} is outside project root {}",
                path.display(),
                project_root.display()
            )
        })?
    } else {
        path
    };
    if relative.components().any(|component| {
        matches!(
            component,
            std::path::Component::ParentDir | std::path::Component::RootDir
        )
    }) || !relative.starts_with(ASSETS_ROOT)
    {
        return Err(format!(
            "{} is not a safe project asset path",
            path.display()
        ));
    }
    Ok(relative.to_path_buf())
}

pub(super) fn validate_property_block_promotion(
    world: &World,
    promotion: &PendingPropertyBlockPromotion,
) -> Result<(), String> {
    if promotion.clear_property_block {
        let current_block = world
            .get::<MaterialPropertyBlocks>(promotion.entity)
            .and_then(|blocks| {
                blocks
                    .slots
                    .iter()
                    .find(|block| block.slot_id == promotion.slot_id)
            })
            .ok_or_else(|| "the runtime property block no longer exists".to_string())?;
        if current_block != &promotion.block {
            return Err(
                "the runtime property block changed; refresh the promotion review".to_string(),
            );
        }
    }
    let current_actor_assignment =
        actor_material_assignment_for_slot(world, promotion.entity, &promotion.slot_id);
    if current_actor_assignment != promotion.actor_assignment {
        return Err(
            "the target renderer slot assignment changed; refresh the promotion review".to_string(),
        );
    }
    let current_base =
        effective_material_for_actor_slot(world, promotion.entity, &promotion.slot_id).ok_or_else(
            || "the target renderer slot is missing or has no project base".to_string(),
        )?;
    if current_base != promotion.base {
        return Err(
            "the target renderer slot's effective base changed; refresh the promotion review"
                .to_string(),
        );
    }
    for source in &promotion.source_snapshots {
        if FileSnapshot::capture(&source.path)? != source.snapshot {
            return Err(format!(
                "{} changed after the promotion review opened",
                source.path.display()
            ));
        }
    }
    if FileSnapshot::capture(&promotion.destination_absolute)? != promotion.target_snapshot {
        return Err(format!(
            "{} changed after the promotion review opened",
            promotion.destination_path.display()
        ));
    }
    Ok(())
}

pub(super) fn commit_property_block_promotion(
    world: &mut World,
    promotion: &PendingPropertyBlockPromotion,
) -> Result<PathBuf, String> {
    validate_property_block_promotion(world, promotion)?;
    let registry_path = promotion.project_root.join(content_pipeline::REGISTRY_PATH);
    let runtime_catalog_path = promotion
        .project_root
        .join(content_pipeline::RUNTIME_CATALOG_PATH);
    let registry_bytes_before = fs::read(&registry_path).ok();
    let runtime_catalog_bytes_before = fs::read(&runtime_catalog_path).ok();
    publish_authored_file(
        world,
        &promotion.destination_absolute,
        &promotion.bytes,
        &promotion.target_snapshot,
        FileWriteIntent::MaterialInstance,
    )?;
    let normalized = promotion
        .destination_path
        .to_string_lossy()
        .replace('\\', "/");
    let registry_before = world
        .get_resource::<AssetRegistry>()
        .ok_or_else(|| "Asset registry is unavailable".to_string())?
        .document();
    let registry_result = if let Some(mut registry) = world.get_resource_mut::<AssetRegistry>() {
        ensure_asset_record_at(
            &mut registry,
            &promotion.project_root,
            normalized.clone(),
            promotion.instance.label.clone(),
            "MaterialInstance",
        )
        .map(|record| record.id.as_string())
    } else {
        Err("Asset registry is unavailable".to_string())
    };
    let id = match registry_result {
        Ok(id) => id,
        Err(error) => {
            return match remove_published_file(&promotion.destination_absolute) {
                Ok(()) => Err(error),
                Err(rollback_error) => Err(format!("{error}; rollback failed: {rollback_error}")),
            };
        }
    };
    let reference = MaterialRef::new(
        EditorAssetRef::new(id, "material:instance", promotion.instance.label.clone())
            .with_source_path(&normalized),
    );
    let history_label = if promotion.clear_property_block {
        "Promote Material Property Block"
    } else {
        "Create Material Instance and Assign"
    };
    let transaction = if let Some(mut renderer) =
        world.get::<SkinnedMeshRenderer>(promotion.entity).cloned()
    {
        let slot = renderer
            .materials
            .slot_mut(&promotion.slot_id)
            .ok_or_else(|| "the target skinned renderer slot is missing".to_string())?;
        slot.material = Some(reference);
        reflected_component_transaction(
            world,
            promotion.entity,
            history_label,
            shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
            COMPONENT_SKINNED_MESH_RENDERER,
            move |world, entity| {
                world.entity_mut(entity).insert(renderer);
                Ok(())
            },
        )
    } else if let Some(mut renderer) = world.get::<StaticMeshRenderer>(promotion.entity).cloned() {
        let slot = renderer
            .materials
            .slot_mut(&promotion.slot_id)
            .ok_or_else(|| "the target static renderer slot is missing".to_string())?;
        slot.material = Some(reference);
        reflected_component_transaction(
            world,
            promotion.entity,
            history_label,
            shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
            COMPONENT_STATIC_MESH_RENDERER,
            move |world, entity| {
                world.entity_mut(entity).insert(renderer);
                Ok(())
            },
        )
    } else if let Some(mut primitive) = world.get::<Primitive>(promotion.entity).cloned() {
        if primitive.surface.id != promotion.slot_id {
            return Err("the target primitive material slot is missing".into());
        }
        primitive.surface.material = Some(reference);
        reflected_component_transaction(
            world,
            promotion.entity,
            history_label,
            shared::AUTHORING_COMPONENT_PRIMITIVE,
            COMPONENT_PRIMITIVE,
            move |world, entity| {
                world.entity_mut(entity).insert(primitive);
                Ok(())
            },
        )
    } else {
        Err("the target entity no longer has a supported renderer".to_string())
    };
    if let Err(error) = transaction {
        let mut rollback_errors = Vec::new();
        if let Err(rollback_error) = remove_published_file(&promotion.destination_absolute) {
            rollback_errors.push(rollback_error);
        }
        if let Some(mut registry) = world.get_resource_mut::<AssetRegistry>() {
            registry.schema_version = registry_before.schema_version;
            registry.defaults = registry_before.defaults;
            registry.records = registry_before.records;
            registry.index_dirty = false;
        }
        if let Err(rollback_error) =
            restore_optional_file(&registry_path, registry_bytes_before.as_deref())
        {
            rollback_errors.push(rollback_error);
        }
        if let Err(rollback_error) = restore_optional_file(
            &runtime_catalog_path,
            runtime_catalog_bytes_before.as_deref(),
        ) {
            rollback_errors.push(rollback_error);
        }
        if !rollback_errors.is_empty() {
            return Err(format!(
                "{error}; promotion rollback failed: {}",
                rollback_errors.join("; ")
            ));
        }
        return Err(error);
    }
    if promotion.clear_property_block {
        clear_promoted_property_block(world, promotion.entity, &promotion.slot_id);
    }
    Ok(promotion.destination_path.clone())
}

pub(super) fn remove_published_file(path: &Path) -> Result<(), String> {
    match fs::remove_file(path) {
        Ok(()) => Ok(()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(format!("could not remove {}: {error}", path.display())),
    }
}

pub(super) fn restore_optional_file(path: &Path, bytes: Option<&[u8]>) -> Result<(), String> {
    if let Some(bytes) = bytes {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
        }
        fs::write(path, bytes)
            .map_err(|error| format!("could not restore {}: {error}", path.display()))
    } else {
        remove_published_file(path)
    }
}

pub(super) fn clear_promoted_property_block(
    world: &mut World,
    entity: Entity,
    slot_id: &ComponentInstanceId,
) {
    let mut remove_component = false;
    if let Some(mut blocks) = world.get_mut::<MaterialPropertyBlocks>(entity) {
        blocks.slots.retain(|block| &block.slot_id != slot_id);
        remove_component = blocks.slots.is_empty();
    }
    if remove_component {
        world.entity_mut(entity).remove::<MaterialPropertyBlocks>();
    }
}

pub(super) fn property_block_promotion_ui(
    ui: &mut egui::Ui,
    block: Option<&MaterialPropertyBlock>,
    has_project_base: bool,
) -> bool {
    let Some(block) = block else {
        return false;
    };
    ui.separator();
    ui.small(
        egui::RichText::new(format!(
            "Runtime overrides: {} parameters, {} textures",
            block.parameters.len(),
            block.textures.len()
        ))
        .color(TEXT_DIM),
    );
    let clicked = ui
        .add_enabled(
            has_project_base,
            egui::Button::new("Promote to Material Instance"),
        )
        .clicked();
    if !has_project_base {
        ui.small(
            egui::RichText::new("Assign a project Material or Instance first").color(TEXT_MUTED),
        );
    }
    clicked
}

pub(super) fn open_property_block_promotion_review(
    world: &mut World,
    entity: Entity,
    slot_id: ComponentInstanceId,
    slot_name: String,
    base: MaterialRef,
    block: MaterialPropertyBlock,
) {
    if slot_id != block.slot_id {
        if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
            scene_io.status = "the runtime property block targets a different slot".to_string();
        }
        return;
    }
    match plan_property_block_promotion(world, entity, &base, &block, &slot_name) {
        Ok(pending) => {
            let mut review = world.resource_mut::<PropertyBlockPromotionReview>();
            review.pending = Some(pending);
            review.error = None;
        }
        Err(error) => {
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = error;
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PromotionReviewAction {
    Cancel,
    Refresh,
    Commit,
}

pub(crate) fn property_block_promotion_review_ui(world: &mut World, context: &egui::Context) {
    let (pending, error) = {
        let review = world.resource::<PropertyBlockPromotionReview>();
        (review.pending.clone(), review.error.clone())
    };
    let Some(pending) = pending else {
        return;
    };
    let mut action = context
        .input(|input| input.key_pressed(egui::Key::Escape))
        .then_some(PromotionReviewAction::Cancel);
    egui::Window::new("Promote Material Property Block")
        .id(egui::Id::new("property_block_promotion_review"))
        .collapsible(false)
        .resizable(true)
        .default_width(560.0)
        .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
        .show(context, |ui| {
            ui.label(
                "Review the exact reusable Material Instance before publishing the slot assignment.",
            );
            ui.add_space(8.0);
            egui::Grid::new("property_block_promotion_summary")
                .num_columns(2)
                .spacing([12.0, 6.0])
                .show(ui, |ui| {
                    ui.label("Slot");
                    ui.label(format!("{} ({})", pending.slot_name, pending.slot_id.0));
                    ui.end_row();
                    ui.label("Selected base");
                    ui.label(&pending.base.0.label);
                    ui.end_row();
                    ui.label("Direct base");
                    ui.label(&pending.instance.base.0.label);
                    ui.end_row();
                    ui.label("Target");
                    ui.add(
                        egui::Label::new(pending.destination_path.display().to_string()).wrap(),
                    );
                    ui.end_row();
                    ui.label("Sparse overrides");
                    ui.label(format!(
                        "{} parameters, {} textures",
                        pending.instance.overrides.values.len(),
                        pending.instance.overrides.textures.len()
                    ));
                    ui.end_row();
                });
            ui.add_space(8.0);
            ui.small(
                egui::RichText::new(
                    "The runtime block remains active until the file, registry record, and exact scene slot all commit successfully.",
                )
                .color(TEXT_DIM),
            );
            if let Some(error) = error.as_deref() {
                ui.add_space(8.0);
                ui.colored_label(egui::Color32::LIGHT_RED, error);
            }
            ui.add_space(12.0);
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    action = Some(PromotionReviewAction::Cancel);
                }
                if ui.button("Refresh Review").clicked() {
                    action = Some(PromotionReviewAction::Refresh);
                }
                if ui
                    .button("Create Instance and Assign Slot")
                    .clicked()
                {
                    action = Some(PromotionReviewAction::Commit);
                }
            });
        });
    match action {
        Some(PromotionReviewAction::Cancel) => {
            let mut review = world.resource_mut::<PropertyBlockPromotionReview>();
            review.pending = None;
            review.error = None;
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = "Property block promotion cancelled".to_string();
            }
        }
        Some(PromotionReviewAction::Refresh) => match plan_property_block_promotion(
            world,
            pending.entity,
            &pending.base,
            &pending.block,
            &pending.slot_name,
        ) {
            Ok(refreshed) => {
                let mut review = world.resource_mut::<PropertyBlockPromotionReview>();
                review.pending = Some(refreshed);
                review.error = None;
            }
            Err(error) => world.resource_mut::<PropertyBlockPromotionReview>().error = Some(error),
        },
        Some(PromotionReviewAction::Commit) => {
            match commit_property_block_promotion(world, &pending) {
                Ok(path) => {
                    let mut review = world.resource_mut::<PropertyBlockPromotionReview>();
                    review.pending = None;
                    review.error = None;
                    world.resource_mut::<EditorAssets>().refresh();
                    invalidate_on_catalog_refresh(world);
                    if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>()
                    {
                        scene_io.status = format!("Promoted property block to {}", path.display());
                    }
                }
                Err(error) => {
                    world.resource_mut::<PropertyBlockPromotionReview>().error =
                        Some(error.clone());
                    if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>()
                    {
                        scene_io.status = error;
                    }
                }
            }
        }
        None => {}
    }
}
