use crate::ecs::EditorWorld;
use crate::project::ProjectState;
use crate::scene_writeback::{sync_hdr_skybox, sync_settings};
use crate::systems::loading;
#[cfg(target_arch = "wasm32")]
use nightshade::ecs::scene::commands::save_scene_binary_to_bytes;
use nightshade::ecs::scene::commands::{load_scene_binary_from_bytes, world_to_scene_with_uuids};
use nightshade::ecs::scene::{Scene, SceneHdrSkybox, spawn_scene};
use nightshade::filesystem::{FileFilter, PendingFileLoad};
use nightshade::prelude::*;

const MAP_EXTENSIONS: &[&str] = &["nsmap"];
const PREFAB_EXTENSIONS: &[&str] = &["nsprefab"];

pub enum PendingLoadKind {
    Map,
    Prefab,
}

pub struct PendingLoad {
    pub kind: PendingLoadKind,
    pub handle: PendingFileLoad,
}

#[derive(Default)]
pub struct ProjectIoState {
    pub pending_load: Option<PendingLoad>,
}

pub fn new_project(editor_world: &mut EditorWorld, world: &mut World) {
    despawn_known_world_entities(editor_world, world);
    editor_world.resources.editor_scene.clear();
    editor_world.resources.project = ProjectState::new("Untitled");
    editor_world.resources.writeback_state = Default::default();
    editor_world.resources.writeback_state.mark_full_resync();
    editor_world.resources.undo.clear();
    if editor_world.resources.sun.sun_entity.is_none() {
        crate::systems::sun::spawn_with_shadows(editor_world, world);
    }
    sync_settings(&mut editor_world.resources.project, world);
    let _ = sync_hdr_skybox;
}

pub fn open_project(editor_world: &mut EditorWorld) {
    let filters = [FileFilter {
        name: "Nightshade map".to_string(),
        extensions: MAP_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
    }];
    editor_world.resources.project_io.pending_load = Some(PendingLoad {
        kind: PendingLoadKind::Map,
        handle: nightshade::filesystem::request_file_load(&filters),
    });
}

pub fn import_prefab(editor_world: &mut EditorWorld) {
    let filters = [FileFilter {
        name: "Nightshade prefab".to_string(),
        extensions: PREFAB_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
    }];
    editor_world.resources.project_io.pending_load = Some(PendingLoad {
        kind: PendingLoadKind::Prefab,
        handle: nightshade::filesystem::request_file_load(&filters),
    });
}

