use super::*;

pub(super) fn build_content_undo_entry(
    project_root: &Path,
    preview: &content_pipeline::ContentTransactionPreview,
) -> Option<ContentUndoEntry> {
    let all_replacements = preview.operations.iter().all(|operation| {
        matches!(
            operation,
            content_pipeline::ContentOperation::ReplaceFile { .. }
        )
    });
    let all_moves = preview
        .operations
        .iter()
        .all(|operation| matches!(operation, content_pipeline::ContentOperation::Move { .. }));
    let (label, action, guarded_paths) = if all_replacements {
        let inverse_operations = preview
            .operations
            .iter()
            .rev()
            .filter_map(|operation| match operation {
                content_pipeline::ContentOperation::ReplaceFile {
                    path,
                    bytes,
                    previous_bytes,
                    ..
                } => Some(content_pipeline::ContentOperation::ReplaceFile {
                    path: path.clone(),
                    bytes: previous_bytes.clone(),
                    expected_fingerprint: content_pipeline::tree_fingerprint(
                        &project_root.join(path),
                    )
                    .ok()?,
                    previous_bytes: bytes.clone(),
                }),
                _ => None,
            })
            .collect::<Vec<_>>();
        if inverse_operations.len() != preview.operations.len() {
            return None;
        }
        let guarded_paths = inverse_operations
            .iter()
            .filter_map(|operation| match operation {
                content_pipeline::ContentOperation::ReplaceFile { path, .. } => Some(path.clone()),
                _ => None,
            })
            .collect::<Vec<_>>();
        (
            format!("Apply {} Material update(s)", inverse_operations.len()),
            ContentUndoAction::Apply(inverse_operations),
            guarded_paths,
        )
    } else if all_moves {
        let inverse_operations: Vec<_> = preview
            .operations
            .iter()
            .rev()
            .filter_map(|operation| match operation {
                content_pipeline::ContentOperation::Move {
                    source,
                    destination,
                } => Some(content_pipeline::ContentOperation::Move {
                    source: destination.clone(),
                    destination: source.clone(),
                }),
                _ => None,
            })
            .collect();
        if inverse_operations.is_empty() {
            return None;
        }
        let guarded_paths = inverse_operations
            .iter()
            .filter_map(|operation| match operation {
                content_pipeline::ContentOperation::Move { source, .. } => Some(source.clone()),
                _ => None,
            })
            .collect::<Vec<_>>();
        (
            format!("Move {} content item(s)", inverse_operations.len()),
            ContentUndoAction::Apply(inverse_operations),
            guarded_paths,
        )
    } else {
        let mut created_paths = Vec::with_capacity(preview.operations.len());
        let mut copy_count = 0;
        let mut folder_count = 0;
        for operation in &preview.operations {
            match operation {
                content_pipeline::ContentOperation::CreateFolder { path } => {
                    folder_count += 1;
                    created_paths.push(path.clone());
                }
                content_pipeline::ContentOperation::Copy { destination, .. } => {
                    copy_count += 1;
                    created_paths.push(destination.clone());
                }
                content_pipeline::ContentOperation::WriteFile { path, .. } => {
                    created_paths.push(path.clone());
                }
                content_pipeline::ContentOperation::ReplaceFile { .. } => return None,
                content_pipeline::ContentOperation::Move { .. } => return None,
            }
        }
        if created_paths.is_empty() {
            return None;
        }
        let label = if copy_count == preview.operations.len() {
            format!("Copy {copy_count} content item(s)")
        } else if folder_count == preview.operations.len() {
            format!("Create {folder_count} folder(s)")
        } else {
            format!("Create {} content item(s)", created_paths.len())
        };
        (
            label,
            ContentUndoAction::TrashCreated(created_paths.clone()),
            created_paths,
        )
    };
    let guarded_sources = guarded_paths
        .into_iter()
        .filter_map(|path| {
            content_pipeline::tree_fingerprint(&project_root.join(&path))
                .ok()
                .map(|fingerprint| (path, fingerprint))
        })
        .collect::<Vec<_>>();
    let expected_guard_count = match &action {
        ContentUndoAction::Apply(operations) => operations.len(),
        ContentUndoAction::TrashCreated(paths) => paths.len(),
        ContentUndoAction::RestoreTrash(_) => 1,
    };
    (guarded_sources.len() == expected_guard_count).then_some(ContentUndoEntry {
        label,
        action,
        guarded_sources,
    })
}

