Editor/Serialization/IR/IRReader.cs
using System;
using System.IO;
using System.Text.Json;
using Grains.RazorDesigner.Document;

namespace Grains.RazorDesigner.Serialization.IR;

public static class IRReader
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	public static DesignerDocument ReadDocument( string json )
	{
		if ( string.IsNullOrEmpty( json ) )
			throw new ArgumentException( $"{LogPrefix} IRReader.ReadDocument: json is null or empty", nameof( json ) );

		Log.Info( $"{LogPrefix} IRReader.ReadDocument: deserialising ({json.Length} chars)" );

		var envelope = JsonSerializer.Deserialize<IRDocumentEnvelope>( json, DesignerIRJson.Options );
		if ( envelope is null )
			throw new InvalidDataException( $"{LogPrefix} IRReader.ReadDocument: deserialised envelope is null" );

		// Schema migration — throws on future version; no-ops if schemaVersion == Current.
		SchemaMigrations.Migrate( ref envelope );

		// Schema-level validation — rejects missing root node; typed wiring is self-validating.
		ValidateSchema( envelope );

		var doc = FromEnvelope( envelope );
		Log.Info( $"{LogPrefix} IRReader.ReadDocument: built document (root children: {doc.RootRecord.Children.Count})" );
		return doc;
	}


	private static void ValidateSchema( IRDocumentEnvelope env )
	{
		if ( env.Root is null )
			throw new InvalidDataException( $"{LogPrefix} IRReader: 'root' node is missing from the envelope." );
	}


	private static DesignerDocument FromEnvelope( IRDocumentEnvelope env )
	{
		var doc = new DesignerDocument();

		CopyEnvelopeAppearanceAndPayloadOnto( env.Root, doc.RootRecord );

		// Recurse regular children (non-slot).
		foreach ( var child in env.Root.Children )
			doc.RootRecord.Children.Add( BuildRecord( child ) );

		// Slot children of the root node (unlikely for a Panel root, but handled generically).
		foreach ( var kvp in env.Root.Slots )
		{
			var built = BuildRecord( kvp.Value );
			built.IsSlot  = true;
			built.SlotName = kvp.Key;
			doc.RootRecord.Children.Add( built );
		}

		doc.SeedCounters();

		doc.Wiring = env.Wiring ?? Grains.RazorDesigner.Wiring.WiringEnvelope.Empty;

		return doc;
	}

	private static ControlRecord BuildRecord( IRNodeEnvelope node )
	{
		// Id is init-only — set it in the object initialiser so the persisted GUID is preserved.
		var record = new ControlRecord
		{
			Type       = node.Kind,
			Id         = node.Id,
			ClassName  = node.ClassName,
			// Appearance is init-only — set it here in the object initialiser.
			Appearance = node.Appearance,
		};

		// Unpack payload fields from the deserialised Payload leaf back into the raw auto-props.
		UnpackPayload( node.Payload, record );

		// Recurse regular (non-slot) children.
		foreach ( var child in node.Children )
			record.Children.Add( BuildRecord( child ) );

		// Rebuild slot children from the Slots dict (e.g. SplitContainer "left"/"right").
		foreach ( var kvp in node.Slots )
		{
			var slot = BuildRecord( kvp.Value );
			slot.IsSlot  = true;
			slot.SlotName = kvp.Key;

			// The slot record's own children were already handled by the recursive BuildRecord above.
			record.Children.Add( slot );
		}

		// Populate per-state style overrides from the States array (omitted in JSON when empty).
		if ( node.States is { Count: > 0 } )
		{
			foreach ( var s in node.States )
			{
				record.StateRules.Add( new StateRule
				{
					State        = s.State,
					NthChildMode = s.NthChildMode,
					NthChildArg  = s.NthChildArg,
					Delta        = s.Delta,
				} );
			}
		}

		if ( node.Bindings is { Count: > 0 } )
			record.Bindings.AddRange( node.Bindings );

		return record;
	}

	private static void CopyEnvelopeAppearanceAndPayloadOnto( IRNodeEnvelope node, ControlRecord target )
	{
		var scratch = new ControlRecord
		{
			Type       = target.Type,
			Id         = target.Id,
			ClassName  = target.ClassName,
			Appearance = node.Appearance,
		};

		// Load payload fields into the scratch record first.
		UnpackPayload( node.Payload, scratch );

		if ( node.Bindings is { Count: > 0 } )
			scratch.Bindings.AddRange( node.Bindings );

		scratch.CopyFieldsTo( target );

		Log.Info( $"{LogPrefix} IRReader: applied envelope root appearance to RootRecord (ClassName={target.ClassName})" );
	}

	private static void UnpackPayload( Payload payload, ControlRecord record )
	{
		if ( payload is null ) return;

		record.Content      = payload.Content;
		record.Placeholder  = payload.Placeholder;
		record.Source       = payload.Source;
		record.IconName     = payload.IconName;
		record.CheckboxSize = payload.CheckboxSize;
	}
}