use super::*;

#[derive(Resource)]
pub(super) struct EditorContentWatch {
    root: PathBuf,
    service: Mutex<Option<content_pipeline::ContentWatchService>>,
}

#[derive(Resource, Default)]
pub(super) struct ExternalContentRefreshQueue {
    active: Option<ExternalContentRefreshJob>,
    pending: Option<PendingExternalContentRefresh>,
}

pub(super) struct ExternalContentRefreshJob {
    handle: std::thread::JoinHandle<Result<(shared::AssetRegistryDocument, String), String>>,
}

pub(super) struct PendingExternalContentRefresh {
    root: PathBuf,
    changed_path_count: usize,
    status_prefix: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ExternalMoveRepairChoice {
    Preserve(String),
    RegisterNew,
}

#[derive(Debug, Clone)]
pub(crate) struct ExternalMoveRepairReview {
    pub(crate) root: PathBuf,
    pub(crate) previous: shared::AssetRegistryDocument,
    pub(crate) conflicts: Vec<content_pipeline::ExternalMoveConflict>,
    pub(crate) choices: Vec<Option<ExternalMoveRepairChoice>>,
    pub(crate) open: bool,
}

#[derive(Resource, Debug, Default, Clone)]
pub(crate) struct PendingExternalMoveRepair {
    pub(crate) review: Option<ExternalMoveRepairReview>,
}

impl Plugin for EditorAssetsPlugin {
    fn build(&self, app: &mut App) {
        let root = PathBuf::from(
            &app.world()
                .resource::<crate::project_io::ProjectWorkspace>()
                .root,
        );
        let service = match content_pipeline::ContentWatchService::watch_project(&root) {
            Ok(service) => Some(service),
            Err(error) => {
                warn!("Live content watching is unavailable: {error}");
                None
            }
        };
        app.init_resource::<EditorAssets>()
            .init_resource::<PendingExternalMoveRepair>()
            .init_resource::<ExternalContentRefreshQueue>()
            .insert_resource(EditorContentWatch {
                root,
                service: Mutex::new(service),
            })
            .init_resource::<crate::assets::prefab_overrides::PrefabHealthCache>()
            .add_observer(crate::assets::prefab_overrides::invalidate_prefab_health_on_ready)
            .add_systems(
                Update,
                (
                    refresh_assets_on_settings_change,
                    crate::assets::prefab_overrides::guard_prefab_reimports,
                ),
            )
            .add_systems(Startup, refresh_content_on_startup)
            .add_systems(
                Update,
                (poll_content_watcher, drive_external_content_refresh).chain(),
            );
    }
}

pub(crate) fn begin_content_watch_transaction(world: &World) {
    let Some(watcher) = world.get_resource::<EditorContentWatch>() else {
        return;
    };
    if let Ok(mut service) = watcher.service.lock() {
        if let Some(service) = service.as_mut() {
            service.begin_transaction();
        }
    }
}

pub(crate) fn end_content_watch_transaction(world: &World) {
    let Some(watcher) = world.get_resource::<EditorContentWatch>() else {
        return;
    };
    if let Ok(mut service) = watcher.service.lock() {
        if let Some(service) = service.as_mut() {
            service.end_transaction();
        }
    }
}

pub(crate) fn suppress_content_watch_path(world: &World, path: &Path) {
    let Some(watcher) = world.get_resource::<EditorContentWatch>() else {
        return;
    };
    if let Ok(mut service) = watcher.service.lock() {
        if let Some(service) = service.as_mut() {
            service.suppress_path(path, Duration::from_millis(750));
        }
    }
}

pub(super) fn poll_content_watcher(world: &mut World) {
    let Some(watcher) = world.get_resource::<EditorContentWatch>() else {
        return;
    };
    let root = watcher.root.clone();
    let changes = {
        let Ok(mut service) = watcher.service.lock() else {
            return;
        };
        let Some(service) = service.as_mut() else {
            return;
        };
        match service.poll(Duration::from_millis(150)) {
            Ok(changes) => changes,
            Err(error) => {
                warn!("Content watcher failed: {error}");
                return;
            }
        }
    };
    if changes.is_empty() {
        return;
    }
    if let Some(mut cache) = world.get_resource_mut::<blacksite_surface::SurfaceMaterialCache>() {
        cache.invalidate_disk_documents();
    }

    if let Err(error) =
        process_or_queue_content_refresh(world, root, changes.len(), "Live content refresh")
    {
        world.resource_mut::<crate::scene_io::SceneIo>().status =
            format!("Live content refresh failed: {error}");
    }
}

pub(super) fn refresh_content_on_startup(world: &mut World) {
    let root = world
        .get_resource::<crate::project_io::ProjectWorkspace>()
        .map(|workspace| PathBuf::from(&workspace.root))
        .unwrap_or_else(|| PathBuf::from("."));
    if let Err(error) = process_or_queue_content_refresh(world, root, 0, "Startup content refresh")
    {
        warn!("Startup content refresh failed: {error}");
    }
}

pub(super) fn process_or_queue_content_refresh(
    world: &mut World,
    root: PathBuf,
    changed_path_count: usize,
    status_prefix: &str,
) -> Result<(), String> {
    let previous = world.resource::<AssetRegistry>().document();
    if queue_external_move_repair(world, root.clone(), previous.clone())? {
        world.resource_mut::<EditorAssets>().refresh();
        crate::assets::invalidate_on_catalog_refresh(world);
        return Ok(());
    }
    let mut queue = world.resource_mut::<ExternalContentRefreshQueue>();
    if queue.active.is_some() {
        let pending = queue
            .pending
            .get_or_insert_with(|| PendingExternalContentRefresh {
                root: root.clone(),
                changed_path_count: 0,
                status_prefix: status_prefix.to_string(),
            });
        pending.root = root;
        pending.changed_path_count = pending
            .changed_path_count
            .saturating_add(changed_path_count);
        pending.status_prefix = status_prefix.to_string();
        return Ok(());
    }
    let status_prefix = status_prefix.to_string();
    let handle = std::thread::spawn(move || {
        process_content_refresh(&root, &previous, changed_path_count, &status_prefix)
    });
    queue.active = Some(ExternalContentRefreshJob { handle });
    Ok(())
}

pub(super) fn drive_external_content_refresh(world: &mut World) {
    let completed = {
        let mut queue = world.resource_mut::<ExternalContentRefreshQueue>();
        if queue
            .active
            .as_ref()
            .is_some_and(|job| job.handle.is_finished())
        {
            queue.active.take()
        } else {
            None
        }
    };
    let Some(job) = completed else {
        return;
    };
    match job
        .handle
        .join()
        .unwrap_or_else(|_| Err("content refresh worker panicked".into()))
    {
        Ok((document, status)) => {
            apply_processed_content(world, document);
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = status;
            }
        }
        Err(error) => {
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = format!("Content refresh failed: {error}");
            }
        }
    }
    let pending = world
        .resource_mut::<ExternalContentRefreshQueue>()
        .pending
        .take();
    if let Some(pending) = pending {
        let _ = process_or_queue_content_refresh(
            world,
            pending.root,
            pending.changed_path_count,
            &pending.status_prefix,
        );
    }
}

