Editor/FabImportHandler.cs
using System;
using System.IO;
using FabBridge.Converters;

namespace FabBridge;

/// <summary>
/// Handles the complete import process for Fab assets
/// </summary>
public class FabImportHandler
{
	/// <summary>
	/// Result of a complete asset import
	/// </summary>
	public class ImportResult
	{
		public bool Success { get; set; }
		public string AssetName { get; set; }
		public List<FabTextureConverter.ConversionResult> TextureResults { get; set; } = new();
		public List<FabModelConverter.ConversionResult> ModelResults { get; set; } = new();
		public FabMaterialConverter.ConversionResult MaterialResult { get; set; }
		public FabDecalConverter.ConversionResult DecalResult { get; set; }
		public List<string> Errors { get; set; } = new();
		public DateTime ImportTime { get; set; }
	}

	/// <summary>
	/// Event raised when an import starts
	/// </summary>
	public event Action<FabAsset> OnImportStarted;

	/// <summary>
	/// Event raised when an import completes
	/// </summary>
	public event Action<ImportResult> OnImportCompleted;

	/// <summary>
	/// Event raised for progress updates
	/// </summary>
	public event Action<string> OnProgressUpdate;

	/// <summary>
	/// The base folder for imported assets (relative to project assets)
	/// </summary>
	public string ImportFolder { get; set; } = "fab_imports";

	/// <summary>
	/// Whether to automatically create materials
	/// </summary>
	public bool CreateMaterials { get; set; } = true;

	/// <summary>
	/// Whether to automatically convert models
	/// </summary>
	public bool ConvertModels { get; set; } = true;

	/// <summary>
	/// Whether the converter should append a PhysicsHullFromRender block to each generated VMDL.
	/// Convex-hull collision is the standard engine-generated collision for props/rocks.
	/// </summary>
	public bool GenerateCollisions { get; set; } = true;

	/// <summary>
	/// Whether to scan Fab's sibling quality-tier folders (high / mid / low) for additional LOD
	/// meshes and bundle them into the VMDL. When off, only the primary mesh that Bridge sent is used.
	/// </summary>
	public bool DetectLods { get; set; } = true;

	/// <summary>
	/// Import all assets from an export data package
	/// </summary>
	public async Task<List<ImportResult>> ImportAllAsync( FabExportData exportData )
	{
		var results = new List<ImportResult>();

		foreach ( var asset in exportData.Assets )
		{
			var result = await ImportAssetAsync( asset );
			results.Add( result );
		}

		return results;
	}

