use crate::{
    serialize_registry, serialize_runtime_catalog, validate_asset_path, write_if_changed,
    REGISTRY_PATH, RUNTIME_CATALOG_PATH,
};
use serde::{Deserialize, Serialize};
use shared::{AssetRecord, AssetRegistryDocument};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

const TRASH_ROOT: &str = "assets/.trash";
const TRASH_MANIFEST: &str = ".blacksite-trash.ron";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TrashManifest {
    pub schema_version: u32,
    pub batch_id: String,
    pub original_paths: Vec<String>,
    #[serde(default)]
    pub derived_paths: Vec<String>,
    pub records: Vec<AssetRecord>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrashBatchSummary {
    pub path: PathBuf,
    pub batch_id: String,
    pub original_paths: Vec<String>,
}

pub fn trash_content(
    project_root: &Path,
    original_paths: &[PathBuf],
    registry: &mut AssetRegistryDocument,
) -> Result<TrashBatchSummary, String> {
    let originals = normalize_originals(project_root, original_paths)?;
    let batch_id = trash_batch_id();
    let batch_path = PathBuf::from(TRASH_ROOT).join(&batch_id);
    let absolute_batch = project_root.join(&batch_path);
    fs::create_dir_all(&absolute_batch)
        .map_err(|error| format!("could not create {}: {error}", absolute_batch.display()))?;
    let records = registry
        .records
        .iter()
        .filter(|record| originals.iter().any(|path| descendant(&record.path, path)))
        .cloned()
        .collect::<Vec<_>>();
    let derived_paths = collect_derived_paths(project_root, &records, &originals)?;
    let manifest = TrashManifest {
        schema_version: 2,
        batch_id: batch_id.clone(),
        original_paths: originals.clone(),
        derived_paths,
        records,
    };
    let original_registry = registry.clone();
    let registry_path = project_root.join(REGISTRY_PATH);
    let original_registry_bytes = fs::read(&registry_path).ok();
    let runtime_path = project_root.join(RUNTIME_CATALOG_PATH);
    let original_runtime_bytes = fs::read(&runtime_path).ok();
    let mut moved = Vec::new();
    let result: Result<(), String> = (|| {
        for original in &originals {
            let source = project_root.join(original);
            let destination = absolute_batch.join(original);
            if let Some(parent) = destination.parent() {
                fs::create_dir_all(parent)
                    .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
            }
            fs::rename(&source, &destination).map_err(|error| {
                format!(
                    "could not move {} to {}: {error}",
                    source.display(),
                    destination.display()
                )
            })?;
            moved.push((source, destination));
        }
        for derived in &manifest.derived_paths {
            let source = project_root.join(derived);
            let destination = absolute_batch.join(derived);
            if let Some(parent) = destination.parent() {
                fs::create_dir_all(parent)
                    .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
            }
            fs::rename(&source, &destination).map_err(|error| {
                format!(
                    "could not move derived artifact {} to trash: {error}",
                    source.display()
                )
            })?;
            moved.push((source, destination));
        }
        let manifest_ron = ron::ser::to_string_pretty(&manifest, ron::ser::PrettyConfig::default())
            .map_err(|error| format!("could not serialize trash manifest: {error}"))?;
        fs::write(absolute_batch.join(TRASH_MANIFEST), manifest_ron)
            .map_err(|error| format!("could not write trash manifest: {error}"))?;
        registry
            .records
            .retain(|record| !originals.iter().any(|path| descendant(&record.path, path)));
        write_if_changed(&registry_path, &serialize_registry(registry)?)?;
        write_runtime_catalog(&runtime_path, registry)?;
        Ok(())
    })();
    if let Err(error) = result {
        rollback_moves(moved);
        restore_registry(&registry_path, original_registry_bytes.as_deref());
        restore_registry(&runtime_path, original_runtime_bytes.as_deref());
        *registry = original_registry;
        let _ = fs::remove_dir_all(&absolute_batch);
        return Err(format!("trash transaction rolled back: {error}"));
    }
    Ok(TrashBatchSummary {
        path: batch_path,
        batch_id,
        original_paths: originals,
    })
}

pub fn list_trash_batches(project_root: &Path) -> Result<Vec<TrashBatchSummary>, String> {
    let root = project_root.join(TRASH_ROOT);
    if !root.exists() {
        return Ok(Vec::new());
    }
    let mut batches = Vec::new();
    for entry in fs::read_dir(&root)
        .map_err(|error| format!("could not inspect {}: {error}", root.display()))?
    {
        let entry = entry.map_err(|error| format!("could not inspect trash entry: {error}"))?;
        if !entry
            .file_type()
            .map_err(|error| format!("could not inspect trash entry: {error}"))?
            .is_dir()
        {
            continue;
        }
        let absolute = entry.path();
        let manifest = load_or_infer_manifest(&absolute)?;
        batches.push(TrashBatchSummary {
            path: PathBuf::from(TRASH_ROOT).join(&manifest.batch_id),
            batch_id: manifest.batch_id,
            original_paths: manifest.original_paths,
        });
    }
    batches.sort_by(|left, right| right.batch_id.cmp(&left.batch_id));
    Ok(batches)
}

pub fn restore_trash_batch(
    project_root: &Path,
    batch_path: &Path,
    registry: &mut AssetRegistryDocument,
) -> Result<TrashBatchSummary, String> {
    let normalized_batch = normalize(batch_path);
    if !normalized_batch.starts_with("assets/.trash/") {
        return Err("restore path must identify a managed trash batch".into());
    }
    let absolute_batch = project_root.join(batch_path);
    let manifest = load_or_infer_manifest(&absolute_batch)?;
    for original in &manifest.original_paths {
        validate_asset_path(Path::new(original))?;
        if project_root.join(original).exists() {
            return Err(format!("restore collision: {original} already exists"));
        }
        if !trashed_source(&absolute_batch, original).exists() {
            return Err(format!("trash batch is missing {original}"));
        }
    }
    for derived in &manifest.derived_paths {
        validate_derived_path(Path::new(derived))?;
        if project_root.join(derived).exists() {
            return Err(format!("restore collision: {derived} already exists"));
        }
        if !trashed_source(&absolute_batch, derived).exists() {
            return Err(format!("trash batch is missing derived artifact {derived}"));
        }
    }
    for record in &manifest.records {
        if registry
            .records
            .iter()
            .any(|existing| existing.path == record.path || existing.id == record.id)
        {
            return Err(format!(
                "registry collision prevents restoring {}",
                record.path
            ));
        }
    }

    let original_registry = registry.clone();
    let registry_path = project_root.join(REGISTRY_PATH);
    let original_registry_bytes = fs::read(&registry_path).ok();
    let runtime_path = project_root.join(RUNTIME_CATALOG_PATH);
    let original_runtime_bytes = fs::read(&runtime_path).ok();
    let mut moved = Vec::new();
    let result: Result<(), String> = (|| {
        for original in &manifest.original_paths {
            let source = trashed_source(&absolute_batch, original);
            let destination = project_root.join(original);
            if let Some(parent) = destination.parent() {
                fs::create_dir_all(parent)
                    .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
            }
            fs::rename(&source, &destination).map_err(|error| {
                format!(
                    "could not restore {} to {}: {error}",
                    source.display(),
                    destination.display()
                )
            })?;
            moved.push((source, destination));
        }
        for derived in &manifest.derived_paths {
            let source = trashed_source(&absolute_batch, derived);
            let destination = project_root.join(derived);
            if let Some(parent) = destination.parent() {
                fs::create_dir_all(parent)
                    .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
            }
            fs::rename(&source, &destination).map_err(|error| {
                format!(
                    "could not restore derived artifact {} to {}: {error}",
                    source.display(),
                    destination.display()
                )
            })?;
            moved.push((source, destination));
        }
        registry.records.extend(manifest.records.iter().cloned());
        registry
            .records
            .sort_by(|left, right| left.path.cmp(&right.path));
        write_if_changed(&registry_path, &serialize_registry(registry)?)?;
        write_runtime_catalog(&runtime_path, registry)?;
        Ok(())
    })();
    if let Err(error) = result {
        rollback_moves(moved);
        restore_registry(&registry_path, original_registry_bytes.as_deref());
        restore_registry(&runtime_path, original_runtime_bytes.as_deref());
        *registry = original_registry;
        return Err(format!("restore transaction rolled back: {error}"));
    }
    let _ = fs::remove_dir_all(&absolute_batch);
    Ok(TrashBatchSummary {
        path: batch_path.to_path_buf(),
        batch_id: manifest.batch_id,
        original_paths: manifest.original_paths,
    })
}

fn normalize_originals(project_root: &Path, paths: &[PathBuf]) -> Result<Vec<String>, String> {
    if paths.is_empty() {
        return Err("no content paths were selected for trash".into());
    }
    let mut originals: Vec<String> = Vec::new();
    for path in paths {
        validate_asset_path(path)?;
        let normalized = normalize(path);
        if !project_root.join(path).exists() {
            return Err(format!("content path does not exist: {}", path.display()));
        }
        if originals
            .iter()
            .any(|parent| descendant(&normalized, parent))
        {
            continue;
        }
        originals.retain(|child| !descendant(child, &normalized));
        originals.push(normalized);
    }
    originals.sort();
    Ok(originals)
}

fn load_or_infer_manifest(batch: &Path) -> Result<TrashManifest, String> {
    let manifest_path = batch.join(TRASH_MANIFEST);
    if let Ok(text) = fs::read_to_string(&manifest_path) {
        return ron::from_str(&text).map_err(|error| {
            format!(
                "invalid trash manifest {}: {error}",
                manifest_path.display()
            )
        });
    }
    let batch_id = batch
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("legacy")
        .to_string();
    let scan_root = if batch.join("assets").is_dir() {
        batch.join("assets")
    } else {
        batch.to_path_buf()
    };
    let mut original_paths = Vec::new();
    for entry in fs::read_dir(&scan_root)
        .map_err(|error| format!("could not inspect legacy trash batch: {error}"))?
    {
        let entry =
            entry.map_err(|error| format!("could not inspect legacy trash item: {error}"))?;
        if entry.file_name() == TRASH_MANIFEST {
            continue;
        }
        original_paths.push(format!("assets/{}", entry.file_name().to_string_lossy()));
    }
    original_paths.sort();
    Ok(TrashManifest {
        schema_version: 0,
        batch_id,
        original_paths,
        derived_paths: Vec::new(),
        records: Vec::new(),
    })
}

fn collect_derived_paths(
    project_root: &Path,
    records: &[AssetRecord],
    originals: &[String],
) -> Result<Vec<String>, String> {
    let mut derived = Vec::new();
    for record in records {
        let Some(settings) = record.import_settings.model() else {
            continue;
        };
        for path in [
            settings.static_mesh_manifest_path.as_deref(),
            settings.animation_manifest_path.as_deref(),
        ]
        .into_iter()
        .flatten()
        {
            validate_derived_path(Path::new(path))?;
            if originals.iter().any(|original| descendant(path, original)) {
                continue;
            }
            let absolute = project_root.join(path);
            let Ok(metadata) = fs::symlink_metadata(&absolute) else {
                continue;
            };
            if metadata.file_type().is_symlink() {
                return Err(format!(
                    "derived artifact may not be a symlink: {}",
                    absolute.display()
                ));
            }
            if metadata.is_file() {
                derived.push(path.to_string());
            }
        }
    }
    derived.sort();
    derived.dedup();
    Ok(derived)
}

fn validate_derived_path(path: &Path) -> Result<(), String> {
    if validate_asset_path(path).is_ok() {
        return Ok(());
    }
    if path.is_absolute()
        || path.components().any(|component| {
            matches!(
                component,
                std::path::Component::ParentDir
                    | std::path::Component::RootDir
                    | std::path::Component::Prefix(_)
            )
        })
    {
        return Err("derived artifact paths must remain project-relative".into());
    }
    let normalized = normalize(path);
    if descendant(&normalized, "assets/meshes/generated")
        || descendant(&normalized, "assets/animations/generated")
    {
        Ok(())
    } else {
        Err(format!(
            "unsupported derived artifact path outside generated manifest roots: {normalized}"
        ))
    }
}

fn trashed_source(batch: &Path, original: &str) -> PathBuf {
    let preserved = batch.join(original);
    if preserved.exists() {
        preserved
    } else {
        batch.join(original.strip_prefix("assets/").unwrap_or(original))
    }
}

fn rollback_moves(moved: Vec<(PathBuf, PathBuf)>) {
    for (source, destination) in moved.into_iter().rev() {
        if let Some(parent) = source.parent() {
            let _ = fs::create_dir_all(parent);
        }
        let _ = fs::rename(destination, source);
    }
}

fn restore_registry(path: &Path, bytes: Option<&[u8]>) {
    if let Some(bytes) = bytes {
        let _ = fs::write(path, bytes);
    } else {
        let _ = fs::remove_file(path);
    }
}

fn write_runtime_catalog(path: &Path, registry: &AssetRegistryDocument) -> Result<(), String> {
    let catalog = shared::RuntimeContentCatalog::from(registry);
    write_if_changed(path, &serialize_runtime_catalog(&catalog)?)?;
    Ok(())
}

fn trash_batch_id() -> String {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| format!("{}-{:09}", duration.as_secs(), duration.subsec_nanos()))
        .unwrap_or_else(|_| "now".into())
}