pub(super) fn undo_last_content_operation(world: &mut World) {
    let Some(entry) = world
        .resource::<AssetBrowserUiState>()
        .content_undo
        .last()
        .cloned()
    else {
        return;
    };
    if let Some(path) = first_changed_content_undo_source(Path::new("."), &entry) {
        world.resource_mut::<SceneIo>().status = format!(
            "Cannot undo {}: {} changed outside the transaction",
            entry.label,
            path.display()
        );
        return;
    }
    let undone = match &entry.action {
        ContentUndoAction::Apply(operations) => {
            commit_content_operations_internal(world, operations.clone(), false)
        }
        ContentUndoAction::TrashCreated(paths) => trash_created_content_for_undo(world, paths),
        ContentUndoAction::RestoreTrash(batch_path) => {
            restore_trashed_content_for_undo(world, batch_path)
        }
    };
    if undone {
        world
            .resource_mut::<AssetBrowserUiState>()
            .content_undo
            .pop();
        world.resource_mut::<SceneIo>().status = format!("Undid {}", entry.label);
    }
}

pub(super) fn first_changed_content_undo_source(
    project_root: &Path,
    entry: &ContentUndoEntry,
) -> Option<PathBuf> {
    entry.guarded_sources.iter().find_map(|(path, expected)| {
        let actual = content_pipeline::tree_fingerprint(&project_root.join(path));
        (actual.as_deref() != Ok(expected.as_str())).then(|| path.clone())
    })
}

pub(super) fn trash_created_content_for_undo(world: &mut World, paths: &[PathBuf]) -> bool {
    let mut document = world
        .resource::<crate::asset_db::AssetRegistry>()
        .document();
    crate::assets::begin_content_watch_transaction(world);
    let trash_result = content_pipeline::trash_content(Path::new("."), paths, &mut document);
    crate::assets::end_content_watch_transaction(world);
    if let Err(error) = trash_result {
        world.resource_mut::<SceneIo>().status = error;
        return false;
    }
    apply_registry_document(world, document);
    let current_folder = world.resource::<EditorAssets>().current_folder.clone();
    {
        let mut assets = world.resource_mut::<EditorAssets>();
        assets.clear_selection();
        assets.refresh();
        if !Path::new(&current_folder).is_dir() {
            assets.current_folder = nearest_existing_content_folder(&current_folder);
        }
    }
    invalidate_on_catalog_refresh(world);
    true
}

pub(super) fn restore_trashed_content_for_undo(world: &mut World, batch_path: &Path) -> bool {
    let mut document = world
        .resource::<crate::asset_db::AssetRegistry>()
        .document();
    crate::assets::begin_content_watch_transaction(world);
    let restore_result =
        content_pipeline::restore_trash_batch(Path::new("."), batch_path, &mut document);
    crate::assets::end_content_watch_transaction(world);
    let restored = match restore_result {
        Ok(restored) => restored,
        Err(error) => {
            world.resource_mut::<SceneIo>().status = error;
            return false;
        }
    };
    apply_registry_document(world, document);
    let selections = restored
        .original_paths
        .iter()
        .map(|path| {
            if Path::new(path).is_dir() {
                AssetSelection::Folder(path.clone())
            } else {
                AssetSelection::File(path.clone())
            }
        })
        .collect::<Vec<_>>();
    {
        let mut assets = world.resource_mut::<EditorAssets>();
        assets.refresh();
        assets.select_all(&selections);
    }
    invalidate_on_catalog_refresh(world);
    true
}

pub(super) fn nearest_existing_content_folder(folder: &str) -> String {
    let mut candidate = Path::new(folder);
    loop {
        if candidate.is_dir() && candidate.starts_with(ASSETS_ROOT) {
            return candidate.to_string_lossy().replace('\\', "/");
        }
        let Some(parent) = candidate.parent() else {
            return ASSETS_ROOT.into();
        };
        candidate = parent;
    }
}

pub(super) fn request_delete_for_asset(world: &mut World, asset: &EditorAsset) {
    let selection = AssetSelection::from_asset(asset);
    if !world.resource::<EditorAssets>().is_selected(&selection) {
        world.resource_mut::<EditorAssets>().select(selection);
    }
    request_delete_for_selection(world);
}

pub(super) fn request_delete_for_selection(world: &mut World) {
    if let Some(request) = build_delete_request(world) {
        world.resource_mut::<AssetBrowserUiState>().pending_delete = Some(request);
    }
}

