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

/// <summary>
/// Settings window for Network Storage runtime behavior.
/// Configure multiplayer proxy mode, sync mappings, and other runtime options.
///
/// Access via Editor menu → Network Storage → Settings.
/// </summary>
[Dock( "Editor", "Network Storage Settings", "settings" )]
public class SettingsWindow : DockWindow
{
	private bool _proxyEnabled;
	private SyncToolConfig.SourceExportMode _sourceExportMode;
	private bool _saved;
	private float _savedTimer;
	private string _generateStatus = "";

	// Logging config state
	private bool _logRequests;
	private bool _logResponses;
	private bool _logTokens;
	private bool _logProxy;
	private bool _logErrors;
	private bool _logConfig;

	private List<ButtonRect> _buttons = new();
	private Vector2 _mousePos;
	private float _scrollY;
	private float _contentHeight;

	// ── Sync mapping editor state ──
	private List<MappingRow> _mappingRows = new();

	private class MappingRow
	{
		public LineEdit CsFileInput;
		public LineEdit CollectionInput;
		public LineEdit DescriptionInput;
	}

	private struct ButtonRect
	{
		public Rect Rect;
		public string Id;
		public Action OnClick;
	}

	public SettingsWindow()
	{
		Title = "Network Storage Settings";
		Size = new Vector2( 520, 700 );
		MinimumSize = new Vector2( 400, 400 );

		SyncToolConfig.Load();
		_proxyEnabled = SyncToolConfig.ProxyEnabled;
		_sourceExportMode = SyncToolConfig.SourceExport;
		RebuildMappingWidgets();
		LoadLogConfig();
	}

	private void LoadLogConfig()
	{
		_logRequests = NetworkStorageLogConfig.LogRequests;
		_logResponses = NetworkStorageLogConfig.LogResponses;
		_logTokens = NetworkStorageLogConfig.LogTokens;
		_logProxy = NetworkStorageLogConfig.LogProxy;
		_logErrors = NetworkStorageLogConfig.LogErrors;
		_logConfig = NetworkStorageLogConfig.LogConfig;
	}

	private void ApplyLogConfig()
	{
		NetworkStorageLogConfig.LogRequests = _logRequests;
		NetworkStorageLogConfig.LogResponses = _logResponses;
		NetworkStorageLogConfig.LogTokens = _logTokens;
		NetworkStorageLogConfig.LogProxy = _logProxy;
		NetworkStorageLogConfig.LogErrors = _logErrors;
		NetworkStorageLogConfig.LogConfig = _logConfig;
	}

	[Menu( "Editor", "Network Storage/Settings" )]
	public static void OpenWindow()
	{
		var window = new SettingsWindow();
		window.Show();
	}

	// ──────────────────────────────────────────────────────
	//  Rendering
	// ──────────────────────────────────────────────────────

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

		var pad = 20f;
		var w = Width - pad * 2;
		var visibleH = Height;

		// Scrollable content — offset all y by -_scrollY
		var y = 38f - _scrollY;

		// ── Title ──
		Paint.SetDefaultFont( size: 14, weight: 700 );
		Paint.SetPen( Color.White );
		Paint.DrawText( new Rect( pad, y, w, 24 ), "Network Storage Settings", TextFlag.LeftCenter );
		y += 32;

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
		Paint.DrawText( new Rect( pad, y, w, 14 ), "Runtime behavior — these settings ship with your game", TextFlag.LeftCenter );
		y += 22;

		DrawSeparator( ref y, w, pad );

		var lineH = 15f;

		// ── Sync Mappings (C# → Collection) — first ──
		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Data Source Mappings", TextFlag.LeftCenter );
		y += 26;

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
		DrawWrappedText( ref y, pad, w, lineH + 2,
			"Map C# data files or directories to collection YAML source. " +
			"The Sync Tool's Generate button parses C# records and " +
			"writes the collection source automatically." );
		y += 12;

		// ── Mapping rows with LineEdit widgets ──
		var inputH = 22f;
		var removeBtnW = 20f;
		var gap = 4f;

