Editor/UI/SecboxStatusPanel.cs
using System;
using System.Linq;
using Editor;
using Sandbox.SecBox.Bridge;
using Sandbox.SecBox.Bridge.Dto;
using Sandbox.SecBox.Lifecycle;
using DiagnosticsLog = Sandbox.SecBox.Bridge.DiagnosticsLog;

namespace Sandbox.SecBox.UI;

// Compact status card injected into the Library Manager's detail pane. Shows
// the trust-store verdict for whatever package the detail pane is currently
// displaying, plus a Re-scan button. Read-only with respect to engine state;
// only writes to the SecBox trust store via the existing ReviewDialog flow.
//
// The host (LibraryDetailPanel) feeds us the live "current library" via the
// resolver delegate so this widget doesn't need its own reflection logic.
public sealed class SecboxStatusPanel : Widget
{
	readonly Func<object> _currentLibraryResolver;

	Label _identLabel;
	Label _statusLabel;
	Label _countsLabel;
	Button _rescanButton;
	Button _reviewButton;

	string _lastIdentShown;
	int _lastStoreVersion = -1;

	const string CssCard    = "background-color: #1f2024; border-radius: 6px; padding: 8px 10px;";
	const string CssHeader  = "color: #c5cad1; font-size: 11px; font-weight: 700; letter-spacing: 0.5px;";
	const string CssIdent   = "color: #e8eaee; font-size: 12px; font-family: 'Consolas','Menlo',monospace;";
	const string CssStatus  = "color: #9aa0a6; font-size: 11px;";
	const string CssCounts  = "color: #c5cad1; font-size: 11px; font-family: monospace;";

	public SecboxStatusPanel( Widget parent, Func<object> currentLibraryResolver ) : base( parent )
	{
		_currentLibraryResolver = currentLibraryResolver ?? (() => null);

		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 4;
		SetStyles( CssCard );

		// Start hidden - RefreshImpl flips us visible once it confirms the
		// detail pane is showing an installed LibraryProject. Avoids a 1-2
		// frame flash when the user opens the Browse tab.
		Visible = false;

		var header = new Label( "SECBOX" );
		header.SetStyles( CssHeader );
		Layout.Add( header );

		_identLabel = new Label( "(no library selected)" );
		_identLabel.SetStyles( CssIdent );
		_identLabel.WordWrap = true;
		Layout.Add( _identLabel );

		_statusLabel = new Label( "" );
		_statusLabel.SetStyles( CssStatus );
		Layout.Add( _statusLabel );

		_countsLabel = new Label( "" );
		_countsLabel.SetStyles( CssCounts );
		Layout.Add( _countsLabel );

		var buttonRow = Layout.AddRow();
		buttonRow.Spacing = 6;

		_rescanButton = new Button( "Re-scan" );
		_rescanButton.Icon = "refresh";
		_rescanButton.Clicked = OnRescan;
		buttonRow.Add( _rescanButton );

		_reviewButton = new Button( "Open Review…" );
		_reviewButton.Icon = "policy";
		_reviewButton.Clicked = OnOpenReview;
		buttonRow.Add( _reviewButton );

		buttonRow.AddStretchCell();
	}

	// Called by host on a low-frequency tick. Cheap when the displayed library
	// hasn't changed (early-out on _lastIdentShown comparison).
	public void Refresh()
	{
		try { RefreshImpl(); }
		catch ( Exception ex ) { DiagnosticsLog.Warn( $"SecboxStatusPanel.Refresh: {ex.Message}" ); }
	}

	void RefreshImpl()
	{
		var lib = _currentLibraryResolver();

		// Browse view exposes a Package (not yet installed locally); the SecBox
		// panel is meaningless there - no folder to hash, no trust entry to show.
		// Only the Installed view yields a LibraryProject. Hide otherwise.
		var showPanel = lib is LibraryProject;
		if ( Visible != showPanel ) Visible = showPanel;
		if ( !showPanel ) { _lastIdentShown = null; return; }

		var (ident, folder) = ResolveIdentAndFolder( lib );

		// Early-out: same library AND trust store unchanged. We still recompute
		// when TrustStore.Save() bumps its version (e.g. user clicked Block in
		// the review dialog) so the status text updates without a click-away.
		var storeVersion = TrustStore.Version;
		if ( ident == _lastIdentShown && storeVersion == _lastStoreVersion ) return;
		_lastIdentShown = ident;
		_lastStoreVersion = storeVersion;

		if ( string.IsNullOrEmpty( ident ) )
		{
			_identLabel.Text = "(no library selected)";
			_statusLabel.Text = "";
			_countsLabel.Text = "";
			_rescanButton.Enabled = false;
			_reviewButton.Enabled = false;
			return;
		}

		_identLabel.Text = ident;
		_rescanButton.Enabled = !string.IsNullOrEmpty( folder );
		_reviewButton.Enabled = !string.IsNullOrEmpty( folder );

		var projectRoot = PackageLocator.CurrentProjectRoot();
		if ( string.IsNullOrEmpty( projectRoot ) )
		{
			_statusLabel.Text = "no open project";
			_countsLabel.Text = "";
			return;
		}

		if ( string.IsNullOrEmpty( folder ) )
		{
			_statusLabel.Text = "engine/external package - out of scope";
			_countsLabel.Text = "";
			return;
		}

		string hash;
		try { hash = PackageHasher.HashFolder( folder ); }
		catch ( Exception ex )
		{
			_statusLabel.Text = $"hash failed: {ex.Message}";
			_countsLabel.Text = "";
			return;
		}

		var store = TrustStore.Load( projectRoot );
		var entry = store.Find( hash );
		if ( entry == null )
		{
			_statusLabel.Text = "not yet scanned - click Re-scan";
			_countsLabel.Text = "";
			return;
		}

		_statusLabel.Text = $"{entry.Decision} · reviewed {entry.ReviewedAt:yyyy-MM-dd HH:mm}";
		_countsLabel.Text = $"Crit={entry.CriticalCount}  High={entry.HighCount}  Med={entry.MediumCount}  Low={entry.LowCount}";
	}