pub(super) fn external_move_choices_valid(
    review: &crate::assets::ExternalMoveRepairReview,
) -> bool {
    if review.choices.len() != review.conflicts.len() || review.choices.iter().any(Option::is_none)
    {
        return false;
    }
    let mut preserved = HashSet::new();
    review
        .conflicts
        .iter()
        .zip(&review.choices)
        .all(|(conflict, choice)| match choice {
            Some(ExternalMoveRepairChoice::Preserve(path)) => {
                conflict.candidate_paths.contains(path) && preserved.insert(path)
            }
            Some(ExternalMoveRepairChoice::RegisterNew) => true,
            None => false,
        })
}

pub(super) fn draw_external_move_repair_modal(world: &mut World, ctx: &egui::Context) {
    let Some(mut review) = world
        .get_resource::<PendingExternalMoveRepair>()
        .and_then(|pending| pending.review.clone())
        .filter(|review| review.open)
    else {
        return;
    };
    let mut action = None;
    egui::Window::new("Resolve Ambiguous External Moves")
        .collapsible(false)
        .resizable(true)
        .default_width(620.0)
        .show(ctx, |ui| {
            ui.label(
                "The same imported bytes match multiple missing registry records. Choose which stable identity moved to each new path.",
            );
            ui.small(
                egui::RichText::new(
                    "Register as New intentionally assigns a fresh ID. No registry or manifest changes are published until every row is resolved.",
                )
                .color(TEXT_DIM),
            );
            ui.separator();
            egui::ScrollArea::vertical()
                .id_salt("external_move_repair_rows")
                .max_height(420.0)
                .auto_shrink([false, true])
                .show(ui, |ui| {
                    for (index, conflict) in review.conflicts.iter().enumerate() {
                        ui.group(|ui| {
                            ui.label(egui::RichText::new(&conflict.new_path).strong());
                            ui.small(format!(
                                "{:?} · {} candidate identities",
                                conflict.kind,
                                conflict.candidate_paths.len()
                            ));
                            let selected_text = match review.choices.get(index).and_then(Option::as_ref) {
                                Some(ExternalMoveRepairChoice::Preserve(path)) => {
                                    format!("Preserve ID from {path}")
                                }
                                Some(ExternalMoveRepairChoice::RegisterNew) => {
                                    "Register as New Asset".into()
                                }
                                None => "Choose stable identity…".into(),
                            };
                            egui::ComboBox::from_id_salt(("external_move_choice", index))
                                .selected_text(selected_text)
                                .width(ui.available_width().max(240.0))
                                .show_ui(ui, |ui| {
                                    for candidate in &conflict.candidate_paths {
                                        let used_elsewhere = review
                                            .choices
                                            .iter()
                                            .enumerate()
                                            .any(|(other_index, choice)| {
                                                other_index != index
                                                    && choice.as_ref()
                                                        == Some(&ExternalMoveRepairChoice::Preserve(
                                                            candidate.clone(),
                                                        ))
                                            });
                                        ui.add_enabled_ui(!used_elsewhere, |ui| {
                                            ui.selectable_value(
                                                &mut review.choices[index],
                                                Some(ExternalMoveRepairChoice::Preserve(
                                                    candidate.clone(),
                                                )),
                                                format!("Preserve ID from {candidate}"),
                                            );
                                        });
                                    }
                                    ui.separator();
                                    ui.selectable_value(
                                        &mut review.choices[index],
                                        Some(ExternalMoveRepairChoice::RegisterNew),
                                        "Register as New Asset",
                                    );
                                });
                        });
                        ui.add_space(6.0);
                    }
                });
            ui.separator();
            ui.horizontal(|ui| {
                if ui.button("Review Later").clicked() {
                    action = Some(false);
                }
                if ui
                    .add_enabled(
                        external_move_choices_valid(&review),
                        egui::Button::new("Apply Identity Repairs"),
                    )
                    .clicked()
                {
                    action = Some(true);
                }
            });
        });

    match action {
        Some(false) => review.open = false,
        Some(true) => {}
        None => {}
    }
    world.resource_mut::<PendingExternalMoveRepair>().review = Some(review);
    if action == Some(true) {
        if let Err(error) = crate::assets::commit_external_move_repairs(world) {
            world.resource_mut::<SceneIo>().status =
                format!("External move repair failed: {error}");
        }
    }
}