fn normalize(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn descendant(path: &str, parent: &str) -> bool {
    path == parent
        || path
            .strip_prefix(parent)
            .is_some_and(|suffix| suffix.starts_with('/'))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trash_and_restore_preserve_bytes_and_registry_ids() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-trash-{}",
            shared::AssetId::new().as_string()
        ));
        fs::create_dir_all(root.join("assets/Props")).unwrap();
        fs::create_dir_all(root.join("assets/.index")).unwrap();
        fs::write(root.join("assets/Props/box.png"), b"asset bytes").unwrap();
        let record = AssetRecord {
            id: shared::AssetId::new(),
            path: "assets/Props/box.png".into(),
            label: "Box".into(),
            kind: shared::AssetKind::Texture,
            source_fingerprint: None,
            import_settings: shared::AssetImportSettings::for_kind(shared::AssetKind::Texture),
            dependencies: Vec::new(),
        };
        let mut registry = AssetRegistryDocument {
            records: vec![record.clone()],
            ..Default::default()
        };
        fs::write(
            root.join(REGISTRY_PATH),
            serialize_registry(&registry).unwrap(),
        )
        .unwrap();

        let batch = trash_content(
            &root,
            &[PathBuf::from("assets/Props/box.png")],
            &mut registry,
        )
        .unwrap();
        assert!(!root.join("assets/Props/box.png").exists());
        assert!(registry.records.is_empty());
        restore_trash_batch(&root, &batch.path, &mut registry).unwrap();
        assert_eq!(
            fs::read(root.join("assets/Props/box.png")).unwrap(),
            b"asset bytes"
        );
        assert_eq!(registry.records, vec![record]);
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn trash_and_restore_include_model_derived_manifests() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-trash-derived-{}",
            shared::AssetId::new().as_string()
        ));
        fs::create_dir_all(root.join("assets/Props")).unwrap();
        fs::create_dir_all(root.join("assets/meshes/generated")).unwrap();
        fs::create_dir_all(root.join("assets/animations/generated")).unwrap();
        fs::create_dir_all(root.join("assets/.index")).unwrap();
        fs::write(root.join("assets/Props/desk.glb"), b"model bytes").unwrap();
        fs::write(
            root.join("assets/meshes/generated/desk.static_mesh.ron"),
            b"static manifest",
        )
        .unwrap();
        fs::write(
            root.join("assets/animations/generated/desk.animation.ron"),
            b"animation manifest",
        )
        .unwrap();
        let mut record = AssetRecord {
            id: shared::AssetId::new(),
            path: "assets/Props/desk.glb".into(),
            label: "Desk".into(),
            kind: shared::AssetKind::Model,
            source_fingerprint: None,
            import_settings: shared::AssetImportSettings::Model(shared::ImportSettings::default()),
            dependencies: Vec::new(),
        };
        record.model_import_mut().static_mesh_manifest_path =
            Some("assets/meshes/generated/desk.static_mesh.ron".into());
        record.model_import_mut().animation_manifest_path =
            Some("assets/animations/generated/desk.animation.ron".into());
        let mut registry = AssetRegistryDocument {
            records: vec![record.clone()],
            ..Default::default()
        };
        fs::write(
            root.join(REGISTRY_PATH),
            serialize_registry(&registry).unwrap(),
        )
        .unwrap();

        let batch = trash_content(
            &root,
            &[PathBuf::from("assets/Props/desk.glb")],
            &mut registry,
        )
        .unwrap();

        assert!(!root.join("assets/Props/desk.glb").exists());
        assert!(!root
            .join("assets/meshes/generated/desk.static_mesh.ron")
            .exists());
        assert!(!root
            .join("assets/animations/generated/desk.animation.ron")
            .exists());
        let manifest: TrashManifest = ron::from_str(
            &fs::read_to_string(root.join(&batch.path).join(TRASH_MANIFEST)).unwrap(),
        )
        .unwrap();
        assert_eq!(manifest.schema_version, 2);
        assert_eq!(
            manifest.derived_paths,
            vec![
                "assets/animations/generated/desk.animation.ron",
                "assets/meshes/generated/desk.static_mesh.ron",
            ]
        );

        restore_trash_batch(&root, &batch.path, &mut registry).unwrap();
        assert_eq!(
            fs::read(root.join("assets/Props/desk.glb")).unwrap(),
            b"model bytes"
        );
        assert_eq!(
            fs::read(root.join("assets/meshes/generated/desk.static_mesh.ron")).unwrap(),
            b"static manifest"
        );
        assert_eq!(
            fs::read(root.join("assets/animations/generated/desk.animation.ron")).unwrap(),
            b"animation manifest"
        );
        assert_eq!(registry.records, vec![record]);
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn restore_collision_keeps_trash_untouched() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-trash-{}",
            shared::AssetId::new().as_string()
        ));
        fs::create_dir_all(root.join("assets/Props")).unwrap();
        fs::create_dir_all(root.join("assets/.index")).unwrap();
        fs::write(root.join("assets/Props/box.png"), b"original").unwrap();
        let mut registry = AssetRegistryDocument::default();
        fs::write(
            root.join(REGISTRY_PATH),
            serialize_registry(&registry).unwrap(),
        )
        .unwrap();
        let batch = trash_content(
            &root,
            &[PathBuf::from("assets/Props/box.png")],
            &mut registry,
        )
        .unwrap();
        fs::write(root.join("assets/Props/box.png"), b"external").unwrap();

        assert!(restore_trash_batch(&root, &batch.path, &mut registry).is_err());
        assert_eq!(
            fs::read(root.join("assets/Props/box.png")).unwrap(),
            b"external"
        );
        assert!(root.join(&batch.path).join("assets/Props/box.png").exists());
        fs::remove_dir_all(root).unwrap();
    }
}