		for ( int i = 0; i < _mappingRows.Count; i++ )
		{
			var row = _mappingRows[i];

			// Row number + remove button
			Paint.SetDefaultFont( size: 9, weight: 600 );
			Paint.SetPen( Color.White.WithAlpha( 0.4f ) );
			Paint.DrawText( new Rect( pad, y, 16, inputH ), $"{i + 1}.", TextFlag.RightCenter );

			// C# source path
			Paint.SetDefaultFont( size: 8 );
			Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
			Paint.DrawText( new Rect( pad + 22, y - 12, w * 0.55f, 12 ), "C# source (file or directory)", TextFlag.LeftCenter );
			PositionInput( row.CsFileInput, pad + 22, y, w * 0.55f - gap, inputH );

			// Collection name
			Paint.DrawText( new Rect( pad + 22 + w * 0.55f, y - 12, w * 0.35f, 12 ), "Collection name", TextFlag.LeftCenter );
			PositionInput( row.CollectionInput, pad + 22 + w * 0.55f, y, w * 0.35f - removeBtnW - gap * 2, inputH );

			// Remove button
			var removeBtnRect = new Rect( pad + w - removeBtnW, y, removeBtnW, inputH );
			var removeScreenRect = new Rect( pad + w - removeBtnW, y + _scrollY, removeBtnW, inputH );
			var removeHovered = removeScreenRect.IsInside( _mousePos );

			Paint.SetBrush( Color.Red.WithAlpha( removeHovered ? 0.2f : 0.08f ) );
			Paint.SetPen( Color.Red.WithAlpha( removeHovered ? 0.6f : 0.25f ) );
			Paint.DrawRect( removeBtnRect, 3 );
			Paint.SetDefaultFont( size: 11, weight: 700 );
			Paint.SetPen( Color.Red.WithAlpha( removeHovered ? 1f : 0.6f ) );
			Paint.DrawText( removeBtnRect, "×", TextFlag.Center );

			var capturedIndex = i;
			_buttons.Add( new ButtonRect
			{
				Rect = removeScreenRect,
				Id = $"remove_mapping_{i}",
				OnClick = () => RemoveMapping( capturedIndex )
			} );

			y += inputH + gap;

			// Description field
			Paint.SetDefaultFont( size: 8 );
			Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
			Paint.DrawText( new Rect( pad + 22, y - 1, 80, 12 ), "Description", TextFlag.LeftCenter );
			PositionInput( row.DescriptionInput, pad + 22 + 64, y, w - 22 - 64 - removeBtnW - gap, inputH - 2 );

			y += inputH + 8;
		}

		// Add mapping button
		var addBtnW = 130f;
		var addBtnH = 26f;
		var addBtnRect = new Rect( pad, y, addBtnW, addBtnH );
		var addBtnScreenRect = new Rect( pad, y + _scrollY, addBtnW, addBtnH );
		var addBtnHovered = addBtnScreenRect.IsInside( _mousePos );