pub(super) fn build_delete_request(world: &World) -> Option<AssetDeleteRequest> {
    let assets = world.resource::<EditorAssets>();
    let selections: Vec<AssetSelection> = assets
        .selections
        .iter()
        .filter(|selection| {
            authored_content_path(selection)
                .is_some_and(|path| !matches!(path, ASSETS_ROOT | BUILTINS_FOLDER))
        })
        .cloned()
        .collect();
    if selections.is_empty() {
        return None;
    }
    let selected_paths: Vec<String> = selected_content_paths(world);
    let mut files = selected_paths.clone();
    let mut warnings = Vec::new();
    for selection in &selections {
        let Some(asset) = assets.asset_for_selection(selection) else {
            continue;
        };
        let Some(path) = asset.path.as_deref() else {
            continue;
        };
        if !matches!(asset.kind, EditorAssetKind::Model) {
            continue;
        }
        if let Some(record) = world
            .get_resource::<crate::asset_db::AssetRegistry>()
            .and_then(|registry| find_asset_by_path(registry, path))
        {
            let settings = record.model_import();
            if let Some(manifest) = settings.static_mesh_manifest_path.clone() {
                files.push(manifest);
            }
            if let Some(manifest) = settings.animation_manifest_path.clone() {
                files.push(manifest);
            }
            if !record.dependencies.is_empty() {
                warnings.push(format!(
                    "{} imported dependencies will be kept because they may be shared.",
                    record.dependencies.len()
                ));
            }
        }
    }
    if let Some(registry) = world.get_resource::<crate::asset_db::AssetRegistry>() {
        let sources = selected_paths.iter().map(PathBuf::from).collect::<Vec<_>>();
        match content_pipeline::find_reference_usages(
            Path::new("."),
            &sources,
            &registry.document(),
        ) {
            Ok(usages) if !usages.is_empty() => {
                warnings.push(format!(
                    "{} external reference(s) will become unresolved:",
                    usages.len()
                ));
                warnings.extend(
                    usages
                        .into_iter()
                        .map(|usage| format!("{} — {}", usage.document.display(), usage.reference)),
                );
            }
            Ok(_) => {}
            Err(error) => warnings.push(format!(
                "Reference usage scan failed; review could be incomplete: {error}"
            )),
        }
    }
    dedup_strings(&mut files);
    let label = if selections.len() == 1 {
        selections[0].display_label().to_string()
    } else {
        format!("{} selected items", selections.len())
    };
    Some(AssetDeleteRequest {
        label,
        files,
        warnings,
    })
}

pub(super) fn draw_delete_modal(world: &mut World, ctx: &egui::Context) {
    let pending = world
        .resource::<AssetBrowserUiState>()
        .pending_delete
        .clone();
    let Some(request) = pending else {
        return;
    };
    let mut action = None;
    egui::Window::new("Move Content To Trash")
        .collapsible(false)
        .resizable(false)
        .default_width(360.0)
        .show(ctx, |ui| {
            ui.label(format!("Move {} to assets/.trash?", request.label));
            ui.separator();
            ui.label(panel_heading("Files"));
            for file in &request.files {
                ui.add(egui::Label::new(file).wrap());
            }
            if !request.warnings.is_empty() {
                ui.separator();
                ui.label(panel_heading("Warnings"));
                for warning in &request.warnings {
                    ui.small(egui::RichText::new(warning).color(TEXT_DIM));
                }
            }
            ui.separator();
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    action = Some(false);
                }
                if ui.button("Move To Trash").clicked() {
                    action = Some(true);
                }
            });
        });

    match action {
        Some(false) => {
            world.resource_mut::<AssetBrowserUiState>().pending_delete = None;
        }
        Some(true) => {
            execute_delete_request(world, &request);
            world.resource_mut::<AssetBrowserUiState>().pending_delete = None;
        }
        None => {}
    }
}