pub fn save_selected_as_prefab(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(selected) = editor_world.resources.ui.selected_entity else {
        tracing::warn!("No entity selected; nothing to save as prefab");
        return;
    };

    let default_name = world
        .core
        .get_name(selected)
        .map(|name| format!("{}.nsprefab", name.0))
        .unwrap_or_else(|| "prefab.nsprefab".to_string());

    #[cfg(not(target_arch = "wasm32"))]
    {
        let filters = [FileFilter {
            name: "Nightshade prefab".to_string(),
            extensions: vec!["nsprefab".to_string()],
        }];
        let Some(path) = nightshade::filesystem::save_file_dialog(&filters, Some(&default_name))
        else {
            return;
        };
        let prefab_name = path
            .file_stem()
            .and_then(|stem| stem.to_str())
            .unwrap_or("prefab")
            .to_string();
        let mut scene =
            nightshade::ecs::scene::commands::subtree_to_scene(world, selected, prefab_name);
        // Stamp the prefab with a stable identifier so instances in
        // maps can link back to this exact prefab even if the file
        // moves. Only preserve the existing source uuid when saving
        // over the same file the instance came from — otherwise the
        // user is deriving a new prefab from an instance, and the new
        // file should have its own identity instead of aliasing the
        // original.
        let prior_source = world.core.get_prefab_source(selected);
        let preserves_identity = prior_source.and_then(|source| {
            source
                .source_path
                .as_deref()
                .map(|prior_path| std::path::Path::new(prior_path) == path.as_path())
        }) == Some(true);
        scene.header.prefab_id = Some(if preserves_identity {
            prior_source
                .and_then(|source| source.source_uuid)
                .unwrap_or_else(nightshade::ecs::scene::AssetUuid::random)
        } else {
            nightshade::ecs::scene::AssetUuid::random()
        });
        match nightshade::ecs::scene::commands::save_scene(&mut scene, &path) {
            Ok(()) => {
                tracing::info!("Saved prefab to {}", path.display());
                let thumbnail_path = path.with_extension("thumb.png");
                hide_other_entities_for_thumbnail(editor_world, world, selected);
                nightshade::ecs::world::commands::capture_thumbnail_to_path(
                    world,
                    thumbnail_path,
                    256,
                );
            }
            Err(error) => tracing::error!("Failed to save prefab: {error}"),
        }
    }

    #[cfg(target_arch = "wasm32")]
    {
        use nightshade::ecs::scene::commands::{save_scene_binary_to_bytes, subtree_to_scene};
        let filters = [FileFilter {
            name: "Nightshade prefab".to_string(),
            extensions: vec!["nsprefab".to_string()],
        }];
        let mut scene = subtree_to_scene(world, selected, "prefab");
        let bytes = match save_scene_binary_to_bytes(&mut scene) {
            Ok(bytes) => bytes,
            Err(error) => {
                tracing::error!("Failed to serialize prefab: {error}");
                return;
            }
        };
        if let Err(error) = nightshade::filesystem::save_file(&default_name, &bytes, &filters) {
            tracing::error!("Failed to save prefab: {error}");
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn hide_other_entities_for_thumbnail(
    editor_world: &mut EditorWorld,
    world: &mut World,
    instance_root: Entity,
) {
    use std::collections::HashSet;
    let mut instance_subtree: HashSet<Entity> = HashSet::new();
    instance_subtree.insert(instance_root);
    for descendant in nightshade::ecs::transform::queries::query_descendants(world, instance_root) {
        instance_subtree.insert(descendant);
    }
    let mut hidden: Vec<Entity> = Vec::new();
    let candidate_entities: Vec<Entity> = world
        .core
        .query_entities(nightshade::ecs::world::VISIBILITY)
        .collect();
    for entity in candidate_entities {
        if instance_subtree.contains(&entity) {
            continue;
        }
        let Some(visibility) = world.core.get_visibility(entity) else {
            continue;
        };
        if !visibility.visible {
            continue;
        }
        world
            .core
            .set_visibility(entity, Visibility { visible: false });
        hidden.push(entity);
    }
    editor_world.resources.loading.pending_thumbnail_restore = hidden;
    editor_world
        .resources
        .loading
        .pending_thumbnail_restore_frames = 2;
}

pub fn poll_thumbnail_isolation(editor_world: &mut EditorWorld, world: &mut World) {
    let frames = editor_world
        .resources
        .loading
        .pending_thumbnail_restore_frames;
    if frames == 0 {
        return;
    }
    let next = frames - 1;
    editor_world
        .resources
        .loading
        .pending_thumbnail_restore_frames = next;
    if next > 0 {
        return;
    }
    let hidden = std::mem::take(&mut editor_world.resources.loading.pending_thumbnail_restore);
    for entity in hidden {
        if world.core.get_visibility(entity).is_some() {
            world
                .core
                .set_visibility(entity, Visibility { visible: true });
        }
    }
}

pub fn save_project(editor_world: &mut EditorWorld, world: &mut World) {
    flush_writeback(editor_world, world);
    #[cfg(not(target_arch = "wasm32"))]
    {
        if let Some(path) = editor_world.resources.project.current_path.clone() {
            write_project_to_path(editor_world, world, &path);
            return;
        }
    }
    save_project_as(editor_world, world);
}

pub fn save_project_as(editor_world: &mut EditorWorld, world: &mut World) {
    flush_writeback(editor_world, world);
    let default_name = if editor_world.resources.project.scene_name.is_empty() {
        "untitled.nsmap".to_string()
    } else {
        format!("{}.nsmap", editor_world.resources.project.scene_name)
    };

    #[cfg(not(target_arch = "wasm32"))]
    {
        let filters = [FileFilter {
            name: "Nightshade map".to_string(),
            extensions: vec!["nsmap".to_string()],
        }];
        let Some(path) = nightshade::filesystem::save_file_dialog(&filters, Some(&default_name))
        else {
            return;
        };
        if editor_world.resources.project.scene_name.is_empty()
            && let Some(stem) = path.file_stem().and_then(|stem| stem.to_str())
        {
            editor_world.resources.project.scene_name = stem.to_string();
        }
        editor_world.resources.project.current_path = Some(path.clone());
        write_project_to_path(editor_world, world, &path);
    }

    #[cfg(target_arch = "wasm32")]
    {
        let filters = [FileFilter {
            name: "Nightshade map".to_string(),
            extensions: vec!["nsmap".to_string()],
        }];
        let bytes = match encode_world_as_binary_scene(editor_world, world) {
            Ok(bytes) => bytes,
            Err(error) => {
                tracing::error!("Failed to serialize map: {error}");
                return;
            }
        };
        if let Err(error) = nightshade::filesystem::save_file(&default_name, &bytes, &filters) {
            tracing::error!("Failed to save map: {error}");
            return;
        }
        editor_world.resources.project.clear_modified();
    }
}

pub fn export_glb(editor_world: &mut EditorWorld, world: &mut World) {
    let excluded = editor_world.resources.editor_scene.scaffolding.clone();
    let bytes = match nightshade::ecs::prefab::commands::export_world_to_glb_bytes(world, &excluded)
    {
        Ok(bytes) => bytes,
        Err(error) => {
            tracing::error!("Failed to export scene to GLB: {error}");
            return;
        }
    };
    let default_name = if editor_world.resources.project.scene_name.is_empty() {
        "untitled.glb".to_string()
    } else {
        format!("{}.glb", editor_world.resources.project.scene_name)
    };
    let filters = [FileFilter {
        name: "glTF Binary".to_string(),
        extensions: vec!["glb".to_string()],
    }];

    #[cfg(not(target_arch = "wasm32"))]
    {
        let Some(path) = nightshade::filesystem::save_file_dialog(&filters, Some(&default_name))
        else {
            return;
        };
        match std::fs::write(&path, &bytes) {
            Ok(()) => tracing::info!("Exported scene to {}", path.display()),
            Err(error) => tracing::error!("Failed to write GLB: {error}"),
        }
    }

    #[cfg(target_arch = "wasm32")]
    {
        if let Err(error) = nightshade::filesystem::save_file(&default_name, &bytes, &filters) {
            tracing::error!("Failed to save GLB: {error}");
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn write_project_to_path(
    editor_world: &mut EditorWorld,
    world: &mut World,
    path: &std::path::Path,
) {
    let scene_name = editor_world.resources.project.scene_name.clone();
    let mut scene = world_to_scene_with_uuids(
        world,
        scene_name,
        &editor_world.resources.editor_scene.entity_to_uuid,
    );
    stamp_source_entity_uuids(editor_world, &mut scene);
    match nightshade::ecs::scene::commands::save_scene(&mut scene, path) {
        Ok(()) => {
            editor_world.resources.project.scene = scene;
            editor_world.resources.project.clear_modified();
            tracing::info!("Saved map to {}", path.display());
        }
        Err(error) => {
            tracing::error!("Failed to save map: {error}");
        }
    }
}

#[cfg(target_arch = "wasm32")]
fn encode_world_as_binary_scene(
    editor_world: &EditorWorld,
    world: &World,
) -> Result<Vec<u8>, nightshade::ecs::scene::commands::SceneError> {
    let scene_name = editor_world.resources.project.scene_name.clone();
    let mut scene = world_to_scene_with_uuids(
        world,
        scene_name,
        &editor_world.resources.editor_scene.entity_to_uuid,
    );
    stamp_source_entity_uuids(editor_world, &mut scene);
    save_scene_binary_to_bytes(&mut scene)
}

fn stamp_source_entity_uuids(editor_world: &EditorWorld, scene: &mut Scene) {
    let links = &editor_world
        .resources
        .prefab_instance_links
        .entity_to_source;
    if links.is_empty() {
        return;
    }
    let uuid_to_entity = &editor_world.resources.editor_scene.uuid_to_entity;
    for scene_entity in &mut scene.entities {
        if let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid)
            && let Some(source_uuid) = links.get(&entity)
        {
            scene_entity.source_entity_uuid = Some(*source_uuid);
        }
    }
}

pub fn load_project_bytes_into_editor(
    editor_world: &mut EditorWorld,
    world: &mut World,
    name: &str,
    bytes: &[u8],
) {
    apply_loaded_map_bytes(
        editor_world,
        world,
        &nightshade::filesystem::LoadedFile {
            name: name.to_string(),
            bytes: bytes.to_vec(),
        },
    );
}

pub fn poll_pending_loads(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(pending) = editor_world.resources.project_io.pending_load.take() else {
        return;
    };
    if !pending.handle.is_ready() {
        editor_world.resources.project_io.pending_load = Some(pending);
        return;
    }
    let Some(loaded) = pending.handle.take() else {
        return;
    };
    match pending.kind {
        PendingLoadKind::Map => apply_loaded_map_bytes(editor_world, world, &loaded),
        PendingLoadKind::Prefab => apply_loaded_prefab_bytes(editor_world, world, &loaded),
    }
}

pub fn import_prefab_from_path(
    editor_world: &mut EditorWorld,
    world: &mut World,
    path: &std::path::Path,
) {
    let Ok(bytes) = std::fs::read(path) else {
        tracing::error!("Failed to read prefab '{}'", path.display());
        return;
    };
    let name = path
        .file_name()
        .map(|name| name.to_string_lossy().to_string())
        .unwrap_or_default();
    let loaded = nightshade::filesystem::LoadedFile { name, bytes };
    apply_loaded_prefab_bytes_with_path(editor_world, world, &loaded, Some(path));
}

fn apply_loaded_prefab_bytes(
    editor_world: &mut EditorWorld,
    world: &mut World,
    loaded: &nightshade::filesystem::LoadedFile,
) {
    apply_loaded_prefab_bytes_with_path(editor_world, world, loaded, None);
}

fn apply_loaded_prefab_bytes_with_path(
    editor_world: &mut EditorWorld,
    world: &mut World,
    loaded: &nightshade::filesystem::LoadedFile,
    full_path: Option<&std::path::Path>,
) {
    let prefab_scene = match load_scene_binary_from_bytes(&loaded.bytes) {
        Ok(scene) => scene,
        Err(error) => {
            tracing::error!("Failed to load prefab '{}': {}", loaded.name, error);
            return;
        }
    };

    let source_uuid = prefab_scene.header.prefab_id;
    let display_name_source = loaded.name.clone();
    let source_path = full_path
        .map(|path| path.to_string_lossy().to_string())
        .unwrap_or_else(|| loaded.name.clone());
    let prefab_display_name = if prefab_scene.header.name.is_empty() {
        derive_scene_name_from_filename(&display_name_source)
    } else {
        prefab_scene.header.name.clone()
    };

    match nightshade::ecs::scene::commands::spawn_prefab_scene(world, &prefab_scene, None) {
        Ok(result) => {
            for warning in &result.warnings {
                tracing::warn!("prefab spawn: {warning}");
            }
            // Attach PrefabSource on each spawned root so the link back
            // to the originating .nsprefab survives map round-trip. The
            // source_uuid is the stable id of the prefab file; the path
            // is informational. Future "refresh from source" workflows
            // can use either to find this instance.
            for &root_entity in &result.root_entities {
                world
                    .core
                    .add_components(root_entity, nightshade::ecs::world::PREFAB_SOURCE);
                world.core.set_prefab_source(
                    root_entity,
                    nightshade::ecs::prefab::components::PrefabSource {
                        prefab_name: prefab_display_name.clone(),
                        source_path: Some(source_path.clone()),
                        source_uuid,
                    },
                );
            }
            for (&entity, source_entity_uuid) in &result.entity_to_source_uuid {
                editor_world
                    .resources
                    .prefab_instance_links
                    .entity_to_source
                    .insert(entity, *source_entity_uuid);
            }
            if let Some(uuid) = source_uuid {
                editor_world
                    .resources
                    .prefab_instance_links
                    .imported_scenes
                    .insert(uuid, prefab_scene.clone());
            }
            // Register the new entities under fresh editor UUIDs so the
            // tree panel, undo, and writeback all see them.
            for &root_entity in &result.root_entities {
                crate::scene_writeback::register_subtree(
                    &mut editor_world.resources.project,
                    &mut editor_world.resources.editor_scene,
                    world,
                    root_entity,
                );
            }
            editor_world.resources.loading.pending_fit_roots = result.root_entities.clone();
            editor_world
                .resources
                .loading
                .model_entities
                .extend(result.root_entities);
            editor_world.resources.loading.pending_fit_frames = 2;
            editor_world.resources.project.mark_modified();
            tracing::info!("Imported prefab '{}'", loaded.name);
        }
        Err(error) => {
            tracing::error!("Failed to spawn prefab: {error}");
        }
    }
}

fn apply_loaded_map_bytes(
    editor_world: &mut EditorWorld,
    world: &mut World,
    loaded: &nightshade::filesystem::LoadedFile,
) {
    let is_json_extension = std::path::Path::new(&loaded.name)
        .extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("json"));

    let scene = if is_json_extension {
        match nightshade::prelude::serde_json::from_slice::<Scene>(&loaded.bytes) {
            Ok(mut scene) => {
                scene.rebuild_uuid_index();
                scene
            }
            Err(error) => {
                tracing::error!("Failed to load '{}' as scene: {}", loaded.name, error);
                return;
            }
        }
    } else {
        match load_scene_binary_from_bytes(&loaded.bytes) {
            Ok(scene) => scene,
            Err(error) => {
                tracing::error!("Failed to load '{}' as binary map: {}", loaded.name, error);
                return;
            }
        }
    };

    despawn_known_world_entities(editor_world, world);
    editor_world.resources.editor_scene.clear();
    editor_world.resources.loading.last_dropped_error = None;

    let scene_name = if scene.header.name.is_empty() {
        derive_scene_name_from_filename(&loaded.name)
    } else {
        scene.header.name.clone()
    };

    editor_world.resources.project = ProjectState {
        scene: Scene::default(),
        scene_name,
        #[cfg(not(target_arch = "wasm32"))]
        current_path: std::path::PathBuf::from(&loaded.name).canonicalize().ok(),
        modified: false,
    };
    editor_world.resources.writeback_state = Default::default();
    editor_world.resources.undo.clear();

    apply_scene_to_world(editor_world, world, &scene);
    editor_world.resources.project.scene = scene;
    if crate::systems::sun::scene_has_lights(editor_world, world) {
        crate::systems::sun::despawn_default(editor_world, world);
        crate::systems::sun::adopt_scene_sun(editor_world, world);
    } else if editor_world.resources.sun.sun_entity.is_none() {
        crate::systems::sun::spawn_with_shadows(editor_world, world);
    }
    auto_refresh_prefab_instances(editor_world, world);
    tracing::info!("Loaded map '{}'", loaded.name);
}

fn auto_refresh_prefab_instances(editor_world: &mut EditorWorld, world: &mut World) {
    use nightshade::ecs::world::PREFAB_SOURCE;
    let candidates: Vec<Entity> = world.core.query_entities(PREFAB_SOURCE).collect();
    let mut refreshed = 0usize;
    for entity in candidates {
        let Some(source) = world.core.get_prefab_source(entity).cloned() else {
            continue;
        };
        if source.source_path.is_none() && source.source_uuid.is_none() {
            continue;
        }
        if !crate::systems::retained_ui::prefab_browser::source_reachable(editor_world, &source) {
            continue;
        }
        crate::systems::retained_ui::prefab_browser::refresh_selected_instance(
            editor_world,
            world,
            entity,
        );
        refreshed += 1;
    }
    if refreshed > 0 {
        tracing::info!("Auto-refreshed {refreshed} prefab instance(s) from source");
    }
}

fn apply_scene_to_world(editor_world: &mut EditorWorld, world: &mut World, scene: &Scene) {
    world.resources.render_settings.atmosphere = scene.atmosphere;
    match &scene.hdr_skybox {
        Some(SceneHdrSkybox::Reference { path }) => {
            nightshade::ecs::world::commands::queue_render_command(
                world,
                nightshade::ecs::world::commands::RenderCommand::LoadHdrSkyboxFromPath {
                    path: std::path::PathBuf::from(path),
                },
            );
        }
        Some(SceneHdrSkybox::Embedded { data }) => {
            nightshade::ecs::world::commands::queue_render_command(
                world,
                nightshade::ecs::world::commands::RenderCommand::LoadHdrSkybox {
                    hdr_data: data.clone(),
                },
            );
        }
        Some(SceneHdrSkybox::Asset { uuid }) => {
            tracing::warn!("scene references HDR by uuid {uuid} but no asset registry is wired");
        }
        None => {}
    }
    match spawn_scene(world, scene, None) {
        Ok(result) => {
            for warning in &result.warnings {
                tracing::warn!("scene spawn: {warning}");
            }
            let uuid_to_entity = result.uuid_to_entity.clone();
            editor_world
                .resources
                .editor_scene
                .replace_entities(result.uuid_to_entity);
            editor_world
                .resources
                .prefab_instance_links
                .entity_to_source
                .clear();
            editor_world
                .resources
                .prefab_instance_links
                .entity_to_source
                .extend(result.entity_to_source_uuid);
            for scene_entity in &scene.entities {
                let Some(runtime_entity) = uuid_to_entity.get(&scene_entity.uuid).copied() else {
                    continue;
                };
                editor_world
                    .resources
                    .editor_scene
                    .set_entity_layer(runtime_entity, scene_entity.layer);
                editor_world
                    .resources
                    .editor_scene
                    .set_entity_chunk(runtime_entity, scene_entity.chunk_id);
            }
            editor_world.resources.loading.pending_fit_roots = result.root_entities.clone();
            editor_world
                .resources
                .loading
                .model_entities
                .extend(result.root_entities);
            editor_world.resources.loading.pending_fit_frames = 2;
        }
        Err(error) => {
            tracing::error!("Failed to spawn map: {error}");
        }
    }
}

fn despawn_known_world_entities(editor_world: &mut EditorWorld, world: &mut World) {
    editor_world.resources.loading.pending_imports.clear();
    loading::clear_scene(editor_world, world);
}

fn derive_scene_name_from_filename(filename: &str) -> String {
    std::path::Path::new(filename)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(|stem| stem.to_string())
        .unwrap_or_else(|| "Imported".to_string())
}

/// Reconciles writeback state before save. The in-memory `Scene` mirror
/// is updated for UI panels that read it (tree, inspector, undo), but
/// the on-disk save itself does a fresh bulk export via
/// `world_to_scene_with_uuids` so every component type and embedded
/// asset is captured uniformly.
fn flush_writeback(editor_world: &mut EditorWorld, world: &mut World) {
    crate::scene_writeback::reconcile(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        &mut editor_world.resources.writeback_state,
        world,
        true,
    );
    sync_settings(&mut editor_world.resources.project, world);
}
