use super::*;

pub(crate) fn begin_gltf_material_extraction(world: &mut World, source: &str) {
    begin_model_material_extraction(world, source, None);
}

pub(crate) fn begin_actor_material_extraction(
    world: &mut World,
    source: &str,
    entity: Entity,
    slot_id: shared::ComponentInstanceId,
    source_sub_asset_id: String,
) {
    begin_model_material_extraction(
        world,
        source,
        Some(GltfMaterialExtractionTarget {
            entity,
            slot_id,
            source_sub_asset_id,
        }),
    );
}

pub(super) fn begin_model_material_extraction(
    world: &mut World,
    source: &str,
    actor_target: Option<GltfMaterialExtractionTarget>,
) {
    let destination = world.resource::<EditorAssets>().current_folder.clone();
    match crate::assets::plan_model_material_extraction(Path::new(source), Path::new(&destination))
    {
        Ok(materials) if materials.is_empty() => {
            world.resource_mut::<SceneIo>().status =
                format!("No convertible source materials were found in {source}");
        }
        Ok(materials) => {
            let materials = materials
                .into_iter()
                .map(|material| {
                    let included = actor_target.as_ref().is_none_or(|target| {
                        material
                            .asset
                            .provenance
                            .as_ref()
                            .is_some_and(|provenance| {
                                provenance.source_sub_asset_id == target.source_sub_asset_id
                            })
                    });
                    let path = material
                        .asset
                        .provenance
                        .as_ref()
                        .and_then(|provenance| {
                            crate::assets::find_matching_extracted_material(
                                Path::new(&destination),
                                Path::new(source),
                                &provenance.source_sub_asset_id,
                            )
                        })
                        .unwrap_or(material.path);
                    GltfMaterialExtractionEntry {
                        included,
                        source_index: material.source_index,
                        source_name: material.source_name,
                        write_mode: if path.exists() {
                            MaterialExtractionWriteMode::Undecided
                        } else {
                            MaterialExtractionWriteMode::CreateNew
                        },
                        path: path.to_string_lossy().replace('\\', "/"),
                        asset: material.asset,
                    }
                })
                .collect::<Vec<_>>();
            if actor_target.is_some() && !materials.iter().any(|material| material.included) {
                world.resource_mut::<SceneIo>().status =
                    "The selected imported material could not be matched for extraction".into();
                return;
            }
            world
                .resource_mut::<AssetBrowserUiState>()
                .pending_gltf_extraction = Some(GltfMaterialExtractionDraft {
                source: source.to_string(),
                destination,
                materials,
                actor_target,
            });
        }
        Err(error) => {
            world.resource_mut::<SceneIo>().status =
                format!("Material extraction review failed: {error}");
        }
    }
}

