Editor/MergeViewWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Editor;

/// <summary>
/// Shows additive remote-only fields after a push or remote check so the user can
/// review and pull those semantics into their local files.
/// </summary>
public class MergeViewWindow : DockWindow
{
	public struct FieldDiff
	{
		public string Name;
		public string LocalValue;
		public string RemoteValue;
		public string Reason;
		public bool IsAdded;
	}

	private readonly string _resourceName;
	private readonly string _resourceType;
	private readonly List<FieldDiff> _addedFields;
	private readonly List<FieldDiff> _changedFields;
	private readonly Action _onMerge;
	// Optional inspect-only callback. When set, the window renders a
	// "View Diff" button so reviewers can drill into the full local-vs-remote
	// content before deciding whether to pull. Useful when the additive-fields
	// summary keeps reappearing after pull/push/sync and the reviewer wants
	// to confirm what the classifier is actually seeing.
	private readonly Action _onViewDiff;
	private Vector2 _mousePos;
	private float _scroll;
	private Rect _cancelRect;
	private Rect _viewDiffRect;
	private Rect _mergeRect;

	private const float LineH = 16f;
	private const float FieldBlockH = 56f;

	private static readonly Dictionary<string, string> Explanations = new()
	{
		["rateLimits"] = "Rate limit configuration. Controls how many saves per time period are allowed.",
		["rateLimitAction"] = "Action when rate limit is exceeded. 'reject' returns an error, 'clamp' caps values.",
		["webhookOnRateLimit"] = "Discord webhook notification when a rate limit is triggered.",
		["version"] = "Collection API version. Set to v3 for all new collections.",
		["accessMode"] = "Controls whether the collection is publicly readable or endpoint-only.",
		["maxRecords"] = "Maximum number of records per player in this collection.",
		["allowRecordDelete"] = "Whether players can delete their own records.",
		["requireSaveVersion"] = "Version tracking for conflict detection on saves.",
		["collectionType"] = "Whether data is stored per-player or globally.",
		["input"] = "Input validation schema. Defines what parameters the endpoint accepts.",
		["description"] = "Human-readable description of this resource.",
		["notes"] = "Editor notes saved with this resource.",
		["builtIn"] = "System flag added by the remote project metadata.",
		["enabled"] = "Whether this resource is active.",
	};

	public MergeViewWindow( string resourceName, string resourceType,
		List<FieldDiff> addedFields, List<FieldDiff> changedFields,
		Action onMerge, Action onViewDiff = null )
	{
		_resourceName = resourceName;
		_resourceType = resourceType;
		_addedFields = addedFields;
		_changedFields = changedFields;
		_onMerge = onMerge;
		_onViewDiff = onViewDiff;
		Title = $"Pull Remote Semantics - {resourceName}";

		var fieldCount = addedFields.Count + changedFields.Count;
		Size = new Vector2( 540, Math.Min( 640, 260 + fieldCount * FieldBlockH ) );
		MinimumSize = new Vector2( 420, 280 );
	}

	/// <summary>
	/// Compare local and remote JSON to identify additive remote fields versus real content changes.
	/// Returns (addedFields, changedFields, isRemoteAdditiveOnly).
	/// </summary>
	public static (List<FieldDiff> Added, List<FieldDiff> Changed, bool IsRemoteAdditiveOnly) AnalyzeDifferences(
		string localJson, string remoteJson )
	{
		var analysis = JsonDiffUtilities.Analyze( localJson, remoteJson );

		return (
			analysis.Added.Select( ToFieldDiff ).ToList(),
			analysis.Changed.Select( ToFieldDiff ).ToList(),
			analysis.IsRemoteAdditiveOnly
		);
	}

	private float ContentHeight => 180 + ( _addedFields.Count + _changedFields.Count ) * FieldBlockH + 80;
	private float MaxScroll => Math.Max( 0, ContentHeight - Height + 40 );