	/// <summary>
	/// Import a single Fab asset
	/// </summary>
	public async Task<ImportResult> ImportAssetAsync( FabAsset fabAsset )
	{
		var result = new ImportResult
		{
			AssetName = fabAsset.Name ?? fabAsset.Id ?? "Unknown",
			ImportTime = DateTime.Now
		};

		// Debug logging for the asset
		Log.Info( $"FabBridge: === Starting import ===" );
		Log.Info( $"FabBridge: Asset ID: {fabAsset.Id}" );
		Log.Info( $"FabBridge: Asset Name: {fabAsset.Name}" );
		Log.Info( $"FabBridge: Display Name: {fabAsset.GetDisplayName()}" );
		Log.Info( $"FabBridge: Asset Type: {fabAsset.Type}" );
		Log.Info( $"FabBridge: Asset Category: {fabAsset.Category}" );
		Log.Info( $"FabBridge: Asset Path: {fabAsset.Path}" );

		// Use the new aggregated methods
		var allMeshes = fabAsset.GetAllMeshes();
		var allTextures = fabAsset.GetAllTextures();

		Log.Info( $"FabBridge: Total meshes (from GetAllMeshes): {allMeshes.Count}" );
		Log.Info( $"FabBridge: Total textures (from GetAllTextures): {allTextures.Count}" );
		Log.Info( $"FabBridge: Meshes array count: {fabAsset.Meshes?.Count ?? 0}" );
		Log.Info( $"FabBridge: MeshList array count: {fabAsset.MeshList?.Count ?? 0}" );
		Log.Info( $"FabBridge: Materials array count: {fabAsset.Materials?.Count ?? 0}" );
		Log.Info( $"FabBridge: Components count: {fabAsset.Components?.Count ?? 0}" );

		// Log mesh details
		foreach ( var mesh in allMeshes )
		{
			Log.Info( $"FabBridge: Mesh - Name: {mesh.Name}, File: {mesh.GetFilePath()}" );
		}

		// Log texture details
		foreach ( var tex in allTextures )
		{
			Log.Info( $"FabBridge: Texture - Type: {tex.Type}, Path: {tex.Path}" );
		}

		// Log materials info
		if ( fabAsset.Materials != null )
		{
			foreach ( var mat in fabAsset.Materials )
			{
				var textureCount = mat.Textures?.Count ?? 0;
				var textureTypes = mat.Textures != null ? string.Join( ", ", mat.Textures.Keys ) : "none";
				Log.Info( $"FabBridge: Material - Name: {mat.Name}, Textures: {textureCount} ({textureTypes})" );
			}
		}

		try
		{
			OnImportStarted?.Invoke( fabAsset );
			OnProgressUpdate?.Invoke( $"Importing {result.AssetName}..." );

			// Get the project assets path
			var project = Project.Current;
			if ( project == null )
			{
				Log.Error( "FabBridge: No project loaded!" );
				result.Errors.Add( "No project loaded" );
				return result;
			}

			var assetsPath = project.GetAssetsPath();
			Log.Info( $"FabBridge: Project assets path: {assetsPath}" );

			var assetBaseName = fabAsset.GetSafeFileName();
			Log.Info( $"FabBridge: Safe file name: {assetBaseName}" );

			// Resolve a folder unique to this asset id so two different assets with the same
			// display name (Fab does this — e.g. multiple "Nordic Beach Rock Formation" entries)
			// don't stomp each other. Same asset id reuses the same folder, so re-imports update
			// rather than create duplicates.
			var importBasePath = ResolveUniqueAssetFolder( Path.Combine( assetsPath, ImportFolder ), assetBaseName, fabAsset.Id );
			var materialsPath = Path.Combine( importBasePath, "materials" );
			var modelsPath = Path.Combine( importBasePath, "models" );

			Log.Info( $"FabBridge: Import base path: {importBasePath}" );
			Log.Info( $"FabBridge: Materials path: {materialsPath}" );
			Log.Info( $"FabBridge: Models path: {modelsPath}" );

			Directory.CreateDirectory( materialsPath );
			Directory.CreateDirectory( modelsPath );
			Log.Info( "FabBridge: Created directories" );

			// Drop a marker so the next import can recognise this folder as belonging to this asset.
			WriteAssetIdMarker( importBasePath, fabAsset.Id );

			// Step 1: Import textures
			OnProgressUpdate?.Invoke( $"Converting textures for {result.AssetName}..." );
			await Task.Yield(); // Allow UI to update

			Log.Info( "FabBridge: Starting texture conversion..." );
			result.TextureResults = FabTextureConverter.ConvertAllTextures( fabAsset, materialsPath );
			Log.Info( $"FabBridge: Texture conversion complete. Results: {result.TextureResults.Count}" );

			foreach ( var texResult in result.TextureResults )
			{
				Log.Info( $"FabBridge: Texture result - Success: {texResult.Success}, Path: {texResult.DestinationPath}, Error: {texResult.Error}" );
				if ( !texResult.Success && !string.IsNullOrEmpty( texResult.Error ) )
				{
					result.Errors.Add( $"Texture: {texResult.Error}" );
				}
			}

			// Decide once whether this asset is a decal — texture-only with an opacity mask. A decal
			// asset gets a .decal resource and nothing else; a regular asset gets a vmat.
			var hasSuccessfulTextures = result.TextureResults.Any( t => t.Success );
			var isDecal = hasSuccessfulTextures && FabDecalConverter.LooksLikeDecal( fabAsset, result.TextureResults );
			Log.Info( $"FabBridge: hasSuccessfulTextures={hasSuccessfulTextures}, isDecal={isDecal}, CreateMaterials={CreateMaterials}" );

			if ( hasSuccessfulTextures )
			{
				// Wait for textures to finish compiling BEFORE writing the vmat/decal. If we don't,
				// s&box compiles the dependent first, discovers its texture deps are out of date,
				// and spams on-demand recompiles for both.
				var pendingTextureAssets = result.TextureResults
					.Where( t => t.Success && t.Asset != null )
					.Select( t => t.Asset )
					.ToList();

				if ( pendingTextureAssets.Count > 0 )
				{
					OnProgressUpdate?.Invoke( $"Compiling {pendingTextureAssets.Count} texture(s)..." );
					Log.Info( $"FabBridge: Waiting for {pendingTextureAssets.Count} texture(s) to compile..." );

					foreach ( var texAsset in pendingTextureAssets )
					{
						try { await texAsset.CompileIfNeededAsync(); }
						catch ( Exception compileEx ) { Log.Warning( $"FabBridge: Texture compile failed for {texAsset.Path}: {compileEx.Message}" ); }
					}

					await MainThread.Wait();
					Log.Info( "FabBridge: Texture compilation complete" );
				}
			}

			if ( isDecal )
			{
				OnProgressUpdate?.Invoke( $"Creating decal for {result.AssetName}..." );
				await Task.Yield();

				Log.Info( "FabBridge: Asset detected as a decal — generating .decal resource" );
				result.DecalResult = FabDecalConverter.Generate( fabAsset, result.TextureResults, materialsPath );
				Log.Info( $"FabBridge: Decal result - Success: {result.DecalResult?.Success}, Path: {result.DecalResult?.DecalPath}, Error: {result.DecalResult?.Error}" );

				if ( !result.DecalResult.Success && !string.IsNullOrEmpty( result.DecalResult.Error ) )
					result.Errors.Add( $"Decal: {result.DecalResult.Error}" );
			}
			else if ( CreateMaterials && hasSuccessfulTextures )
			{
				OnProgressUpdate?.Invoke( $"Creating material for {result.AssetName}..." );
				await Task.Yield();

				Log.Info( "FabBridge: Starting material generation..." );
				result.MaterialResult = FabMaterialConverter.GenerateFromFabAsset(
					fabAsset,
					result.TextureResults,
					materialsPath
				);
				Log.Info( $"FabBridge: Material result - Success: {result.MaterialResult?.Success}, Path: {result.MaterialResult?.VmatPath}, Error: {result.MaterialResult?.Error}" );

				if ( !result.MaterialResult.Success && !string.IsNullOrEmpty( result.MaterialResult.Error ) )
				{
					result.Errors.Add( $"Material: {result.MaterialResult.Error}" );
				}
			}

			// Step 3: Import models
			var meshesToConvert = fabAsset.GetAllMeshes();
			var hasMeshes = meshesToConvert.Count > 0;
			var hasLods = fabAsset.LodList?.Count > 0;
			var hasMeshComponents = fabAsset.Components?.Any( c => IsMeshFile( c.Path ) ) ?? false;
			Log.Info( $"FabBridge: ConvertModels={ConvertModels}, hasMeshes={hasMeshes} (count={meshesToConvert.Count}), hasLods={hasLods}, hasMeshComponents={hasMeshComponents}" );

			if ( ConvertModels && (hasMeshes || hasLods || hasMeshComponents) )
			{
				OnProgressUpdate?.Invoke( $"Converting models for {result.AssetName}..." );
				await Task.Yield();

				Log.Info( "FabBridge: Starting model conversion..." );
				result.ModelResults = FabModelConverter.ConvertAllMeshes( fabAsset, modelsPath, GenerateCollisions, DetectLods );
				Log.Info( $"FabBridge: Model conversion complete. Results: {result.ModelResults.Count}" );

				foreach ( var modelResult in result.ModelResults )
				{
					Log.Info( $"FabBridge: Model result - Success: {modelResult.Success}, Path: {modelResult.VmdlPath}, Error: {modelResult.Error}" );
					if ( !modelResult.Success && !string.IsNullOrEmpty( modelResult.Error ) )
					{
						result.Errors.Add( $"Model: {modelResult.Error}" );
					}
				}

				// Step 4: Set default material on models
				if ( result.MaterialResult?.Success == true && result.ModelResults.Any( m => m.Success ) )
				{
					OnProgressUpdate?.Invoke( $"Assigning material to models..." );
					await Task.Yield();

					Log.Info( "FabBridge: Assigning default material to models..." );
					foreach ( var modelResult in result.ModelResults.Where( m => m.Success ) )
					{
						var materialSet = FabModelConverter.SetDefaultMaterial( modelResult, result.MaterialResult );
						Log.Info( $"FabBridge: Set material on {modelResult.VmdlPath}: {materialSet}" );
					}
				}
			}

			// Determine overall success
			result.Success = result.TextureResults.Any( t => t.Success ) ||
			                 result.ModelResults.Any( m => m.Success ) ||
			                 (result.MaterialResult?.Success ?? false) ||
			                 (result.DecalResult?.Success ?? false);

			Log.Info( $"FabBridge: === Import complete ===" );
			Log.Info( $"FabBridge: Success: {result.Success}" );
			Log.Info( $"FabBridge: Errors: {string.Join( ", ", result.Errors )}" );

			OnProgressUpdate?.Invoke( result.Success
				? $"Successfully imported {result.AssetName}"
				: $"Failed to import {result.AssetName}" );
		}
		catch ( Exception ex )
		{
			result.Errors.Add( ex.Message );
			Log.Error( $"FabBridge: Import failed for {result.AssetName}: {ex.Message}" );
			Log.Error( $"FabBridge: Stack trace: {ex.StackTrace}" );
		}

		OnImportCompleted?.Invoke( result );
		return result;
	}
	

