Editor/SetupWindow.cs

Editor window UI for configuring Network Storage credentials. It renders labels and real LineEdit inputs, lets you save configuration to SyncToolConfig, and tests credentials via SyncToolApi, displaying validation results and permissions.

NetworkingFile Access
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Sandbox;
using Editor;

/// <summary>
/// Setup window for entering Network Storage credentials.
/// Uses Paint for layout/chrome but real LineEdit widgets for text inputs,
/// giving native text selection, copy/paste, drag, and unlimited key length.
///
/// Access via Editor menu → Network Storage → Setup, or the Setup button in the Sync Tool.
/// </summary>
public class SetupWindow : PaintedWindow
{
	// ── Real input widgets (native text selection, copy/paste, any length) ──
	private LineEdit _projectIdInput;
	private LineEdit _publicKeyInput;
	private LineEdit _secretKeyInput;
	private LineEdit _baseUrlInput;
	private LineEdit _cdnUrlInput;
	private LineEdit _dataFolderInput;

	// ── UI state ──
	private string _status = "";
	private string _statusColor = "white";
	private bool _secretKeyMasked = true;
	private List<ButtonRect> _buttons = new();
	private Vector2 _mousePos;

	// ── Test results ──
	private string _testProjectId;
	private string _testSecretKey;
	private string _testPublicKey;
	private string _testProjectTitle;
	private string _testPermissions;

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

	public SetupWindow()
	{
		Title = "Network Storage Setup";
		Size = new Vector2( 520, 720 );
		MinimumSize = new Vector2( 400, 580 );

		SyncToolConfig.Load();
		CreateInputWidgets();
	}

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

	// ──────────────────────────────────────────────────────
	//  Create real LineEdit widgets (children of the window)
	// ──────────────────────────────────────────────────────

	private void CreateInputWidgets()
	{
		_projectIdInput = CreateLineEdit( SyncToolConfig.ProjectId, "From sbox.cool dashboard" );
		_publicKeyInput = CreateLineEdit( SyncToolConfig.PublicApiKey, "Public key — used by game client" );
		_secretKeyInput = CreateLineEdit( SyncToolConfig.SecretKey, "Secret key — editor only, NEVER ships" );
		_baseUrlInput = CreateLineEdit( SyncToolConfig.BaseUrl, "Default: https://api.sboxcool.com" );
		_cdnUrlInput = CreateLineEdit( SyncToolConfig.CdnUrl, "Optional: storage.sboxcool.com" );
		_dataFolderInput = CreateLineEdit( SyncToolConfig.DataFolder, "Subfolder under Editor/ (default: Network Storage)" );
	}

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