	void OnRescan()
	{
		var lib = _currentLibraryResolver();
		var (ident, folder) = ResolveIdentAndFolder( lib );
		if ( string.IsNullOrEmpty( ident ) || string.IsNullOrEmpty( folder ) ) return;

		_statusLabel.Text = "scanning…";
		_lastIdentShown = null; // force refresh after scan

		// Off the UI thread - the Core scan can take seconds for big libs and
		// holds GetAwaiter().GetResult internally that would deadlock here.
		System.Threading.Tasks.Task.Run( () => RunScan( ident, folder ) );
	}

	void RunScan( string ident, string folder )
	{
		try
		{
			SecboxCoreClient.EnsureReadyAsync().GetAwaiter().GetResult();
			var report = SecboxCoreClient.ScanFolder( folder );

			var critical = report.Findings.Count( f => f.Severity == Severity.Critical );
			var high     = report.Findings.Count( f => f.Severity == Severity.High );
			var medium   = report.Findings.Count( f => f.Severity == Severity.Medium );
			var low      = report.Findings.Count( f => f.Severity == Severity.Low );

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

			var hash = PackageHasher.HashFolder( folder );
			var store = TrustStore.Load( projectRoot );

			store.Upsert( new TrustEntry
			{
				PackageIdent = ident,
				ContentHash = hash,
				Decision = store.Find( hash )?.Decision ?? Decision.NotReviewed,
				ReviewedAt = DateTime.UtcNow,
				CriticalCount = critical,
				HighCount = high,
				MediumCount = medium,
				LowCount = low,
				Notes = $"Manual re-scan from Library Manager. {report.Findings.Count} findings.",
			} );
			store.Save();

			DiagnosticsLog.Info( $"Library Manager re-scan {ident}: Crit={critical} High={high} Med={medium} Low={low}" );

			var maxSev = report.Findings.Count == 0 ? Severity.Info : report.Findings.Max( f => f.Severity );
			if ( maxSev >= store.Policy.PromptThreshold )
				ReviewDialog.Show( ident, hash, report.Findings, store );

			MainThread.Queue( Refresh );
		}
		catch ( Exception ex )
		{
			DiagnosticsLog.Error( $"Library Manager re-scan {ident} failed", ex );
			MainThread.Queue( () =>
			{
				_statusLabel.Text = $"scan failed: {ex.Message}";
			} );
		}
	}

	void OnOpenReview()
	{
		var lib = _currentLibraryResolver();
		var (ident, folder) = ResolveIdentAndFolder( lib );
		if ( string.IsNullOrEmpty( ident ) || string.IsNullOrEmpty( folder ) ) return;

		// Re-scan synchronously enough to populate the review dialog. If a
		// recent trust entry exists this is fine; otherwise the user effectively
		// gets a fresh scan + review in one click.
		System.Threading.Tasks.Task.Run( () =>
		{
			try
			{
				SecboxCoreClient.EnsureReadyAsync().GetAwaiter().GetResult();
				var report = SecboxCoreClient.ScanFolder( folder );
				var projectRoot = PackageLocator.CurrentProjectRoot();
				if ( string.IsNullOrEmpty( projectRoot ) ) return;
				var hash = PackageHasher.HashFolder( folder );
				var store = TrustStore.Load( projectRoot );
				ReviewDialog.Show( ident, hash, report.Findings, store );
			}
			catch ( Exception ex )
			{
				DiagnosticsLog.Error( $"open-review of {ident} failed", ex );
			}
		} );
	}

	// Extract (ident, folder) from whatever object the detail pane is showing.
	// Accepts either a LibraryProject (installed view) or a Package (browse).
	// Returns nulls when nothing identifiable is present.
	static (string ident, string folder) ResolveIdentAndFolder( object lib )
	{
		if ( lib == null ) return (null, null);

		// LibraryProject: has .Project.Package and .Project.RootDirectory.
		if ( lib is LibraryProject lp )
		{
			try
			{
				var proj = lp.Project;
				var ident = proj?.Package?.FullIdent ?? proj?.Package?.Ident;
				var folder = proj?.RootDirectory?.FullName;
				return (ident, folder);
			}
			catch { }
		}

		// Package: has .FullIdent / .Ident, no folder (not yet installed).
		if ( lib is Package pkg )
		{
			try
			{
				var ident = pkg.FullIdent ?? pkg.Ident;
				// Try PackageLocator in case the package is already installed locally.
				var folder = PackageLocator.FolderFor( pkg );
				return (ident, folder);
			}
			catch { }
		}

		// Fallback: reflect for common shapes.
		var p = ReflectionHelpers.GetProp( lib, "Package" );
		if ( p is Package pkg2 )
		{
			try
			{
				var ident = pkg2.FullIdent ?? pkg2.Ident;
				var folder = PackageLocator.FolderFor( pkg2 );
				return (ident, folder);
			}
			catch { }
		}

		return (null, null);
	}
}