pub(super) fn draw_gltf_material_extraction_modal(world: &mut World, ctx: &egui::Context) {
    let Some(mut draft) = world
        .resource::<AssetBrowserUiState>()
        .pending_gltf_extraction
        .clone()
    else {
        return;
    };
    let mut cancel = false;
    let mut extract = false;
    let validation_error = gltf_extraction_validation_error(&draft);
    let included = draft
        .materials
        .iter()
        .filter(|material| material.included)
        .count();

    egui::Window::new("Extract Editable Model Materials")
        .collapsible(false)
        .resizable(true)
        .default_width(880.0)
        .min_width(680.0)
        .show(ctx, |ui| {
            ui.label(format!("Source: {}", draft.source));
            ui.label(format!("Destination: {}", draft.destination));
            ui.small(
                egui::RichText::new(
                    "Review converted PBR values, texture references, final project paths, and any provenance-matched diff. Existing files require an explicit guarded Apply or Create New decision.",
                )
                .color(TEXT_DIM),
            );
            ui.separator();
            egui::ScrollArea::vertical()
                .max_height(420.0)
                .show(ui, |ui| {
                    for material in &mut draft.materials {
                        ui.push_id(material.source_index, |ui| {
                            egui::Frame::group(ui.style()).show(ui, |ui| {
                                ui.horizontal(|ui| {
                                    ui.checkbox(&mut material.included, "");
                                    ui.strong(format!(
                                        "{}  ·  source material {}",
                                        material.source_name, material.source_index
                                    ));
                                });
                                ui.horizontal(|ui| {
                                    ui.label("Project path");
                                    let changed = ui
                                        .add(
                                        egui::TextEdit::singleline(&mut material.path)
                                            .desired_width(f32::INFINITY),
                                    )
                                        .changed();
                                    if changed {
                                        material.write_mode = if Path::new(material.path.trim()).exists() {
                                            MaterialExtractionWriteMode::Undecided
                                        } else {
                                            MaterialExtractionWriteMode::CreateNew
                                        };
                                    }
                                });
                                let resolved = material_asset_desc(&material.asset);
                                ui.small(format!(
                                    "Metallic {:.3}  ·  Roughness {:.3}  ·  {}",
                                    resolved.metallic,
                                    resolved.roughness,
                                    gltf_material_texture_summary(&material.asset)
                                ));
                                ui.small(format!(
                                    "Alpha {:?}  ·  Double sided {}  ·  provenance {}",
                                    material.asset.render_state.alpha_mode,
                                    material.asset.render_state.double_sided,
                                    material
                                        .asset
                                        .provenance
                                        .as_ref()
                                        .map(|provenance| provenance.source_sub_asset_id.as_str())
                                        .unwrap_or("missing")
                                ));
                                match review_existing_extraction_target(material) {
                                    Ok(Some(review)) if review.provenance_matches => {
                                        ui.separator();
                                        ui.strong("Existing extracted Material found");
                                        if review.changes.is_empty() {
                                            ui.small("Diff: converted values are unchanged; applying refreshes source provenance.");
                                        } else {
                                            ui.small(format!("Diff: {}", review.changes.join(" · ")));
                                        }
                                        ui.horizontal(|ui| {
                                            let selected = matches!(
                                                &material.write_mode,
                                                MaterialExtractionWriteMode::ApplyExisting {
                                                    expected_fingerprint,
                                                    ..
                                                } if expected_fingerprint == &review.fingerprint
                                            );
                                            if ui
                                                .selectable_label(selected, "Apply Reviewed Update")
                                                .on_hover_text(
                                                    "Replace this Material transactionally only if its reviewed bytes have not changed.",
                                                )
                                                .clicked()
                                            {
                                                material.write_mode =
                                                    MaterialExtractionWriteMode::ApplyExisting {
                                                        expected_fingerprint: review.fingerprint,
                                                        previous_bytes: review.bytes,
                                                    };
                                            }
                                            if ui.button("Create New Copy").clicked() {
                                                material.path = unique_extraction_copy_path(
                                                    Path::new(material.path.trim()),
                                                )
                                                .to_string_lossy()
                                                .replace('\\', "/");
                                                material.write_mode =
                                                    MaterialExtractionWriteMode::CreateNew;
                                            }
                                        });
                                    }
                                    Ok(Some(_)) => {
                                        ui.small(
                                            egui::RichText::new(
                                                "Existing Material provenance does not match this source slot; Apply is blocked.",
                                            )
                                            .color(WARNING),
                                        );
                                        if ui.button("Create New Copy").clicked() {
                                            material.path = unique_extraction_copy_path(
                                                Path::new(material.path.trim()),
                                            )
                                            .to_string_lossy()
                                            .replace('\\', "/");
                                            material.write_mode =
                                                MaterialExtractionWriteMode::CreateNew;
                                        }
                                    }
                                    Ok(None) => {
                                        material.write_mode = MaterialExtractionWriteMode::CreateNew;
                                        ui.small("Create new editable project Material");
                                    }
                                    Err(error) => {
                                        ui.small(egui::RichText::new(error).color(WARNING));
                                    }
                                }
                            });
                            ui.add_space(4.0);
                        });
                    }
                });
            ui.separator();
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    cancel = true;
                }
                if ui
                    .add_enabled(
                        validation_error.is_none(),
                        egui::Button::new(format!("Extract {included} Material(s)")),
                    )
                    .clicked()
                {
                    extract = true;
                }
            });
            if let Some(error) = validation_error.as_deref() {
                ui.small(egui::RichText::new(error).color(WARNING));
            }
        });

    if cancel {
        world
            .resource_mut::<AssetBrowserUiState>()
            .pending_gltf_extraction = None;
    } else if extract {
        match commit_gltf_material_extraction(world, &draft) {
            Ok((count, mapped_slots, warning)) => {
                world
                    .resource_mut::<AssetBrowserUiState>()
                    .pending_gltf_extraction = None;
                let scope = if draft.actor_target.is_some() {
                    "actor slot"
                } else {
                    "model slot"
                };
                let mut status = format!(
                    "Extracted {count} editable model material(s) into {} and assigned {mapped_slots} {scope}(s)",
                    draft.destination
                );
                if let Some(warning) = warning {
                    status.push_str(&format!("; {warning}"));
                }
                world.resource_mut::<SceneIo>().status = status;
            }
            Err(error) => {
                world.resource_mut::<SceneIo>().status = error;
                world
                    .resource_mut::<AssetBrowserUiState>()
                    .pending_gltf_extraction = Some(draft);
            }
        }
    } else {
        world
            .resource_mut::<AssetBrowserUiState>()
            .pending_gltf_extraction = Some(draft);
    }
}

