Editor/SyncToolWindow.PreflightFix.cs
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

public partial class SyncToolWindow
{
	private async Task<bool> RunPreflightOrOfferFix( JsonElement payload, Action retryAfterFix, IEnumerable<string> endpointIds = null )
	{
		_busyItem = "preflight";
		_status = "Running preflight validation...";
		Update();

		var resp = await SyncToolApi.PreflightSync( payload, _publishTarget );
		if ( !resp.HasValue )
		{
			var errCode = SyncToolApi.LastErrorCode ?? "";
			var errMsg = SyncToolApi.LastErrorMessage ?? "";
			if ( IsBatchSyncEndpointUnavailable( errCode, errMsg ) )
			{
				_syncLog.Add( new SyncLogEntry { Name = "Preflight", Type = "Validation", Ok = true, Detail = "Skipped - backend preflight route missing" } );
				return true;
			}
			_syncLog.Add( new SyncLogEntry { Name = "Preflight", Type = "Validation", Ok = false, Detail = string.IsNullOrWhiteSpace( errMsg ) ? "Preflight failed" : errMsg } );
			return false;
		}

		var ok = !resp.Value.TryGetProperty( "ok", out var okProp ) || okProp.ValueKind == JsonValueKind.True;
		AppendPreflightLog( resp.Value );
		if ( ok ) return true;

		if ( HasMissingStepIdDiagnostics( resp.Value ) )
		{
			_status = "Some endpoint steps are missing IDs.";
			var plans = BuildStepIdFixPlans( endpointIds ).Where( p => p.HasChanges ).ToList();
			if ( plans.Count > 0 )
			{
				foreach ( var plan in plans )
					SetItemState( $"ep_{plan.ResourceId}", result: "FAIL", remoteDiffers: false, status: SyncStatus.LocalOnly, diffSummary: "Missing IDs - ready to auto-fix" );

				StepIdAutoFixWindow.Show( plans, () =>
				{
					RefreshFileList();
					retryAfterFix?.Invoke();
				} );
			}
			return false;
		}

		_status = "Preflight failed - fix validation errors before pushing";
		return false;
	}

	private JsonElement BuildPushAllPayload( out bool hasAny )
	{
		var localEps = SyncToolConfig.LoadSourcePayloadResources( "endpoint", includeDeprecated: false );
		var localCols = SyncToolConfig.LoadSourcePayloadResources( "collection" );
		var localWfs = SyncToolConfig.LoadSourcePayloadResources( "workflow" );
		hasAny = localEps.Count > 0 || localCols.Count > 0 || localWfs.Count > 0;
		var batchPayload = new Dictionary<string, object>();
		if ( localEps.Count > 0 ) batchPayload["endpoints"] = JsonSerializer.Deserialize<object>( JsonSerializer.Serialize( localEps ) );
		if ( localCols.Count > 0 ) batchPayload["collections"] = JsonSerializer.Deserialize<object>( JsonSerializer.Serialize( localCols ) );
		if ( localWfs.Count > 0 ) batchPayload["workflows"] = JsonSerializer.Deserialize<object>( JsonSerializer.Serialize( localWfs ) );
		return JsonSerializer.Deserialize<JsonElement>( JsonSerializer.Serialize( batchPayload ) );
	}

	private JsonElement BuildSinglePushPayload( string id )
	{
		var batchPayload = new Dictionary<string, object>();
		if ( id.StartsWith( "ep_" ) )
		{
			var slug = id[3..];
			var file = _endpointFiles.FirstOrDefault( f => ResourceIdFromFile( f, "endpoint" ) == slug );
			if ( file != null && SyncToolConfig.TryLoadSourcePayloadResource( "endpoint", file, out var ep, includeDeprecated: true ) )
				batchPayload["endpoints"] = new[] { JsonSerializer.Deserialize<object>( ep.GetRawText() ) };
		}
		else if ( id.StartsWith( "col_" ) )
		{
			var name = id[4..];
			var file = _collectionFiles.FirstOrDefault( f => ResourceIdFromFile( f, "collection" ) == name );
			if ( file != null && SyncToolConfig.TryLoadSourcePayloadResource( "collection", file, out var col ) )
				batchPayload["collections"] = new[] { JsonSerializer.Deserialize<object>( col.GetRawText() ) };
		}
		else if ( id.StartsWith( "wf_" ) )
		{
			var name = id[3..];
			var file = _workflowFiles.FirstOrDefault( f => ResourceIdFromFile( f, "workflow" ) == name );
			if ( file != null && SyncToolConfig.TryLoadSourcePayloadResource( "workflow", file, out var wf ) )
				batchPayload["workflows"] = new[] { JsonSerializer.Deserialize<object>( wf.GetRawText() ) };
		}
		return JsonSerializer.Deserialize<JsonElement>( JsonSerializer.Serialize( batchPayload ) );
	}

	private List<StepIdFixPlan> BuildStepIdFixPlans( IEnumerable<string> endpointIds = null )
	{
		var wanted = endpointIds?.ToHashSet( StringComparer.OrdinalIgnoreCase );
		var plans = new List<StepIdFixPlan>();
		foreach ( var file in GetActiveEndpointFiles() )
		{
			var slug = ResourceIdFromFile( file, "endpoint" );
			if ( wanted != null && !wanted.Contains( slug ) ) continue;
			plans.Add( StepIdAutoFixer.BuildPlan( file, slug ) );
		}
		return plans;
	}

	private static bool HasMissingStepIdDiagnostics( JsonElement payload )
	{
		foreach ( var diagnostic in EnumerateDiagnostics( payload ) )
		{
			var text = diagnostic.ToString();
			if ( text.Contains( "steps[", StringComparison.OrdinalIgnoreCase ) && text.Contains( ".id", StringComparison.OrdinalIgnoreCase ) ) return true;
			if ( text.Contains( "non-empty string", StringComparison.OrdinalIgnoreCase ) && text.Contains( "id", StringComparison.OrdinalIgnoreCase ) ) return true;
		}
		return false;
	}

	private static IEnumerable<JsonElement> EnumerateDiagnostics( JsonElement payload )
	{
		if ( payload.ValueKind != JsonValueKind.Object ) yield break;
		foreach ( var prop in payload.EnumerateObject() )
		{
			if ( prop.NameEquals( "diagnostics" ) && prop.Value.ValueKind == JsonValueKind.Array )
				foreach ( var d in prop.Value.EnumerateArray() ) yield return d;
			if ( prop.Value.ValueKind == JsonValueKind.Object )
				foreach ( var d in EnumerateDiagnostics( prop.Value ) ) yield return d;
			if ( prop.Value.ValueKind == JsonValueKind.Array )
				foreach ( var item in prop.Value.EnumerateArray() )
					foreach ( var d in EnumerateDiagnostics( item ) ) yield return d;
		}
	}

	private void AppendPreflightLog( JsonElement preflight )
	{
		if ( preflight.TryGetProperty( "summary", out var summary ) && summary.ValueKind == JsonValueKind.Object )
		{
			var failed = summary.TryGetProperty( "failed", out var f ) && f.TryGetInt32( out var n ) ? n : 0;
			var total = summary.TryGetProperty( "total", out var t ) && t.TryGetInt32( out var tn ) ? tn : 0;
			_syncLog.Add( new SyncLogEntry { Name = "Preflight", Type = "Validation", Ok = failed == 0, Detail = failed == 0 ? $"Ready to push ({total})" : $"{failed}/{total} failed" } );
		}
	}

	private static bool IsResourceSyncLog( SyncLogEntry entry ) => entry.Type == "Endpoint" || entry.Type == "Collection" || entry.Type == "Workflow";
}