	/// <summary>
	/// Check if a file path is a mesh file
	/// </summary>
	private static bool IsMeshFile( string path )
	{
		if ( string.IsNullOrEmpty( path ) )
			return false;

		var ext = Path.GetExtension( path )?.ToLowerInvariant();
		return ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" || ext == ".dae";
	}

	/// <summary>Marker file written into each asset folder to identify which Fab asset id owns it.</summary>
	private const string AssetIdMarkerName = ".fab_asset_id";

	/// <summary>
	/// Pick a folder under <paramref name="parentPath"/> for this asset. Tries the clean
	/// `{baseName}` first, then `{baseName}_{sanitisedId}`, then numeric suffixes — skipping any
	/// folder that already belongs to a different Fab asset (per its <see cref="AssetIdMarkerName"/>
	/// marker). A folder whose marker matches this asset's id is reused so re-imports update in place.
	/// </summary>
	private static string ResolveUniqueAssetFolder( string parentPath, string baseName, string assetId )
	{
		var candidate = Path.Combine( parentPath, baseName );
		if ( IsAvailableOrSameAsset( candidate, assetId ) )
			return candidate;

		if ( !string.IsNullOrEmpty( assetId ) )
		{
			var safeId = SanitizeIdForFolder( assetId );
			if ( !string.IsNullOrEmpty( safeId ) )
			{
				candidate = Path.Combine( parentPath, $"{baseName}_{safeId}" );
				if ( IsAvailableOrSameAsset( candidate, assetId ) )
					return candidate;
			}
		}

		// Last resort: numeric suffix. Stops well below int.MaxValue but high enough that hitting
		// the cap means something is very wrong.
		for ( int i = 2; i < 1000; i++ )
		{
			candidate = Path.Combine( parentPath, $"{baseName}_{i}" );
			if ( IsAvailableOrSameAsset( candidate, assetId ) )
				return candidate;
		}

		// Should never happen in practice — fall back to a guid-tail to guarantee uniqueness.
		var fallback = Path.Combine( parentPath, $"{baseName}_{Guid.NewGuid():N}".Substring( 0, parentPath.Length + baseName.Length + 9 ) );
		Log.Warning( $"FabBridge: Exhausted disambiguation slots, falling back to {fallback}" );
		return fallback;
	}