	protected override void OnPaint()
	{
		base.OnPaint();

		var pad = 20f;
		var w = Width - pad * 2;
		var y = 20f - _scroll;

		Paint.SetDefaultFont( size: 13, weight: 700 );
		Paint.SetPen( Color.White );
		Paint.DrawText( new Rect( pad, y, w, 22 ), $"Pull Remote Semantics - {_resourceName}", TextFlag.LeftCenter );
		y += 30;

		Paint.SetDefaultFont( size: 10 );
		Paint.SetPen( Color.White.WithAlpha( 0.75f ) );
		DrawWrappedText( pad, ref y, w,
			$"The remote version of this {_resourceType} contains additive fields only. " +
			"Your local content still matches what is stored remotely. Review the remote-only semantics below and pull them into your local files if you want to keep those additions locally." );
		y += 12;

		Paint.SetDefaultFont( size: 9, weight: 600 );
		var sx = pad;
		if ( _addedFields.Count > 0 )
		{
			Paint.SetPen( Color.Green.WithAlpha( 0.8f ) );
			Paint.DrawText( new Rect( sx, y, 100, 14 ), $"+{_addedFields.Count} added", TextFlag.LeftCenter );
			sx += 80;
		}
		if ( _changedFields.Count > 0 )
		{
			Paint.SetPen( Color.Yellow.WithAlpha( 0.8f ) );
			Paint.DrawText( new Rect( sx, y, 110, 14 ), $"~{_changedFields.Count} changed", TextFlag.LeftCenter );
		}
		y += 22;

		Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
		Paint.DrawLine( new Vector2( pad, y ), new Vector2( pad + w, y ) );
		y += 8;

		if ( _addedFields.Count > 0 )
		{
			Paint.SetDefaultFont( size: 9, weight: 700 );
			Paint.SetPen( Color.Green.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( pad, y, w, 16 ), "ADDED ON REMOTE", TextFlag.LeftCenter );
			y += 22;

			foreach ( var field in _addedFields )
				DrawFieldBlock( pad, w, ref y, field, Color.Green );
		}

		if ( _changedFields.Count > 0 )
		{
			Paint.SetDefaultFont( size: 9, weight: 700 );
			Paint.SetPen( Color.Yellow.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( pad, y, w, 16 ), "DIFFERENT ON REMOTE", TextFlag.LeftCenter );
			y += 22;

			foreach ( var field in _changedFields )
				DrawFieldBlock( pad, w, ref y, field, Color.Yellow );
		}

		y += 16;

		Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
		Paint.DrawLine( new Vector2( pad, y ), new Vector2( pad + w, y ) );
		y += 12;

		var btnH = 34f;
		var gapW = 12f;
		var showViewDiff = _onViewDiff != null;

		// Layout: Cancel | (View Diff)? | Pull Remote Semantics
		// View Diff is optional so the inspect-only callback stays opt-in for
		// callers that don't have YAML strings to hand over.
		var cancelW = 80f;
		var viewDiffW = showViewDiff ? 100f : 0f;
		var viewDiffSlot = showViewDiff ? viewDiffW + gapW : 0f;
		var mergeW = w - cancelW - viewDiffSlot - gapW;

		_cancelRect = new Rect( pad, y, cancelW, btnH );
		var cancelHovered = _cancelRect.IsInside( _mousePos );
		Paint.SetBrush( Color.White.WithAlpha( cancelHovered ? 0.1f : 0.04f ) );
		Paint.SetPen( Color.White.WithAlpha( cancelHovered ? 0.3f : 0.15f ) );
		Paint.DrawRect( _cancelRect, 4 );
		Paint.SetDefaultFont( size: 11, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( cancelHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( _cancelRect, "Cancel", TextFlag.Center );

		var mergeX = pad + cancelW + gapW;
		if ( showViewDiff )
		{
			_viewDiffRect = new Rect( mergeX, y, viewDiffW, btnH );
			var diffHovered = _viewDiffRect.IsInside( _mousePos );
			Paint.SetBrush( Color.Cyan.WithAlpha( diffHovered ? 0.18f : 0.06f ) );
			Paint.SetPen( Color.Cyan.WithAlpha( diffHovered ? 0.55f : 0.25f ) );
			Paint.DrawRect( _viewDiffRect, 4 );
			Paint.SetDefaultFont( size: 11, weight: 600 );
			Paint.SetPen( Color.Cyan.WithAlpha( diffHovered ? 1f : 0.75f ) );
			Paint.DrawText( _viewDiffRect, "View Diff", TextFlag.Center );
			mergeX += viewDiffW + gapW;
		}

		_mergeRect = new Rect( mergeX, y, mergeW, btnH );
		var mergeHovered = _mergeRect.IsInside( _mousePos );
		Paint.SetBrush( Color.Green.WithAlpha( mergeHovered ? 0.25f : 0.12f ) );
		Paint.SetPen( Color.Green.WithAlpha( mergeHovered ? 0.6f : 0.3f ) );
		Paint.DrawRect( _mergeRect, 4 );
		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.Green.WithAlpha( mergeHovered ? 1f : 0.85f ) );
		Paint.DrawText( _mergeRect, "Pull Remote Semantics", TextFlag.Center );
	}

	private void DrawFieldBlock( float pad, float w, ref float y, FieldDiff field, Color accentColor )
	{
		Paint.SetBrush( accentColor.WithAlpha( 0.04f ) );
		Paint.SetPen( accentColor.WithAlpha( 0.12f ) );
		Paint.DrawRect( new Rect( pad, y, w, FieldBlockH - 6 ), 4 );

		Paint.SetDefaultFont( size: 10, weight: 600 );
		Paint.SetPen( accentColor.WithAlpha( 0.9f ) );
		var prefix = field.IsAdded ? "+" : "~";
		Paint.DrawText( new Rect( pad + 10, y + 4, w - 20, 16 ), $"{prefix} {field.Name}", TextFlag.LeftCenter );

		Paint.SetDefaultFont( size: 9 );
		if ( field.IsAdded )
		{
			Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( pad + 10, y + 20, w - 20, 14 ), field.RemoteValue, TextFlag.LeftCenter );
		}
		else
		{
			Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
			var valueText = $"{field.LocalValue ?? "null"} -> {field.RemoteValue}";
			Paint.DrawText( new Rect( pad + 10, y + 20, w - 20, 14 ), valueText, TextFlag.LeftCenter );
		}

		Paint.SetDefaultFont( size: 8 );
		Paint.SetPen( Color.White.WithAlpha( 0.4f ) );
		Paint.DrawText( new Rect( pad + 10, y + 34, w - 20, 12 ), field.Reason, TextFlag.LeftCenter );

		y += FieldBlockH;
	}