#[cfg(test)]
pub(super) fn process_content_refresh_for_test(
    world: &mut World,
    root: PathBuf,
) -> Result<(), String> {
    world.init_resource::<ExternalContentRefreshQueue>();
    process_or_queue_content_refresh(world, root, 0, "Startup content refresh")?;
    for _ in 0..1_000 {
        drive_external_content_refresh(world);
        let queue = world.resource::<ExternalContentRefreshQueue>();
        if queue.active.is_none() && queue.pending.is_none() {
            return Ok(());
        }
        std::thread::sleep(std::time::Duration::from_millis(1));
    }
    Err("content refresh worker did not finish within one second".into())
}

pub(super) fn queue_external_move_repair(
    world: &mut World,
    root: PathBuf,
    previous: shared::AssetRegistryDocument,
) -> Result<bool, String> {
    let conflicts = content_pipeline::find_ambiguous_external_moves(&root, &previous)?;
    if conflicts.is_empty() {
        return Ok(false);
    }
    let count = conflicts.len();
    world.resource_mut::<PendingExternalMoveRepair>().review = Some(ExternalMoveRepairReview {
        root,
        previous,
        choices: vec![None; count],
        conflicts,
        open: true,
    });
    if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
        scene_io.status = format!(
            "{count} ambiguous external move(s) require identity repair in the Content Browser"
        );
    }
    Ok(true)
}

pub(crate) fn refresh_content_browser(world: &mut World) {
    let root = world
        .get_resource::<crate::project_io::ProjectWorkspace>()
        .map(|workspace| PathBuf::from(&workspace.root))
        .unwrap_or_else(|| PathBuf::from("."));
    match process_or_queue_content_refresh(world, root, 0, "Manual content refresh") {
        Ok(()) => {}
        Err(error) => {
            if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
                scene_io.status = format!("Manual content refresh failed: {error}");
            }
        }
    }
}

