Editor/UI/LibraryRowBadge.cs
using System;
using System.Collections.Generic;
using Editor;
using Sandbox.SecBox.Bridge.Dto;
using Sandbox.SecBox.Lifecycle;
using DiagnosticsLog = Sandbox.SecBox.Bridge.DiagnosticsLog;

namespace Sandbox.SecBox.UI;

// Installs an ItemPaint wrapper on every LibraryList descendant of the
// LibraryManagerDock (there are two - ListLocal and ListGlobal - held as
// private fields, so we discover them by descendant walk rather than by
// reflecting the dock's fields).
//
// The wrapper invokes the previously-set ItemPaint (or falls through to the
// protected PaintItem on BaseItemWidget) and then overlays a small severity-
// colored stripe per row sourced from the SecBox trust store.
//
// Both lists survive across row clicks, but the engine may re-create them
// when the dock rebuilds (it has an [EditorEvent.Frame] -> Rebuild path).
// EnsureInstalled is therefore called every frame and tracks wrappers
// per-list; stale entries are pruned when the underlying widget reports
// !IsValid.
internal static class LibraryRowBadge
{
	static Type _listType;

	// Per-list state: original paint (or null) + our wrapper closure for that
	// list. Keyed by the list widget itself. References go stale when Qt
	// destroys the widget - IsValid catches that on each tick.
	sealed class ListState
	{
		public Action<VirtualWidget> Original;
		public Action<VirtualWidget> Wrapper;
	}
	static readonly Dictionary<BaseItemWidget, ListState> _wrapped = new();

	// Trust-store cache keyed by package ident. Refreshed on a low-frequency
	// timer so we don't re-load + re-hash on every frame.
	static readonly Dictionary<string, TrustSnapshot> _cache = new( StringComparer.OrdinalIgnoreCase );
	static DateTime _cacheLoadedAt = DateTime.MinValue;
	static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds( 3 );

	// True once we've ever managed to install at least one list. Cosmetic -
	// caller logs once on the install transition.
	public static bool Installed { get; private set; }

	// Idempotent per-tick installer. Returns true if at least one list is
	// currently wrapped. Cheap when nothing has changed.
	public static bool EnsureInstalled( Widget libraryManagerDock )
	{
		if ( libraryManagerDock == null || !libraryManagerDock.IsValid )
		{
			ResetState();
			return false;
		}

		if ( _listType == null )
		{
			_listType = ReflectionHelpers.ResolveEditorType(
				"Editor.LibraryManager.LibraryList", anchor: libraryManagerDock.GetType() );
			if ( _listType == null )
			{
				DiagnosticsLog.Warn( "[secbox] LibraryRowBadge: Editor.LibraryManager.LibraryList type not found - row badges disabled" );
				return false;
			}
		}

		// Prune stale entries.
		List<BaseItemWidget> dead = null;
		foreach ( var kv in _wrapped )
		{
			if ( kv.Key == null || !kv.Key.IsValid )
			{
				dead ??= new List<BaseItemWidget>();
				dead.Add( kv.Key );
			}
		}
		if ( dead != null ) foreach ( var k in dead ) _wrapped.Remove( k );

		// Walk descendants and wrap each LibraryList we don't already cover.
		int wrappedCount = 0;
		try
		{
			foreach ( var w in libraryManagerDock.GetDescendants<Widget>() )
			{
				if ( w == null || w.GetType() != _listType ) continue;
				if ( w is not BaseItemWidget list ) continue;

				if ( _wrapped.TryGetValue( list, out var state ) )
				{
					// Re-assert if something replaced our delegate (e.g. engine
					// refresh re-set ItemPaint).
					if ( !ReferenceEquals( list.ItemPaint, state.Wrapper ) )
					{
						state.Original = list.ItemPaint;
						list.ItemPaint = state.Wrapper;
					}
				}
				else
				{
					var original = list.ItemPaint;
					Action<VirtualWidget> wrapper = null;
					wrapper = vw => OnItemPaint( list, wrapper, vw );
					list.ItemPaint = wrapper;
					_wrapped[list] = new ListState { Original = original, Wrapper = wrapper };

					if ( !Installed )
					{
						Installed = true;
						DiagnosticsLog.Info( "[secbox] LibraryRowBadge installed on a LibraryList instance" );
					}
				}
				wrappedCount++;
			}
		}
		catch ( Exception ex )
		{
			DiagnosticsLog.Warn( $"[secbox] LibraryRowBadge.EnsureInstalled: walk threw: {ex.Message}" );
		}

		if ( wrappedCount == 0 && _wrapped.Count == 0 )
		{
			// No lists realized yet (dock just opened, list waiting to lay out).
			// Not an error; we'll try again next frame.
			return false;
		}

		return true;
	}