pub(super) fn draw_trash_modal(world: &mut World, ctx: &egui::Context) {
    if !world.resource::<AssetBrowserUiState>().show_trash {
        return;
    }
    let batches = content_pipeline::list_trash_batches(Path::new("."));
    let mut close = false;
    let mut restore = None;
    egui::Window::new("Content Trash")
        .collapsible(false)
        .resizable(true)
        .default_width(520.0)
        .min_width(380.0)
        .show(ctx, |ui| {
            ui.label("Restore a complete transaction to its original project paths.");
            ui.small(
                egui::RichText::new(
                    "Restore is blocked when a destination or stable registry ID is already in use.",
                )
                .color(TEXT_DIM),
            );
            ui.separator();
            match &batches {
                Err(error) => {
                    ui.label(egui::RichText::new(error).color(ERROR));
                }
                Ok(batches) if batches.is_empty() => {
                    ui.label(egui::RichText::new("Trash is empty.").color(TEXT_DIM));
                }
                Ok(batches) => {
                    egui::ScrollArea::vertical()
                        .max_height(420.0)
                        .show(ui, |ui| {
                            for batch in batches {
                                egui::Frame::group(ui.style()).show(ui, |ui| {
                                    ui.horizontal(|ui| {
                                        ui.strong(&batch.batch_id);
                                        ui.with_layout(
                                            egui::Layout::right_to_left(egui::Align::Center),
                                            |ui| {
                                                if ui.button("Restore").clicked() {
                                                    restore = Some(batch.clone());
                                                }
                                            },
                                        );
                                    });
                                    for path in &batch.original_paths {
                                        ui.small(path);
                                    }
                                });
                                ui.add_space(4.0);
                            }
                        });
                }
            }
            ui.separator();
            if ui.button("Close").clicked() {
                close = true;
            }
        });

    if let Some(batch) = restore {
        let mut document = world
            .resource::<crate::asset_db::AssetRegistry>()
            .document();
        crate::assets::begin_content_watch_transaction(world);
        let restore_result =
            content_pipeline::restore_trash_batch(Path::new("."), &batch.path, &mut document);
        crate::assets::end_content_watch_transaction(world);
        match restore_result {
            Ok(restored) => {
                apply_registry_document(world, document);
                forget_restored_trash_undo(
                    &mut world.resource_mut::<AssetBrowserUiState>(),
                    &batch.path,
                );
                let selections = restored
                    .original_paths
                    .iter()
                    .map(|path| {
                        if Path::new(path).is_dir() {
                            AssetSelection::Folder(path.clone())
                        } else {
                            AssetSelection::File(path.clone())
                        }
                    })
                    .collect::<Vec<_>>();
                {
                    let mut assets = world.resource_mut::<EditorAssets>();
                    assets.refresh();
                    assets.select_all(&selections);
                }
                invalidate_on_catalog_refresh(world);
                world.resource_mut::<SceneIo>().status = format!(
                    "Restored {} content item(s) from Trash",
                    restored.original_paths.len()
                );
            }
            Err(error) => world.resource_mut::<SceneIo>().status = error,
        }
    }
    if close {
        world.resource_mut::<AssetBrowserUiState>().show_trash = false;
    }
}

pub(super) fn forget_restored_trash_undo(state: &mut AssetBrowserUiState, batch_path: &Path) {
    state.content_undo.retain(|entry| {
        !matches!(
            &entry.action,
            ContentUndoAction::RestoreTrash(path) if path == batch_path
        )
    });
}

pub(super) fn execute_delete_request(world: &mut World, request: &AssetDeleteRequest) {
    let paths = request
        .files
        .iter()
        .map(PathBuf::from)
        .filter(|path| path.exists())
        .collect::<Vec<_>>();
    let mut document = world
        .resource::<crate::asset_db::AssetRegistry>()
        .document();
    crate::assets::begin_content_watch_transaction(world);
    let trash_result = content_pipeline::trash_content(Path::new("."), &paths, &mut document);
    crate::assets::end_content_watch_transaction(world);
    let batch = match trash_result {
        Ok(batch) => batch,
        Err(error) => {
            world.resource_mut::<SceneIo>().status = error;
            return;
        }
    };
    apply_registry_document(world, document);

    if let Ok(fingerprint) = content_pipeline::tree_fingerprint(&batch.path) {
        push_content_undo(
            world,
            ContentUndoEntry {
                label: format!("Move {} to Trash", request.label),
                action: ContentUndoAction::RestoreTrash(batch.path.clone()),
                guarded_sources: vec![(batch.path.clone(), fingerprint)],
            },
        );
    }

    {
        let mut assets = world.resource_mut::<EditorAssets>();
        assets.clear_selection();
        assets.refresh();
    }
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = format!(
        "Moved {} file(s) for {} to {} (restorable from Trash)",
        paths.len(),
        request.label,
        batch.path.to_string_lossy()
    );
}

pub(super) fn push_content_undo(world: &mut World, entry: ContentUndoEntry) {
    let mut state = world.resource_mut::<AssetBrowserUiState>();
    state.content_undo.push(entry);
    if state.content_undo.len() > 32 {
        state.content_undo.remove(0);
    }
}

pub(super) fn apply_registry_document(world: &mut World, document: shared::AssetRegistryDocument) {
    let mut registry = world.resource_mut::<crate::asset_db::AssetRegistry>();
    registry.schema_version = document.schema_version;
    registry.defaults = document.defaults;
    registry.records = document.records;
    registry.index_dirty = false;
    registry.migration_required = false;
}

pub(super) fn dedup_strings(values: &mut Vec<String>) {
    let mut seen = HashSet::new();
    values.retain(|value| seen.insert(value.clone()));
}