		Paint.SetBrush( Color.Green.WithAlpha( addBtnHovered ? 0.15f : 0.06f ) );
		Paint.SetPen( Color.Green.WithAlpha( addBtnHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( addBtnRect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Green.WithAlpha( addBtnHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( addBtnRect, "+ Add Mapping", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = addBtnScreenRect,
			Id = "add_mapping",
			OnClick = AddMapping
		} );

		// Save mappings button (next to add)
		var saveMappingsBtnW = 130f;
		var saveMappingsBtnRect = new Rect( pad + addBtnW + 8, y, saveMappingsBtnW, addBtnH );
		var saveMappingsScreenRect = new Rect( pad + addBtnW + 8, y + _scrollY, saveMappingsBtnW, addBtnH );
		var saveMappingsHovered = saveMappingsScreenRect.IsInside( _mousePos );

		var mappingsDirty = AreMappingsDirty();
		var saveMappingsColor = mappingsDirty ? Color.Cyan : Color.White;
		Paint.SetBrush( saveMappingsColor.WithAlpha( saveMappingsHovered ? 0.15f : 0.06f ) );
		Paint.SetPen( saveMappingsColor.WithAlpha( saveMappingsHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( saveMappingsBtnRect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( saveMappingsColor.WithAlpha( saveMappingsHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( saveMappingsBtnRect, "Save Mappings", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = saveMappingsScreenRect,
			Id = "save_mappings",
			OnClick = SaveMappings
		} );

		y += addBtnH + 12;

		DrawSeparator( ref y, w, pad );

		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Pull / Export Format", TextFlag.LeftCenter );
		y += 26;

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
		DrawWrappedText( ref y, pad, w, lineH + 2,
			"The Sync Tool writes YAML source only. Legacy JSON export and local JSON fallback are no longer supported." );
		y += 8;

		DrawReadonlyStatusRow( ref y, pad, w, "Export Format", "YAML Source Only", true );
		y += 6;

		DrawSeparator( ref y, w, pad );

		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Project Security", TextFlag.LeftCenter );
		y += 26;

		DrawReadonlyStatusRow( ref y, pad, w, "Auth Sessions", SyncToolConfig.AuthSessionsLabel, SyncToolConfig.EnableAuthSessions );
		DrawReadonlyStatusRow( ref y, pad, w, "Encrypted Requests", SyncToolConfig.EncryptedRequestsLabel, SyncToolConfig.EnableEncryptedRequests );

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
		DrawWrappedText( ref y, pad, w, lineH + 2,
			"Read-only project settings. Go to Sync Tool and pull changes to resync these settings." );
		y += 12;

		// ── Multiplayer Auth Proxy ──
		DrawSeparator( ref y, w, pad );

		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Multiplayer Auth Proxy", TextFlag.LeftCenter );

		// Info button (next to title)
		var infoBtnW = 40f;
		var infoBtnH = 20f;
		var infoBtnX = pad + w - infoBtnW;
		var infoBtnRect = new Rect( infoBtnX, y, infoBtnW, infoBtnH );
		var infoBtnScreenRect = new Rect( infoBtnX, y + _scrollY, infoBtnW, infoBtnH );
		var infoBtnHovered = infoBtnScreenRect.IsInside( _mousePos );

		Paint.SetBrush( Color.White.WithAlpha( infoBtnHovered ? 0.12f : 0.05f ) );
		Paint.SetPen( Color.White.WithAlpha( infoBtnHovered ? 0.4f : 0.2f ) );
		Paint.DrawRect( infoBtnRect, 3 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( infoBtnHovered ? 0.9f : 0.55f ) );
		Paint.DrawText( infoBtnRect, "Info", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = infoBtnScreenRect,
			Id = "proxy_info",
			OnClick = ProxyInfoDialog.Open
		} );

		y += 26;

		// Checkbox + label
		var checkboxSize = 22f;
		var checkboxRect = new Rect( pad, y, checkboxSize, checkboxSize );
		var checkScreenRect = new Rect( pad, y + _scrollY, checkboxSize, checkboxSize );
		var checkHovered = checkScreenRect.IsInside( _mousePos );

		var checkColor = _proxyEnabled ? Color.Cyan : Color.White;
		Paint.SetBrush( checkColor.WithAlpha( _proxyEnabled ? 0.15f : checkHovered ? 0.08f : 0.04f ) );
		Paint.SetPen( checkColor.WithAlpha( _proxyEnabled ? 0.5f : checkHovered ? 0.25f : 0.15f ) );
		Paint.DrawRect( checkboxRect, 3 );

		if ( _proxyEnabled )
		{
			Paint.SetDefaultFont( size: 14, weight: 700 );
			Paint.SetPen( Color.Cyan );
			Paint.DrawText( checkboxRect, "\u2713", TextFlag.Center );
		}

		Paint.SetDefaultFont( size: 10, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( 0.85f ) );
		Paint.DrawText( new Rect( pad + checkboxSize + 10, y, w - checkboxSize - 10, checkboxSize ), "Host proxies API calls for non-host clients", TextFlag.LeftCenter );

		_buttons.Add( new ButtonRect
		{
			Rect = checkScreenRect,
			Id = "proxy_toggle",
			OnClick = () =>
			{
				_proxyEnabled = !_proxyEnabled;
				_saved = false;
				Update();
			}
		} );

		y += checkboxSize + 14;

		DrawProxySecurityNotice( ref y, pad, w, lineH );

		// Save button
		var btnW = 120f;
		var btnH = 30f;
		var btnRect = new Rect( pad, y, btnW, btnH );
		var btnScreenRect = new Rect( pad, y + _scrollY, btnW, btnH );
		var btnHovered = btnScreenRect.IsInside( _mousePos );

		var isDirty = _proxyEnabled != SyncToolConfig.ProxyEnabled
			|| _sourceExportMode != SyncToolConfig.SourceExport;
		var btnColor = _saved ? Color.Green : ( isDirty ? Color.Cyan : Color.White );
		Paint.SetBrush( btnColor.WithAlpha( btnHovered ? 0.2f : 0.1f ) );
		Paint.SetPen( btnColor.WithAlpha( btnHovered ? 0.6f : 0.35f ) );
		Paint.DrawRect( btnRect, 4 );

		Paint.SetDefaultFont( size: 9, weight: 700 );
		Paint.SetPen( btnColor.WithAlpha( 0.9f ) );
		var btnLabel = _saved ? "Saved!" : "Save Settings";
		Paint.DrawText( btnRect, btnLabel, TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = btnScreenRect,
			Id = "save",
			OnClick = () =>
			{
				SyncToolConfig.SetProxyEnabled( _proxyEnabled );
				SyncToolConfig.SetSourceExportMode( _sourceExportMode );
				_saved = true;
				_savedTimer = 2f;
				Update();
			}
		} );

		y += btnH + 20;

		DrawSeparator( ref y, w, pad );

		// ── Generate Code ──
		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Generate Code", TextFlag.LeftCenter );
		y += 26;

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
		DrawWrappedText( ref y, pad, w, lineH + 2,
			"Reads your local endpoint definitions and collection schemas, then writes strongly-typed C# classes to Code/Data/NetworkStorage/. " +
			"These generated files (prefixed autoGenerated_) give you type-safe access to collection fields, endpoint slugs, and workflow IDs in game code. " +
			"Runs automatically after every Push — use this button if you have pulled changes from remote and want to regenerate without pushing." );
		y += 12;

		var genBtnW = 140f;
		var genBtnH = 28f;
		var genBtnRect = new Rect( pad, y, genBtnW, genBtnH );
		var genBtnScreenRect = new Rect( pad, y + _scrollY, genBtnW, genBtnH );
		var genBtnHovered = genBtnScreenRect.IsInside( _mousePos );

		Paint.SetBrush( Color.Cyan.WithAlpha( genBtnHovered ? 0.15f : 0.06f ) );
		Paint.SetPen( Color.Cyan.WithAlpha( genBtnHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( genBtnRect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Cyan.WithAlpha( genBtnHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( genBtnRect, "Generate Code", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = genBtnScreenRect,
			Id = "generate_code",
			OnClick = () =>
			{
				SyncToolConfig.Load();
				var count = CodeGenerator.Generate();
				_generateStatus = $"✓ {count} files written to Code/Data/NetworkStorage/";
				Update();
			}
		} );

		if ( !string.IsNullOrEmpty( _generateStatus ) )
		{
			Paint.SetDefaultFont( size: 9 );
			Paint.SetPen( Color.Green.WithAlpha( 0.8f ) );
			Paint.DrawText( new Rect( pad + genBtnW + 10, y, w - genBtnW - 10, genBtnH ), _generateStatus, TextFlag.LeftCenter );
		}

		y += genBtnH + 20;

		// ── Console Logging ──
		DrawSeparator( ref y, w, pad );

		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( pad, y, w, 20 ), "Console Logging", TextFlag.LeftCenter );
		y += 26;

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.45f ) );
		DrawWrappedText( ref y, pad, w, lineH + 2,
			"Control which [Network Storage] log messages appear in the console. " +
			"Changes apply immediately at runtime (not persisted across sessions)." );
		y += 8;

		// Enable All / Disable All buttons
		var logBtnW = 90f;
		var logBtnH = 24f;
		var logBtnGap = 8f;

		var enableAllRect = new Rect( pad, y, logBtnW, logBtnH );
		var enableAllScreenRect = new Rect( pad, y + _scrollY, logBtnW, logBtnH );
		var enableAllHovered = enableAllScreenRect.IsInside( _mousePos );

		Paint.SetBrush( Color.Green.WithAlpha( enableAllHovered ? 0.15f : 0.06f ) );
		Paint.SetPen( Color.Green.WithAlpha( enableAllHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( enableAllRect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Green.WithAlpha( enableAllHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( enableAllRect, "Enable All", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = enableAllScreenRect,
			Id = "log_enable_all",
			OnClick = () =>
			{
				_logRequests = _logResponses = _logTokens = _logProxy = _logErrors = _logConfig = true;
				ApplyLogConfig();
				Update();
			}
		} );

		var disableAllRect = new Rect( pad + logBtnW + logBtnGap, y, logBtnW, logBtnH );
		var disableAllScreenRect = new Rect( pad + logBtnW + logBtnGap, y + _scrollY, logBtnW, logBtnH );
		var disableAllHovered = disableAllScreenRect.IsInside( _mousePos );

		Paint.SetBrush( Color.Red.WithAlpha( disableAllHovered ? 0.15f : 0.06f ) );
		Paint.SetPen( Color.Red.WithAlpha( disableAllHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( disableAllRect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Red.WithAlpha( disableAllHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( disableAllRect, "Disable All", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = disableAllScreenRect,
			Id = "log_disable_all",
			OnClick = () =>
			{
				_logRequests = _logResponses = _logTokens = _logProxy = _logErrors = _logConfig = false;
				ApplyLogConfig();
				Update();
			}
		} );

		y += logBtnH + 12;

		// Log category checkboxes (2 columns)
		var colW = (w - 10) / 2f;
		var checkSize = 18f;
		var checkRowH = 26f;

		DrawLogCheckbox( ref y, pad, colW, checkSize, checkRowH, "Requests", ref _logRequests );
		DrawLogCheckbox( ref y, pad + colW + 10, colW, checkSize, checkRowH, "Responses", ref _logResponses, sameRow: true );
		y += checkRowH;

		DrawLogCheckbox( ref y, pad, colW, checkSize, checkRowH, "Tokens", ref _logTokens );
		DrawLogCheckbox( ref y, pad + colW + 10, colW, checkSize, checkRowH, "Proxy", ref _logProxy, sameRow: true );
		y += checkRowH;

		DrawLogCheckbox( ref y, pad, colW, checkSize, checkRowH, "Errors", ref _logErrors );
		DrawLogCheckbox( ref y, pad + colW + 10, colW, checkSize, checkRowH, "Config", ref _logConfig, sameRow: true );
		y += checkRowH + 8;

		// Bottom padding so content isn't flush against the edge
		y += 100;

		// Track content height for scroll clamping
		_contentHeight = y + _scrollY;

		// ── Scrollbar ──
		var scrollbarW = 4f;
		var scrollbarX = Width - scrollbarW - 4f;
		var scrollRatio = visibleH / Math.Max( _contentHeight, visibleH );
		if ( scrollRatio < 1f )
		{
			var trackH = visibleH - 8f;
			var thumbH = Math.Max( 30f, trackH * scrollRatio );
			var thumbY = 4f + ( _scrollY / ( _contentHeight - visibleH ) ) * ( trackH - thumbH );

			Paint.SetBrush( Color.White.WithAlpha( 0.08f ) );
			Paint.SetPen( Color.Transparent );
			Paint.DrawRect( new Rect( scrollbarX, 4f, scrollbarW, trackH ), scrollbarW / 2 );

			Paint.SetBrush( Color.White.WithAlpha( 0.2f ) );
			Paint.DrawRect( new Rect( scrollbarX, thumbY, scrollbarW, thumbH ), scrollbarW / 2 );
		}

		// Fade "Saved!" label after timer
		if ( _saved && _savedTimer > 0 )
		{
			_savedTimer -= RealTime.Delta;
			if ( _savedTimer <= 0 )
			{
				_saved = false;
				_savedTimer = 0;
			}
			Update();
		}
	}

	private void DrawWrappedText( ref float y, float x, float w, float lineH, string text )
	{
		var charsPerLine = Math.Max( 40, (int)( w / 6.2f ) );
		var words = text.Split( ' ' );
		var line = "";

		foreach ( var word in words )
		{
			if ( line.Length + word.Length + 1 > charsPerLine && line.Length > 0 )
			{
				Paint.DrawText( new Rect( x, y, w, lineH ), line, TextFlag.LeftCenter );
				y += lineH;
				line = word;
			}
			else
			{
				line = line.Length > 0 ? $"{line} {word}" : word;
			}
		}

		if ( line.Length > 0 )
		{
			Paint.DrawText( new Rect( x, y, w, lineH ), line, TextFlag.LeftCenter );
			y += lineH;
		}
	}

	private void DrawSeparator( ref float y, float w, float pad )
	{
		Paint.SetPen( Color.White.WithAlpha( 0.08f ) );
		Paint.DrawLine( new Vector2( pad, y ), new Vector2( pad + w, y ) );
		y += 10;
	}

	private void DrawSourceExportButton( SyncToolConfig.SourceExportMode mode, string label, Rect rect, Rect screenRect )
	{
		var selected = _sourceExportMode == mode;
		var hovered = screenRect.IsInside( _mousePos );
		var color = selected ? Color.Cyan : Color.White;

		Paint.SetBrush( color.WithAlpha( selected ? 0.16f : hovered ? 0.09f : 0.04f ) );
		Paint.SetPen( color.WithAlpha( selected ? 0.55f : hovered ? 0.35f : 0.18f ) );
		Paint.DrawRect( rect, 4 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( color.WithAlpha( selected || hovered ? 0.9f : 0.6f ) );
		Paint.DrawText( rect, label, TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = screenRect,
			Id = $"source_export_{mode}",
			OnClick = () =>
			{
				_sourceExportMode = mode;
				_saved = false;
				Update();
			}
		} );
	}

	// ──────────────────────────────────────────────────────
	//  Sync mapping helpers
	// ──────────────────────────────────────────────────────

	private void DrawReadonlyStatusRow( ref float y, float pad, float w, string label, string value, bool enabled )
	{
		var rowH = 24f;
		Paint.SetBrush( Color.White.WithAlpha( 0.035f ) );
		Paint.SetPen( Color.White.WithAlpha( 0.08f ) );
		Paint.DrawRect( new Rect( pad, y, w, rowH ), 4 );

		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( 0.82f ) );
		Paint.DrawText( new Rect( pad + 10, y, w * 0.55f, rowH ), label, TextFlag.LeftCenter );

		var color = enabled ? Color.Green : Color.White;
		Paint.SetPen( color.WithAlpha( enabled ? 0.9f : 0.55f ) );
		Paint.DrawText( new Rect( pad + w * 0.55f, y, w * 0.45f - 10, rowH ), value, TextFlag.RightCenter );
		y += rowH + 6;
	}

	private void DrawProxySecurityNotice( ref float y, float pad, float w, float lineH )
	{
		var noticeY = y;
		var noticeH = 138f;
		Paint.SetBrush( Color.Orange.WithAlpha( 0.055f ) );
		Paint.SetPen( Color.Orange.WithAlpha( 0.22f ) );
		Paint.DrawRect( new Rect( pad, noticeY, w, noticeH ), 4 );

		var textY = noticeY + 10f;
		var textX = pad + 12f;
		var textW = w - 24f;
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Orange.WithAlpha( 0.9f ) );
		Paint.DrawText( new Rect( textX, textY, textW, 14 ), "Encryption is recommended when proxy mode is enabled.", TextFlag.LeftCenter );
		textY += 18f;

		Paint.SetDefaultFont( size: 8 );
		Paint.SetPen( Color.White.WithAlpha( 0.62f ) );
		DrawWrappedText( ref textY, textX, textW, lineH + 1,
			"Proxy mode routes non-host Network Storage calls through the host. Without encrypted requests, that host can inspect endpoint payloads." );
		DrawWrappedText( ref textY, textX, textW, lineH + 1,
			"Encryption hides the payload, but it does not stop replay by itself. A hostile host could resend the same encrypted auth-session request, like a Drop $500 call." );
		DrawWrappedText( ref textY, textX, textW, lineH + 1,
			"Encrypted requests should include a one-use key such as unixSeconds_random6+. The backend rejects keys that are too old or already used." );

		y = noticeY + noticeH + 12f;
	}

	private void PositionInput( LineEdit input, float x, float y, float w, float h )
	{
		input.Position = new Vector2( x, y );
		input.Size = new Vector2( w, h );
	}

	private LineEdit CreateLineEdit( string value, string placeholder )
	{
		var input = new LineEdit( this );
		input.Text = value ?? "";
		input.PlaceholderText = placeholder;
		input.Visible = true;
		return input;
	}

	private void RebuildMappingWidgets()
	{
		// Destroy old widgets
		foreach ( var row in _mappingRows )
		{
			row.CsFileInput?.Destroy();
			row.CollectionInput?.Destroy();
			row.DescriptionInput?.Destroy();
		}
		_mappingRows.Clear();

		// Create widgets for current mappings
		foreach ( var mapping in SyncToolConfig.SyncMappings )
		{
			_mappingRows.Add( new MappingRow
			{
				CsFileInput = CreateLineEdit( mapping.CsFile, "Code/Fishing" ),
				CollectionInput = CreateLineEdit( mapping.Collection, "game_values" ),
				DescriptionInput = CreateLineEdit( mapping.Description, "Optional description" )
			} );
		}
	}

	private void AddMapping()
	{
		_mappingRows.Add( new MappingRow
		{
			CsFileInput = CreateLineEdit( "", "Code/Fishing" ),
			CollectionInput = CreateLineEdit( "", "game_values" ),
			DescriptionInput = CreateLineEdit( "", "Optional description" )
		} );
		Update();
	}

	private void RemoveMapping( int index )
	{
		if ( index < 0 || index >= _mappingRows.Count ) return;
		var row = _mappingRows[index];
		row.CsFileInput?.Destroy();
		row.CollectionInput?.Destroy();
		row.DescriptionInput?.Destroy();
		_mappingRows.RemoveAt( index );
		Update();
	}

	private void SaveMappings()
	{
		var mappings = _mappingRows
			.Where( r => !string.IsNullOrWhiteSpace( r.CsFileInput?.Text ) && !string.IsNullOrWhiteSpace( r.CollectionInput?.Text ) )
			.Select( r => new SyncToolConfig.SyncMapping
			{
				CsFile = r.CsFileInput.Text.Trim(),
				Collection = r.CollectionInput.Text.Trim(),
				Description = r.DescriptionInput?.Text?.Trim() ?? ""
			} )
			.ToList();

		SyncToolConfig.SaveSyncMappings( mappings );
		_saved = true;
		_savedTimer = 2f;
		Update();
	}

	private bool AreMappingsDirty()
	{
		var current = SyncToolConfig.SyncMappings;
		if ( _mappingRows.Count != current.Count ) return true;
		for ( int i = 0; i < _mappingRows.Count; i++ )
		{
			if ( i >= current.Count ) return true;
			if ( _mappingRows[i].CsFileInput?.Text != current[i].CsFile ) return true;
			if ( _mappingRows[i].CollectionInput?.Text != current[i].Collection ) return true;
			if ( ( _mappingRows[i].DescriptionInput?.Text ?? "" ) != ( current[i].Description ?? "" ) ) return true;
		}
		return false;
	}

	private void DrawLogCheckbox( ref float y, float x, float colW, float checkSize, float rowH, string label, ref bool value, bool sameRow = false )
	{
		var checkRect = new Rect( x, y, checkSize, checkSize );
		var checkScreenRect = new Rect( x, y + _scrollY, checkSize, checkSize );
		var checkHovered = checkScreenRect.IsInside( _mousePos );

		var checkColor = value ? Color.Cyan : Color.White;
		Paint.SetBrush( checkColor.WithAlpha( value ? 0.15f : checkHovered ? 0.08f : 0.04f ) );
		Paint.SetPen( checkColor.WithAlpha( value ? 0.5f : checkHovered ? 0.25f : 0.15f ) );
		Paint.DrawRect( checkRect, 3 );

		if ( value )
		{
			Paint.SetDefaultFont( size: 12, weight: 700 );
			Paint.SetPen( Color.Cyan );
			Paint.DrawText( checkRect, "✓", TextFlag.Center );
		}

		Paint.SetDefaultFont( size: 9, weight: 500 );
		Paint.SetPen( Color.White.WithAlpha( 0.8f ) );
		Paint.DrawText( new Rect( x + checkSize + 8, y, colW - checkSize - 8, checkSize ), label, TextFlag.LeftCenter );

		var fieldName = label;
		_buttons.Add( new ButtonRect
		{
			Rect = checkScreenRect,
			Id = $"log_{label.ToLower()}",
			OnClick = () =>
			{
				ToggleLogField( fieldName );
				ApplyLogConfig();
				Update();
			}
		} );
	}

	private void ToggleLogField( string fieldName )
	{
		switch ( fieldName )
		{
			case "Requests": _logRequests = !_logRequests; break;
			case "Responses": _logResponses = !_logResponses; break;
			case "Tokens": _logTokens = !_logTokens; break;
			case "Proxy": _logProxy = !_logProxy; break;
			case "Errors": _logErrors = !_logErrors; break;
			case "Config": _logConfig = !_logConfig; break;
		}
	}

	// ──────────────────────────────────────────────────────
	//  Input handling
	// ──────────────────────────────────────────────────────

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

		foreach ( var btn in _buttons )
		{
			if ( btn.Rect.IsInside( e.LocalPosition ) )
			{
				btn.OnClick?.Invoke();
				return;
			}
		}
	}

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

	protected override void OnMouseWheel( WheelEvent e )
	{
		base.OnMouseWheel( e );

		var maxScroll = Math.Max( 0f, _contentHeight - Height );
		_scrollY = Math.Clamp( _scrollY - e.Delta * 30f, 0f, maxScroll );
		Update();
	}
}