	/// <summary>
	/// True if the folder doesn't exist yet, or it exists and its marker says it already belongs to
	/// <paramref name="assetId"/>. A pre-existing folder with no marker is treated as foreign — we
	/// won't write into it.
	/// </summary>
	private static bool IsAvailableOrSameAsset( string folder, string assetId )
	{
		if ( !Directory.Exists( folder ) )
			return true;

		var marker = Path.Combine( folder, AssetIdMarkerName );
		if ( !File.Exists( marker ) )
			return false;

		try
		{
			var existing = File.ReadAllText( marker ).Trim();
			return !string.IsNullOrEmpty( existing ) && existing == (assetId ?? "");
		}
		catch
		{
			return false;
		}
	}

	/// <summary>
	/// Sanitise an asset id into something safe for a folder suffix. Keeps it short so the path
	/// doesn't blow up Windows' MAX_PATH limits on deeply nested assets.
	/// </summary>
	private static string SanitizeIdForFolder( string id )
	{
		if ( string.IsNullOrEmpty( id ) )
			return "";

		var result = id.ToLowerInvariant();
		foreach ( var c in Path.GetInvalidFileNameChars() )
			result = result.Replace( c, '_' );
		result = result.Replace( ' ', '_' ).Replace( '-', '_' );

		// Cap length — Fab ids are sometimes long GUIDs and we just need enough to disambiguate.
		if ( result.Length > 12 )
			result = result.Substring( 0, 12 );

		return result;
	}

	private static void WriteAssetIdMarker( string folder, string assetId )
	{
		try
		{
			var marker = Path.Combine( folder, AssetIdMarkerName );
			File.WriteAllText( marker, assetId ?? "" );
		}
		catch ( Exception ex )
		{
			Log.Warning( $"FabBridge: Failed to write asset id marker: {ex.Message}" );
		}
	}
}