pub(super) fn gltf_extraction_validation_error(
    draft: &GltfMaterialExtractionDraft,
) -> Option<String> {
    let selected = draft
        .materials
        .iter()
        .filter(|material| material.included)
        .collect::<Vec<_>>();
    if selected.is_empty() {
        return Some("Select at least one source material to extract.".into());
    }
    let mut paths = HashSet::new();
    for material in selected {
        let path = Path::new(material.path.trim());
        if let Err(error) = content_pipeline::validate_asset_path(path) {
            return Some(format!("Invalid project path {}: {error}", path.display()));
        }
        if path.extension().and_then(|extension| extension.to_str()) != Some("ron") {
            return Some(format!(
                "Material path must end in .ron: {}",
                path.display()
            ));
        }
        let normalized = material.path.trim().replace('\\', "/");
        if !paths.insert(normalized) {
            return Some("Two selected materials have the same project path.".into());
        }
        match &material.write_mode {
            MaterialExtractionWriteMode::Undecided => {
                return Some(format!(
                    "Review the existing Material diff and choose Apply or Create New: {}",
                    path.display()
                ));
            }
            MaterialExtractionWriteMode::CreateNew if path.exists() => {
                return Some(format!("Project path already exists: {}", path.display()));
            }
            MaterialExtractionWriteMode::ApplyExisting {
                expected_fingerprint,
                previous_bytes,
            } => match review_existing_extraction_target(material) {
                Ok(Some(review))
                    if review.provenance_matches
                        && review.fingerprint == *expected_fingerprint
                        && review.bytes == *previous_bytes => {}
                Ok(Some(_)) => {
                    return Some(format!(
                        "Existing Material changed or has different provenance: {}",
                        path.display()
                    ));
                }
                Ok(None) => {
                    return Some(format!("Apply target no longer exists: {}", path.display()));
                }
                Err(error) => return Some(error),
            },
            MaterialExtractionWriteMode::CreateNew => {}
        }
    }
    None
}

pub(super) struct ExistingExtractionReview {
    pub(super) fingerprint: String,
    pub(super) bytes: Vec<u8>,
    pub(super) provenance_matches: bool,
    pub(super) changes: Vec<&'static str>,
}