	/// <summary>
	/// Position a LineEdit at the given rect. Called during OnPaint so they track layout.
	/// </summary>
	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 );
	}

	// ──────────────────────────────────────────────────────
	//  Rendering (Paint for chrome, real widgets for inputs)
	// ──────────────────────────────────────────────────────

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

		var y = 38f;
		var pad = 20f;
		var w = Width - pad * 2;
		var fieldH = 28f;

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

		// ── Description ──
		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
		Paint.DrawText( new Rect( pad, y, w, 14 ), "Credentials are stored in Editor/ config files (gitignored, never published)", TextFlag.LeftCenter );
		y += 22;

		DrawSeparator( ref y, w, pad );

		// ── Input fields (label + real LineEdit widget) ──
		DrawFieldLabel( ref y, pad, w, "Project ID" );
		PositionInput( _projectIdInput, pad, y, w, fieldH );
		y += fieldH + 6;

		DrawFieldLabel( ref y, pad, w, "Public API Key" );
		PositionInput( _publicKeyInput, pad, y, w, fieldH );
		y += fieldH + 2;

		// Public key prefix hint
		var pubKey = _publicKeyInput.Text?.Trim() ?? "";
		if ( !string.IsNullOrEmpty( pubKey ) && !pubKey.StartsWith( "sbox_ns_" ) )
		{
			Paint.SetDefaultFont( size: 8 );
			Paint.SetPen( Color.Orange.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( pad, y, w, 12 ), "Standard public keys start with sbox_ns_", TextFlag.LeftCenter );
			y += 14;
		}
		y += 4;

		// Secret Key — with show/hide toggle
		var toggleW = 48f;
		var toggleGap = 6f;
		var fieldInputW = w - toggleW - toggleGap;

		// Label
		DrawFieldLabel( ref y, pad, w, "Secret Key" );

		if ( _secretKeyMasked )
		{
			// Hide the real input, draw a masked box with dots
			_secretKeyInput.Visible = false;

			var maskedRect = new Rect( pad, y, fieldInputW, fieldH );
			var maskedHovered = maskedRect.IsInside( _mousePos );

			Paint.SetBrush( Color.White.WithAlpha( maskedHovered ? 0.08f : 0.04f ) );
			Paint.SetPen( Color.White.WithAlpha( maskedHovered ? 0.2f : 0.1f ) );
			Paint.DrawRect( maskedRect, 3 );

			Paint.SetDefaultFont( size: 12 );
			var secText = _secretKeyInput.Text?.Trim() ?? "";
			if ( string.IsNullOrEmpty( secText ) )
			{
				Paint.SetPen( Color.White.WithAlpha( 0.25f ) );
				Paint.SetDefaultFont( size: 10 );
				Paint.DrawText( new Rect( pad + 8, y, fieldInputW - 16, fieldH ), "Secret key — editor only, NEVER ships", TextFlag.LeftCenter );
			}
			else
			{
				Paint.SetPen( Color.White.WithAlpha( 0.6f ) );
				var dots = new string( '\u2022', Math.Min( secText.Length, 40 ) );
				Paint.DrawText( new Rect( pad + 8, y, fieldInputW - 16, fieldH ), dots, TextFlag.LeftCenter );
			}
		}
		else
		{
			// Show the real input
			_secretKeyInput.Visible = true;
			PositionInput( _secretKeyInput, pad, y, fieldInputW, fieldH );
		}

		// Show/Hide toggle button
		var toggleRect = new Rect( pad + fieldInputW + toggleGap, y, toggleW, fieldH );
		var toggleHovered = toggleRect.IsInside( _mousePos );
		var toggleColor = Color.Cyan;

		Paint.SetBrush( toggleColor.WithAlpha( toggleHovered ? 0.2f : 0.08f ) );
		Paint.SetPen( toggleColor.WithAlpha( toggleHovered ? 0.5f : 0.2f ) );
		Paint.DrawRect( toggleRect, 3 );
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( toggleColor.WithAlpha( toggleHovered ? 1f : 0.7f ) );
		Paint.DrawText( toggleRect, _secretKeyMasked ? "Show" : "Hide", TextFlag.Center );

		_buttons.Add( new ButtonRect
		{
			Rect = toggleRect,
			Id = "toggle_secret",
			OnClick = () =>
			{
				_secretKeyMasked = !_secretKeyMasked;
				Update();
			}
		} );

		y += fieldH + 2;

		// Secret key prefix hint
		var secKey = _secretKeyInput.Text?.Trim() ?? "";
		if ( !string.IsNullOrEmpty( secKey ) && !secKey.StartsWith( "sbox_sk_" ) )
		{
			Paint.SetDefaultFont( size: 8 );
			Paint.SetPen( Color.Orange.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( pad, y, w, 12 ), "Standard secret keys start with sbox_sk_", TextFlag.LeftCenter );
			y += 14;
		}
		y += 4;

		DrawFieldLabel( ref y, pad, w, "Base URL" );
		PositionInput( _baseUrlInput, pad, y, w, fieldH );
		y += fieldH + 6;

		DrawFieldLabel( ref y, pad, w, "CDN URL" );
		PositionInput( _cdnUrlInput, pad, y, w, fieldH );
		y += fieldH + 6;

		DrawFieldLabel( ref y, pad, w, "Editor Data Folder" );
		PositionInput( _dataFolderInput, pad, y, w, fieldH );
		y += fieldH + 8;

		DrawSeparator( ref y, w, pad );

		// ── Data Source Preference ──
		Paint.SetDefaultFont( size: 10, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
		Paint.DrawText( new Rect( pad, y, w, 18 ), "Data Source: API Only", TextFlag.LeftCenter );
		y += 24;

		// ── Save button ──
		var saveBtnH = 34f;
		var saveBtnRect = new Rect( pad, y, w, saveBtnH );
		var saveHovered = saveBtnRect.IsInside( _mousePos );

		Paint.SetBrush( Color.Green.WithAlpha( saveHovered ? 0.25f : 0.15f ) );
		Paint.SetPen( Color.Green.WithAlpha( saveHovered ? 0.6f : 0.3f ) );
		Paint.DrawRect( saveBtnRect, 4 );
		Paint.SetDefaultFont( size: 11, weight: 700 );
		Paint.SetPen( Color.Green.WithAlpha( saveHovered ? 1f : 0.8f ) );
		Paint.DrawText( saveBtnRect, "Save Configuration", TextFlag.Center );

		_buttons.Add( new ButtonRect { Rect = saveBtnRect, Id = "save", OnClick = SaveConfig } );
		y += saveBtnH + 8;

		// ── Test Connection button ──
		var testBtnH = 28f;
		var testBtnRect = new Rect( pad, y, w, testBtnH );
		var testHovered = testBtnRect.IsInside( _mousePos );

		Paint.SetBrush( Color.White.WithAlpha( testHovered ? 0.1f : 0.04f ) );
		Paint.SetPen( Color.White.WithAlpha( testHovered ? 0.3f : 0.15f ) );
		Paint.DrawRect( testBtnRect, 4 );
		Paint.SetDefaultFont( size: 10 );
		Paint.SetPen( Color.White.WithAlpha( testHovered ? 0.9f : 0.6f ) );
		Paint.DrawText( testBtnRect, "Test Connection", TextFlag.Center );

		_buttons.Add( new ButtonRect { Rect = testBtnRect, Id = "test", OnClick = () => _ = TestConnection() } );
		y += testBtnH + 8;

		// ── Test results ──
		if ( _testProjectId != null )
		{
			DrawTestResult( ref y, pad, w, "Project ID", _testProjectId, _testProjectTitle );
			DrawTestResult( ref y, pad, w, "Secret Key", _testSecretKey );
			DrawTestResult( ref y, pad, w, "Public Key", _testPublicKey );

			if ( !string.IsNullOrEmpty( _testPermissions ) )
			{
				y += 4;
				Paint.SetDefaultFont( size: 8, weight: 600 );
				Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
				Paint.DrawText( new Rect( pad, y, w, 12 ), "Key Permissions:", TextFlag.LeftCenter );
				y += 14;
				Paint.SetDefaultFont( size: 8 );
				Paint.SetPen( Color.Cyan.WithAlpha( 0.7f ) );
				Paint.DrawText( new Rect( pad + 8, y, w - 8, 12 ), _testPermissions, TextFlag.LeftCenter );
				y += 14;
			}
			y += 4;
		}

		// ── Status ──
		if ( !string.IsNullOrEmpty( _status ) )
		{
			Paint.SetDefaultFont( size: 9 );
			var col = _statusColor == "green" ? Color.Green : _statusColor == "red" ? Color.Red : Color.White.WithAlpha( 0.6f );
			Paint.SetPen( col );
			Paint.DrawText( new Rect( pad, y, w, 16 ), _status, TextFlag.LeftCenter );
			y += 22;
		}

		// ── File location ──
		Paint.SetDefaultFont( size: 8 );
		Paint.SetPen( Color.White.WithAlpha( 0.25f ) );
		Paint.DrawText( new Rect( pad, y, w, 14 ), $"Saved to: {SyncToolConfig.EnvFilePath}", TextFlag.LeftCenter );
	}

	private void DrawFieldLabel( ref float y, float pad, float w, string label )
	{
		Paint.SetDefaultFont( size: 10, weight: 600 );
		Paint.SetPen( Color.White.WithAlpha( 0.8f ) );
		Paint.DrawText( new Rect( pad, y, w, 16 ), label, TextFlag.LeftCenter );
		y += 18;
	}

	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 DrawTestResult( ref float y, float pad, float w, string label, string result, string extra = null )
	{
		if ( result == null ) return;

		var isOk = result.StartsWith( "Valid" ) || result.StartsWith( "Found" );
		var color = isOk ? Color.Green : Color.Red;

		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( color.WithAlpha( 0.8f ) );
		Paint.DrawText( new Rect( pad, y, 16, 15 ), isOk ? "+" : "x", TextFlag.Center );

		Paint.SetDefaultFont( size: 9 );
		Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
		Paint.DrawText( new Rect( pad + 18, y, 80, 15 ), label, TextFlag.LeftCenter );

		var display = !string.IsNullOrEmpty( extra ) ? $"{result} — {extra}" : result;
		Paint.SetPen( color.WithAlpha( 0.7f ) );
		Paint.DrawText( new Rect( pad + 100, y, w - 100, 15 ), display, TextFlag.LeftCenter );

		y += 17;
	}

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

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

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

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

	// ──────────────────────────────────────────────────────
	//  Actions
	// ──────────────────────────────────────────────────────

	private void SaveConfig()
	{
		var projectId = _projectIdInput.Text?.Trim() ?? "";
		var publicKey = _publicKeyInput.Text?.Trim() ?? "";
		var secretKey = _secretKeyInput.Text?.Trim() ?? "";
		var baseUrl = _baseUrlInput.Text?.Trim() ?? "";
		var cdnUrl = _cdnUrlInput.Text?.Trim() ?? "";
		var dataFolder = _dataFolderInput.Text?.Trim() ?? "";

		if ( string.IsNullOrEmpty( projectId ) )
		{
			_status = "Project ID is required";
			_statusColor = "red";
			Update();
			return;
		}

		SyncToolConfig.Save( secretKey, publicKey, projectId, baseUrl, SyncToolConfig.DataSourceMode.ApiOnly, dataFolder, cdnUrl );

		// Warn about non-standard prefixes but still save
		var warnings = "";
		if ( !string.IsNullOrEmpty( publicKey ) && !publicKey.StartsWith( "sbox_ns_" ) )
			warnings += " (public key: non-standard prefix)";
		if ( !string.IsNullOrEmpty( secretKey ) && !secretKey.StartsWith( "sbox_sk_" ) )
			warnings += " (secret key: non-standard prefix)";

		if ( !string.IsNullOrEmpty( warnings ) )
		{
			_status = $"Saved with warnings:{warnings}";
			_statusColor = "green";
		}
		else
		{
			_status = "Configuration saved successfully";
			_statusColor = "green";
		}

		Update();
	}

	private async Task TestConnection()
	{
		SaveConfig();

		var projectId = _projectIdInput.Text?.Trim() ?? "";
		var secretKey = _secretKeyInput.Text?.Trim() ?? "";
		var publicKey = _publicKeyInput.Text?.Trim() ?? "";

		if ( string.IsNullOrEmpty( projectId ) || string.IsNullOrEmpty( secretKey ) )
		{
			_status = "Need at least Project ID + Secret Key to test";
			_statusColor = "red";
			_testProjectId = null;
			Update();
			return;
		}

		_status = "Testing credentials...";
		_statusColor = "white";
		_testProjectId = null;
		_testSecretKey = null;
		_testPublicKey = null;
		_testProjectTitle = null;
		_testPermissions = null;
		Update();

		var resp = await SyncToolApi.Validate( publicKey );

		if ( !resp.HasValue )
		{
			var errDetail = SyncToolApi.LastErrorMessage ?? "Could not reach server";
			_status = $"Validation failed: {errDetail}";
			_statusColor = "red";
			_testProjectId = SyncToolApi.LastErrorCode ?? "Connection failed";
			_testSecretKey = SyncToolApi.LastErrorCode ?? "Connection failed";
			_testPublicKey = SyncToolApi.LastErrorCode ?? "Connection failed";
			Log.Warning( $"[SyncTool] Validate returned null — LastError: {SyncToolApi.LastErrorCode} — {SyncToolApi.LastErrorMessage}" );
			Update();
			return;
		}

		var result = resp.Value;

		// Check for KEY_UPGRADE_REQUIRED error
		if ( SyncToolApi.LastErrorCode == "KEY_UPGRADE_REQUIRED" ||
		     (result.TryGetProperty( "error", out var errEl ) && errEl.GetString() == "KEY_UPGRADE_REQUIRED") )
		{
			_status = "Secret key uses an old format. Generate a new key at sbox.cool.";
			_statusColor = "red";
			_testProjectId = "Not checked";
			_testSecretKey = "Old format — regenerate on dashboard";
			_testPublicKey = "Not checked";
			Update();
			return;
		}

		if ( result.TryGetProperty( "checks", out var checks ) )
		{
			_testProjectId = GetCheckMessage( checks, "projectId" );
			_testSecretKey = GetCheckMessage( checks, "secretKey" );
			_testPublicKey = GetCheckMessage( checks, "publicKey" );
		}

		if ( result.TryGetProperty( "project", out var proj ) && proj.ValueKind == JsonValueKind.Object )
		{
			_testProjectTitle = proj.TryGetProperty( "title", out var t ) ? t.GetString() : null;
		}

		// Parse permissions if present
		if ( result.TryGetProperty( "permissions", out var perms ) && perms.ValueKind == JsonValueKind.Object )
		{
			var parts = new List<string>();
			var labels = new Dictionary<string, string>
			{
				{ "endpoints", "Endpoints" },
				{ "queries", "Queries" },
				{ "collections", "Collections" },
				{ "workflows", "Workflows" },
				{ "game_values", "Game Values" },
				{ "rate_limits", "Rate Limits" },
				{ "settings", "Settings" }
			};
			foreach ( var kv in labels )
			{
				if ( perms.TryGetProperty( kv.Key, out var val ) )
				{
					var access = val.GetString() ?? "rw";
					var label = FormatPermissionLabel( access );
					parts.Add( $"{kv.Value}: {label}" );
				}
			}
			_testPermissions = string.Join( "  |  ", parts );
		}

		var allOk = result.TryGetProperty( "ok", out var okEl ) && okEl.ValueKind == JsonValueKind.True;
		if ( allOk )
		{
			_status = "All credentials validated";
			_statusColor = "green";
		}
		else
		{
			// Build a meaningful error message from the check results
			var failedChecks = new List<string>();
			if ( _testProjectId != null && _testProjectId != "Found" ) failedChecks.Add( $"Project: {_testProjectId}" );
			if ( _testSecretKey != null && _testSecretKey != "Valid" ) failedChecks.Add( $"Secret key: {_testSecretKey}" );
			if ( _testPublicKey != null && _testPublicKey != "Valid" && _testPublicKey != "Not provided" ) failedChecks.Add( $"Public key: {_testPublicKey}" );

			_status = failedChecks.Count > 0
				? $"Failed: {string.Join( " | ", failedChecks )}"
				: "Validation failed — check console for details";
			_statusColor = "red";
		}
		Update();
	}

	private static string FormatPermissionLabel( string access )
	{
		if ( string.IsNullOrWhiteSpace( access ) || access == "none" ) return "-";
		return access.ToUpperInvariant();
	}

	private static string GetCheckMessage( JsonElement checks, string key )
	{
		if ( !checks.TryGetProperty( key, out var check ) ) return "Not checked";
		var msg = check.TryGetProperty( "message", out var m ) ? m.GetString() : "Unknown";
		return msg ?? "Unknown";
	}
}