	static void OnItemPaint( BaseItemWidget list, Action<VirtualWidget> selfRef, VirtualWidget vw )
	{
		try
		{
			// Closure trick: selfRef is the wrapper itself. We look up our state
			// by the list widget to get the original paint without capturing it
			// directly (so reassert can update it later if the engine resets).
			Action<VirtualWidget> original = null;
			if ( _wrapped.TryGetValue( list, out var state ) )
				original = state.Original;

			if ( original != null )
			{
				original( vw );
			}
			else
			{
				ReflectionHelpers.InvokeNonPublic( list, "PaintItem", vw );
			}

			DrawBadge( vw );
		}
		catch ( Exception ex )
		{
			// A throw out of a paint delegate would propagate into Qt, which
			// will at best repaint forever and at worst tear down the dock.
			// Swallow + log; user keeps a working editor minus a badge.
			DiagnosticsLog.Warn( $"[secbox] LibraryRowBadge.OnItemPaint: {ex.Message}" );
		}
	}

	static void DrawBadge( VirtualWidget vw )
	{
		if ( vw.Object == null ) return;

		var ident = IdentOf( vw.Object );
		if ( string.IsNullOrEmpty( ident ) ) return;

		var snap = LookupSnapshot( ident );
		if ( !snap.Has ) return;

		// 4px wide vertical stripe on the right edge of the row.
		var r = vw.Rect;
		float stripeWidth = 4f;
		var stripe = new Rect( r.Right - stripeWidth - 2f, r.Top + 2f, stripeWidth, r.Height - 4f );

		Paint.ClearPen();
		Paint.SetBrush( SeverityColor( snap.MaxSeverity, snap.Decision ) );
		Paint.DrawRect( stripe, 1f );
	}

	static TrustSnapshot LookupSnapshot( string ident )
	{
		if ( DateTime.UtcNow - _cacheLoadedAt > CacheTtl )
			RefreshCache();

		if ( _cache.TryGetValue( ident, out var s ) ) return s;
		return TrustSnapshot.Missing;
	}

	static void RefreshCache()
	{
		try
		{
			_cache.Clear();
			_cacheLoadedAt = DateTime.UtcNow;

			var projectRoot = PackageLocator.CurrentProjectRoot();
			if ( string.IsNullOrEmpty( projectRoot ) ) return;

			var store = TrustStore.Load( projectRoot );
			foreach ( var entry in store.Entries )
			{
				if ( string.IsNullOrEmpty( entry.PackageIdent ) ) continue;
				_cache[entry.PackageIdent] = new TrustSnapshot
				{
					Has = true,
					Decision = entry.Decision,
					MaxSeverity =
						entry.CriticalCount > 0 ? Severity.Critical :
						entry.HighCount     > 0 ? Severity.High     :
						entry.MediumCount   > 0 ? Severity.Medium   :
						entry.LowCount      > 0 ? Severity.Low      :
						Severity.Info,
				};
			}
		}
		catch ( Exception ex )
		{
			DiagnosticsLog.Warn( $"[secbox] LibraryRowBadge cache refresh failed: {ex.Message}" );
		}
	}

	static string IdentOf( object lib )
	{
		if ( lib is LibraryProject lp )
		{
			try
			{
				var proj = lp.Project;
				return proj?.Package?.FullIdent ?? proj?.Package?.Ident;
			}
			catch { }
		}
		if ( lib is Package pkg )
		{
			try { return pkg.FullIdent ?? pkg.Ident; }
			catch { }
		}
		return null;
	}

	static Color SeverityColor( Severity sev, Decision decision )
	{
		// Decision dominates: explicit user decisions colorize regardless of
		// finding counts so the user can see TrustAlways / Block at a glance.
		switch ( decision )
		{
			case Decision.TrustAlways: return new Color( 0.30f, 0.69f, 0.31f );  // green
			case Decision.Block:       return new Color( 0.55f, 0.11f, 0.11f );  // dark red
			case Decision.Quarantine:  return new Color( 0.50f, 0.50f, 0.50f );  // grey
		}
		return sev switch
		{
			Severity.Critical => new Color( 0.90f, 0.22f, 0.21f ),  // red
			Severity.High     => new Color( 0.98f, 0.55f, 0.00f ),  // orange
			Severity.Medium   => new Color( 0.99f, 0.85f, 0.21f ),  // yellow
			Severity.Low      => new Color( 0.56f, 0.64f, 0.68f ),  // grey-blue
			_                 => new Color( 0.38f, 0.49f, 0.55f ),  // info
		};
	}

	static void ResetState()
	{
		_wrapped.Clear();
		Installed = false;
	}

	readonly struct TrustSnapshot
	{
		public bool Has { get; init; }
		public Decision Decision { get; init; }
		public Severity MaxSeverity { get; init; }
		public static TrustSnapshot Missing => default;
	}
}