pub(super) fn review_existing_extraction_target(
    material: &GltfMaterialExtractionEntry,
) -> Result<Option<ExistingExtractionReview>, String> {
    let path = Path::new(material.path.trim());
    if !path.exists() {
        return Ok(None);
    }
    let bytes = fs::read(path).map_err(|error| {
        format!(
            "Could not read existing Material {}: {error}",
            path.display()
        )
    })?;
    let existing: MaterialAsset = ron::de::from_bytes(&bytes)
        .map_err(|error| format!("Existing target is not a valid Material: {error}"))?;
    let fingerprint = content_pipeline::tree_fingerprint(path)?;
    let provenance_matches = existing
        .provenance
        .as_ref()
        .zip(material.asset.provenance.as_ref())
        .is_some_and(|(existing, incoming)| {
            existing.source_path == incoming.source_path
                && existing.source_sub_asset_id == incoming.source_sub_asset_id
        });
    let mut changes = Vec::new();
    if existing.label != material.asset.label {
        changes.push("label");
    }
    if existing.shader != material.asset.shader || existing.shader_ref != material.asset.shader_ref
    {
        changes.push("shader selection");
    }
    if existing.render_state != material.asset.render_state {
        changes.push("render state");
    }
    if existing.inputs != material.asset.inputs {
        changes.push("PBR values or textures");
    }
    if existing
        .provenance
        .as_ref()
        .map(|value| &value.source_fingerprint)
        != material
            .asset
            .provenance
            .as_ref()
            .map(|value| &value.source_fingerprint)
    {
        changes.push("source revision");
    }
    Ok(Some(ExistingExtractionReview {
        fingerprint,
        bytes,
        provenance_matches,
        changes,
    }))
}

pub(super) fn unique_extraction_copy_path(path: &Path) -> PathBuf {
    let parent = path.parent().unwrap_or_else(|| Path::new("assets"));
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("material.material.ron");
    let stem = file_name
        .strip_suffix(".material.ron")
        .or_else(|| file_name.strip_suffix(".ron"))
        .unwrap_or(file_name);
    (2..)
        .map(|suffix| parent.join(format!("{stem}_{suffix}.material.ron")))
        .find(|candidate| !candidate.exists())
        .expect("numeric material suffix space is effectively unbounded")
}

pub(super) fn gltf_material_texture_summary(asset: &MaterialAsset) -> String {
    let mut textures = Vec::new();
    if asset
        .inputs
        .texture("base_color")
        .is_some_and(|value| value.texture.is_some())
    {
        textures.push("base color");
    }
    if asset
        .inputs
        .texture("normal")
        .is_some_and(|value| value.texture.is_some())
    {
        textures.push("normal");
    }
    if asset
        .inputs
        .texture("roughness")
        .is_some_and(|value| value.texture.is_some())
        || asset
            .inputs
            .texture("metallic")
            .is_some_and(|value| value.texture.is_some())
    {
        textures.push("metal/rough");
    }
    if asset
        .inputs
        .texture("emissive")
        .is_some_and(|value| value.texture.is_some())
    {
        textures.push("emissive");
    }
    if textures.is_empty() {
        "no converted textures".into()
    } else {
        format!("textures: {}", textures.join(", "))
    }
}

pub(super) fn material_asset_desc(asset: &MaterialAsset) -> MaterialDesc {
    let mut material = MaterialDesc {
        shader: asset.shader.clone(),
        ..Default::default()
    };
    asset.inputs.apply_to_material_desc(&mut material);
    material
}