pub(super) fn process_content_refresh(
    root: &Path,
    previous: &shared::AssetRegistryDocument,
    changed_path_count: usize,
    status_prefix: &str,
) -> Result<(shared::AssetRegistryDocument, String), String> {
    let mut processed = content_pipeline::scan_project(root, previous)?;
    let current_ids = processed
        .registry
        .records
        .iter()
        .map(|record| record.id.as_string())
        .collect::<HashSet<_>>();
    let removed_paths = previous
        .records
        .iter()
        .filter(|record| !current_ids.contains(&record.id.as_string()))
        .map(|record| PathBuf::from(&record.path))
        .collect::<Vec<_>>();
    let removed_usages = if removed_paths.is_empty() {
        Vec::new()
    } else {
        content_pipeline::find_reference_usages(root, &removed_paths, previous)
            .map_err(|error| format!("could not inspect removed-asset usages: {error}"))?
    };
    let mut model_plans = Vec::new();
    for record in processed
        .registry
        .records
        .iter_mut()
        .filter(|record| record.kind == shared::AssetKind::Model)
    {
        let plan = crate::assets::plan_model_artifacts_at(root, record)
            .map_err(|error| format!("model processing failed for {}: {error}", record.path))?;
        *record = plan.record.clone();
        model_plans.push(plan);
    }
    processed.runtime_catalog = shared::RuntimeContentCatalog::from(&processed.registry);
    let mut texture_plans = Vec::new();
    for record in processed
        .registry
        .records
        .iter()
        .filter(|record| record.kind == shared::AssetKind::Texture)
    {
        let plan = content_pipeline::plan_texture_artifact(root, record)
            .map_err(|error| format!("Texture processing failed for {}: {error}", record.path))?;
        content_pipeline::apply_texture_plan_to_catalog(
            &mut processed.runtime_catalog,
            &record.id,
            &plan,
        )?;
        texture_plans.push(plan);
    }
    let mut material_plans = Vec::new();
    for record in processed.registry.records.iter().filter(|record| {
        matches!(
            record.kind,
            shared::AssetKind::Material | shared::AssetKind::MaterialInstance
        )
    }) {
        if let Some(plan) =
            content_pipeline::plan_material_artifact(root, record, &processed.registry)?
        {
            content_pipeline::apply_material_plan_to_catalog(
                &mut processed.runtime_catalog,
                &record.id,
                &plan,
            )?;
            material_plans.push(plan);
        }
    }
    let mut publication_paths = model_plans
        .iter()
        .flat_map(|plan| [plan.static_path.clone(), plan.animation_path.clone()])
        .collect::<Vec<_>>();
    publication_paths.extend(texture_plans.iter().map(|plan| plan.output_path.clone()));
    publication_paths.extend(material_plans.iter().map(|plan| plan.output_path.clone()));
    publication_paths.push(root.join(content_pipeline::REGISTRY_PATH));
    publication_paths.push(root.join(content_pipeline::RUNTIME_CATALOG_PATH));
    publication_paths.sort();
    publication_paths.dedup();
    let backups = publication_paths
        .iter()
        .map(|path| (path.clone(), fs::read(path).ok()))
        .collect::<Vec<_>>();
    let publication = (|| {
        for plan in &model_plans {
            crate::assets::publish_model_artifacts(plan)?;
        }
        for plan in &texture_plans {
            content_pipeline::publish_texture_artifact(plan)?;
        }
        for plan in &material_plans {
            content_pipeline::publish_material_artifact(plan)?;
        }
        content_pipeline::publish_content_documents_with_catalog(
            root,
            &processed.registry,
            &processed.runtime_catalog,
        )
    })();
    if let Err(error) = publication {
        for (path, bytes) in &backups {
            if let Some(bytes) = bytes {
                let _ = fs::write(path, bytes);
            } else {
                let _ = fs::remove_file(path);
            }
        }
        return Err(format!("content publication rolled back: {error}"));
    }

    let mut status = format!(
        "{status_prefix}: {changed_path_count} changed path(s), {} model artifact set(s), {} Texture artifact(s), {} packed Material artifact(s), {} added, {} moved, {} removed",
        model_plans.len(),
        texture_plans.len(),
        material_plans.len(),
        processed.report.added,
        processed.report.moved,
        processed.report.removed
    );
    if !removed_usages.is_empty() {
        let documents = removed_usages
            .iter()
            .map(|usage| usage.document.display().to_string())
            .collect::<BTreeSet<_>>()
            .into_iter()
            .take(3)
            .collect::<Vec<_>>()
            .join(", ");
        status.push_str(&format!(
            "; {} reference(s) now unresolved in {}",
            removed_usages.len(),
            documents
        ));
    }
    Ok((processed.registry, status))
}