	private void DrawWrappedText( float x, ref float y, float maxW, string text )
	{
		var words = text.Split( ' ' );
		var line = "";
		foreach ( var word in words )
		{
			var test = string.IsNullOrEmpty( line ) ? word : $"{line} {word}";
			if ( test.Length * 6.2f > maxW && !string.IsNullOrEmpty( line ) )
			{
				Paint.DrawText( new Rect( x, y, maxW, 16 ), line, TextFlag.LeftCenter );
				y += 17;
				line = word;
			}
			else
			{
				line = test;
			}
		}

		if ( !string.IsNullOrEmpty( line ) )
		{
			Paint.DrawText( new Rect( x, y, maxW, 16 ), line, TextFlag.LeftCenter );
			y += 17;
		}
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		if ( _cancelRect.IsInside( e.LocalPosition ) )
		{
			Close();
		}
		else if ( _onViewDiff != null && _viewDiffRect.IsInside( e.LocalPosition ) )
		{
			// Inspect-only — keep the merge window open so the reviewer can
			// flip back and forth between the field summary and the full diff.
			_onViewDiff();
		}
		else if ( _mergeRect.IsInside( e.LocalPosition ) )
		{
			Close();
			_onMerge?.Invoke();
		}
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		_mousePos = e.LocalPosition;
		Update();
	}

	protected override void OnMouseWheel( WheelEvent e )
	{
		var direction = e.Delta > 0 ? -1 : 1;
		_scroll = Math.Clamp( _scroll + direction * LineH * 3, 0, MaxScroll );
		Update();
		e.Accept();
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		switch ( e.Key )
		{
			case KeyCode.Escape: Close(); break;
			case KeyCode.Up: _scroll = Math.Max( 0, _scroll - LineH ); Update(); break;
			case KeyCode.Down: _scroll = Math.Min( MaxScroll, _scroll + LineH ); Update(); break;
			case KeyCode.PageUp: _scroll = Math.Max( 0, _scroll - LineH * 10 ); Update(); break;
			case KeyCode.PageDown: _scroll = Math.Min( MaxScroll, _scroll + LineH * 10 ); Update(); break;
			default: base.OnKeyPress( e ); break;
		}
	}

	private static FieldDiff ToFieldDiff( JsonDiffUtilities.FieldDifference diff )
	{
		return new FieldDiff
		{
			Name = diff.Path,
			LocalValue = diff.LocalValue,
			RemoteValue = diff.RemoteValue,
			Reason = GetReason( diff.Path, diff.IsAdded ),
			IsAdded = diff.IsAdded
		};
	}

	private static string GetReason( string path, bool isAdded )
	{
		var key = path?.Split( '.' ).LastOrDefault() ?? "";
		if ( Explanations.TryGetValue( key, out var explanation ) )
			return explanation;

		return isAdded
			? "Present on remote but missing locally."
			: "Value differs between local and remote.";
	}
}