pub(super) fn commit_gltf_material_extraction(
    world: &mut World,
    draft: &GltfMaterialExtractionDraft,
) -> Result<(usize, usize, Option<String>), String> {
    if let Some(error) = gltf_extraction_validation_error(draft) {
        return Err(error);
    }
    let included = draft
        .materials
        .iter()
        .filter(|material| material.included)
        .cloned()
        .collect::<Vec<_>>();
    let operations = included
        .iter()
        .map(|material| {
            ron::ser::to_string_pretty(&material.asset, ron::ser::PrettyConfig::default())
                .map(|ron| {
                    let path = PathBuf::from(material.path.trim());
                    let bytes = format!("{ron}\n").into_bytes();
                    match &material.write_mode {
                        MaterialExtractionWriteMode::CreateNew => {
                            content_pipeline::ContentOperation::WriteFile { path, bytes }
                        }
                        MaterialExtractionWriteMode::ApplyExisting {
                            expected_fingerprint,
                            previous_bytes,
                        } => content_pipeline::ContentOperation::ReplaceFile {
                            path,
                            bytes,
                            expected_fingerprint: expected_fingerprint.clone(),
                            previous_bytes: previous_bytes.clone(),
                        },
                        MaterialExtractionWriteMode::Undecided => {
                            unreachable!("extraction validation rejects undecided existing targets")
                        }
                    }
                })
                .map_err(|error| format!("Material extraction serialization failed: {error}"))
        })
        .collect::<Result<Vec<_>, _>>()?;
    let count = operations.len();
    let manifest = world
        .resource::<AssetRegistry>()
        .records
        .iter()
        .find(|record| record.path == draft.source)
        .ok_or_else(|| format!("Model is not registered: {}", draft.source))?
        .model_import()
        .static_mesh_manifest_path
        .as_deref()
        .ok_or_else(|| format!("Model has no generated mesh manifest: {}", draft.source))
        .and_then(load_static_mesh_manifest)?;
    let bindings = included
        .iter()
        .map(|material| ExtractedMaterialBinding {
            source_index: material.source_index,
            path: material.path.trim().replace('\\', "/"),
        })
        .collect::<Vec<_>>();
    if let Some(target) = draft.actor_target.as_ref() {
        validate_actor_material_extraction_target(world, target)?;
    }
    let mapped_slots = std::cell::Cell::new(0);
    let committed = commit_content_operations_internal_with_registry_update(
        world,
        operations,
        true,
        |document| {
            if draft.actor_target.is_none() {
                mapped_slots.set(bind_extracted_material_slots(
                    document,
                    &draft.source,
                    &manifest,
                    &bindings,
                )?);
            }
            Ok(())
        },
    );
    if !committed {
        return Err(world.resource::<SceneIo>().status.clone());
    }

    if let Some(target) = draft.actor_target.as_ref() {
        let binding = included
            .iter()
            .find(|material| {
                material
                    .asset
                    .provenance
                    .as_ref()
                    .is_some_and(|provenance| {
                        provenance.source_sub_asset_id == target.source_sub_asset_id
                    })
            })
            .ok_or_else(|| "Selected source material was not included in extraction".to_string())?;
        let path = binding.path.trim().replace('\\', "/");
        let record = world
            .resource::<AssetRegistry>()
            .records
            .iter()
            .find(|record| record.path == path)
            .ok_or_else(|| format!("Extracted Material was not registered: {path}"))?;
        let reference = MaterialRef::new(
            EditorAssetRef::new(
                record.id.as_string(),
                "material:source",
                record.label.clone(),
            )
            .with_source_path(path),
        );
        assign_extracted_material_to_actor_slot(world, target, reference)?;
        mapped_slots.set(1);
    }

    let warning = if draft.actor_target.is_none() {
        refresh_extracted_model_artifacts(world, &draft.source).err()
    } else {
        None
    };
    Ok((count, mapped_slots.get(), warning))
}

pub(super) fn validate_actor_material_extraction_target(
    world: &World,
    target: &GltfMaterialExtractionTarget,
) -> Result<(), String> {
    let exists = world
        .get::<shared::Primitive>(target.entity)
        .is_some_and(|primitive| primitive.surface.id == target.slot_id)
        || world
            .get::<shared::StaticMeshRenderer>(target.entity)
            .is_some_and(|renderer| renderer.materials.slot(&target.slot_id).is_some())
        || world
            .get::<shared::SkinnedMeshRenderer>(target.entity)
            .is_some_and(|renderer| renderer.materials.slot(&target.slot_id).is_some());
    if exists {
        Ok(())
    } else {
        Err("The actor or exact material slot changed while extraction was open".into())
    }
}