pub(super) fn apply_processed_content(world: &mut World, document: shared::AssetRegistryDocument) {
    if world.contains_resource::<crate::asset_documents::AuthoredAssetDocumentStore>() {
        crate::asset_documents::adopt_external_registry_document(world, &document);
    } else {
        let mut registry = world.resource_mut::<AssetRegistry>();
        registry.schema_version = document.schema_version;
        registry.defaults = document.defaults;
        registry.records = document.records;
        registry.index_dirty = false;
        registry.migration_required = false;
    }
    world.resource_mut::<EditorAssets>().refresh();
    crate::assets::invalidate_on_catalog_refresh(world);
}

pub(crate) fn commit_external_move_repairs(world: &mut World) -> Result<(), String> {
    let review = world
        .resource::<PendingExternalMoveRepair>()
        .review
        .clone()
        .ok_or_else(|| "no external move repair is pending".to_string())?;
    if review.choices.iter().any(Option::is_none) {
        return Err("choose an identity for every ambiguous external move".into());
    }
    let current_registry = world.resource::<AssetRegistry>().document();
    if current_registry != review.previous {
        return Err("asset registry changed after the external move review opened".into());
    }
    let current_conflicts =
        content_pipeline::find_ambiguous_external_moves(&review.root, &review.previous)?;
    if current_conflicts != review.conflicts {
        let count = current_conflicts.len();
        world.resource_mut::<PendingExternalMoveRepair>().review = (!current_conflicts.is_empty())
            .then(|| ExternalMoveRepairReview {
                root: review.root,
                previous: current_registry,
                choices: vec![None; count],
                conflicts: current_conflicts,
                open: true,
            });
        return Err("external files changed after review; inspect the refreshed conflicts".into());
    }

    let repaired = registry_after_external_move_repairs(&review)?;

    let (document, status) =
        process_content_refresh(&review.root, &repaired, 0, "External move repair")?;
    apply_processed_content(world, document);
    world.resource_mut::<PendingExternalMoveRepair>().review = None;
    world.resource_mut::<crate::scene_io::SceneIo>().status = status;
    Ok(())
}

pub(super) fn registry_after_external_move_repairs(
    review: &ExternalMoveRepairReview,
) -> Result<shared::AssetRegistryDocument, String> {
    if review.choices.len() != review.conflicts.len() || review.choices.iter().any(Option::is_none)
    {
        return Err("choose an identity for every ambiguous external move".into());
    }

    let mut repaired = review.previous.clone();
    let mut preserved = HashSet::new();
    for (conflict, choice) in review.conflicts.iter().zip(review.choices.iter()) {
        if let Some(ExternalMoveRepairChoice::Preserve(old_path)) = choice {
            if !conflict.candidate_paths.contains(old_path) {
                return Err(format!(
                    "selected identity {old_path} is not a candidate for {}",
                    conflict.new_path
                ));
            }
            if !preserved.insert(old_path.clone()) {
                return Err(format!(
                    "stable identity {old_path} cannot be assigned to more than one new path"
                ));
            }
            let record = repaired
                .records
                .iter_mut()
                .find(|record| record.path == *old_path)
                .ok_or_else(|| format!("candidate registry record disappeared: {old_path}"))?;
            record.path.clone_from(&conflict.new_path);
        }
    }
    let candidate_paths = review
        .conflicts
        .iter()
        .flat_map(|conflict| conflict.candidate_paths.iter())
        .cloned()
        .collect::<HashSet<_>>();
    for record in &mut repaired.records {
        if candidate_paths.contains(&record.path) && !preserved.contains(&record.path) {
            record.source_fingerprint = None;
        }
    }
    Ok(repaired)
}

pub(super) fn refresh_assets_on_settings_change(
    settings: Res<settings::ProjectSettings>,
    mut assets: ResMut<EditorAssets>,
) {
    if settings.is_changed() {
        assets.refresh();
    }
}
