Editor/UI/ReviewWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox.SecBox.Bridge.Dto;

namespace Sandbox.SecBox.UI;

// Custom Qt review window. Two tabs:
//  - Default: plain-language summary of concerns, Critical first. Uses
//    FindingTranslator to convert RuleIds to short titles and explanations.
//  - Advanced: the original per-finding card list (technical view).
//
// MUST be constructed on the editor main thread. Callers should wrap via
// MainThread.Queue if they're on a thread-pool thread.
public sealed class ReviewWindow : BaseWindow
{
	readonly string _packageIdent;
	string _contentHash;
	List<Finding> _findings;
	readonly TrustStore _store;
	readonly Action<Decision> _onDecision;

	// Layout slots we rebuild when findings are merged in.
	Layout _headerSlot;
	Layout _chipsSlot;
	TabWidget _tabs;
	ScrollArea _defaultScroll;
	ScrollArea _advancedScroll;

	const string CssCard         = "background-color: #2b2b2f; border-radius: 6px; padding: 8px 10px;";
	const string CssRuleId       = "font-family: 'Consolas','Menlo',monospace; color: #c5cad1; font-size: 11px;";
	const string CssMessage      = "color: #e8eaee; font-size: 13px;";
	const string CssLocation     = "color: #9aa0a6; font-size: 11px; font-family: monospace;";
	const string CssFixHint      = "color: #81c784; font-size: 11px;";
	const string CssChipBase     = "color: white; padding: 3px 9px; border-radius: 10px; font-size: 11px; font-weight: 700;";
	const string CssH1           = "font-size: 18px; font-weight: 700; color: #ffffff;";
	const string CssSubtle       = "color: #9aa0a6; font-size: 11px;";
	const string CssDefaultTitle = "color: #ffffff; font-size: 14px; font-weight: 700;";
	const string CssDefaultBody  = "color: #e8eaee; font-size: 12px;";
	const string CssCountTag     = "color: #9aa0a6; font-size: 11px;";

	public ReviewWindow(
		string packageIdent,
		string contentHash,
		IList<Finding> findings,
		TrustStore store,
		Action<Decision> onDecision) : base()
	{
		_packageIdent = packageIdent;
		_contentHash = contentHash;
		_findings = (findings ?? new List<Finding>()).ToList();
		_store = store;
		_onDecision = onDecision ?? (_ => { });

		DeleteOnClose = true;
		Size = new Vector2(820, 640);

		Layout = Layout.Column();
		Layout.Margin = 16;
		Layout.Spacing = 12;

		_headerSlot = Layout.AddColumn();
		_chipsSlot = Layout.AddRow();
		BuildTabs(); // populates _tabs, _defaultScroll, _advancedScroll
		BuildFooter();

		RebuildHeaderAndChips();
	}

	// Live view of the currently-displayed merged findings. ReviewDialog
	// reads this when the user clicks a decision button.
	public IReadOnlyList<Finding> CurrentFindings => _findings;

	// Fires after the window is destroyed (Qt-level). ReviewDialog uses this
	// to forget the window from its open-window registry.
	public event Action Destroyed;

	public override void OnDestroyed()
	{
		try { Destroyed?.Invoke(); } catch { }
		base.OnDestroyed();
	}

	// Called by ReviewDialog when a follow-up scan of the same package fires.
	// Merges new findings (by RuleId + Location) into the existing set,
	// rebuilds the header/chips/lists, and refreshes the title.
	public void MergeFindings(string contentHash, IList<Finding> incoming)
	{
		if (incoming == null || incoming.Count == 0) return;

		_contentHash = contentHash;

		var seen = new HashSet<string>(_findings.Select(f => $"{f.RuleId}|{f.Location}"));
		foreach (var f in incoming)
		{
			var key = $"{f.RuleId}|{f.Location}";
			if (seen.Add(key)) _findings.Add(f);
		}

		RebuildHeaderAndChips();
		RebuildFindingsLists();

		try { Raise(); }
		catch { }
	}

	void RebuildHeaderAndChips()
	{
		var critical = _findings.Count(f => f.Severity == Severity.Critical);
		var high = _findings.Count(f => f.Severity == Severity.High);
		var medium = _findings.Count(f => f.Severity == Severity.Medium);
		var low = _findings.Count(f => f.Severity == Severity.Low);

		WindowTitle = critical > 0
			? $"secbox - CRITICAL findings in {_packageIdent}"
			: $"secbox - review {_packageIdent}";
		SetWindowIcon(critical > 0 ? "report" : "policy");

		_headerSlot.Clear(true);
		_headerSlot.Spacing = 4;
		var title = new Label(_packageIdent);
		title.SetStyles(CssH1);
		_headerSlot.Add(title);
		var sub = new Label($"Content hash {_contentHash[..16]}… · {_findings.Count} findings · scanned {DateTime.Now:HH:mm:ss}");
		sub.SetStyles(CssSubtle);
		_headerSlot.Add(sub);

		_chipsSlot.Clear(true);
		_chipsSlot.Spacing = 8;
		AddChip(_chipsSlot, "Critical", critical, "#e53935");
		AddChip(_chipsSlot, "High",     high,     "#fb8c00");
		AddChip(_chipsSlot, "Medium",   medium,   "#fdd835");
		AddChip(_chipsSlot, "Low",      low,      "#90a4ae");
		_chipsSlot.AddStretchCell();
	}

	static void AddChip(Layout row, string label, int count, string bgHex)
	{
		var chip = new Label($"{label} {count}");
		chip.SetStyles($"{CssChipBase} background-color: {bgHex};");
		row.Add(chip);
	}

	void BuildTabs()
	{
		_tabs = new TabWidget(this);

		var defaultPage = new Widget(_tabs);
		defaultPage.Layout = Layout.Column();
		defaultPage.Layout.Margin = 0;
		defaultPage.Layout.Spacing = 0;
		_defaultScroll = defaultPage.Layout.Add(new ScrollArea(defaultPage), 1);
		_defaultScroll.Canvas = new Widget(_defaultScroll);
		_defaultScroll.Canvas.Layout = Layout.Column();
		_defaultScroll.Canvas.Layout.Margin = 4;
		_defaultScroll.Canvas.Layout.Spacing = 8;

		var advancedPage = new Widget(_tabs);
		advancedPage.Layout = Layout.Column();
		advancedPage.Layout.Margin = 0;
		advancedPage.Layout.Spacing = 0;
		_advancedScroll = advancedPage.Layout.Add(new ScrollArea(advancedPage), 1);
		_advancedScroll.Canvas = new Widget(_advancedScroll);
		_advancedScroll.Canvas.Layout = Layout.Column();
		_advancedScroll.Canvas.Layout.Margin = 4;
		_advancedScroll.Canvas.Layout.Spacing = 6;

		_tabs.AddPage("Default", "shield", defaultPage);
		_tabs.AddPage("Advanced", "code", advancedPage);
		_tabs.StateCookie = "secbox.review-window.tab";

		Layout.Add(_tabs, 1);

		PopulateDefaultList();
		PopulateAdvancedList();
	}

	void RebuildFindingsLists()
	{
		if (_defaultScroll?.Canvas?.Layout != null)
		{
			_defaultScroll.Canvas.Layout.Clear(true);
			PopulateDefaultList();
		}
		if (_advancedScroll?.Canvas?.Layout != null)
		{
			_advancedScroll.Canvas.Layout.Clear(true);
			PopulateAdvancedList();
		}
	}

	// --------------------------------------------------------------
	// Default tab: grouped plain-language concerns. Critical first.
	// --------------------------------------------------------------

	void PopulateDefaultList()
	{
		var layout = _defaultScroll.Canvas.Layout;

		if (_findings.Count == 0)
		{
			var ok = new Label("No findings. Nothing to review.");
			ok.SetStyles(CssDefaultBody);
			ok.WordWrap = true;
			layout.Add(ok);
			layout.AddStretchCell();
			return;
		}

		// Group by translated title. Group severity = worst observed.
		var groups = new Dictionary<string, ConcernGroup>(StringComparer.Ordinal);
		foreach (var f in _findings)
		{
			var exp = FindingTranslator.Translate(f.RuleId);
			if (!groups.TryGetValue(exp.Title, out var g))
			{
				g = new ConcernGroup { Explanation = exp };
				groups[exp.Title] = g;
			}
			g.Findings.Add(f);
			if (f.Severity > g.WorstSeverity) g.WorstSeverity = f.Severity;
		}

		var critCount = _findings.Count(f => f.Severity == Severity.Critical);
		var highCount = _findings.Count(f => f.Severity == Severity.High);
		var summary = new Label(BuildSummaryText(critCount, highCount, _findings.Count));
		summary.SetStyles(CssDefaultBody);
		summary.WordWrap = true;
		layout.Add(summary);
		layout.AddSpacingCell(4);

		var ordered = groups.Values
			.OrderByDescending(g => g.WorstSeverity)
			.ThenBy(g => g.Explanation.Title, StringComparer.Ordinal);

		foreach (var g in ordered)
			layout.Add(BuildConcernCard(g));

		layout.AddStretchCell();
	}

	static string BuildSummaryText(int crit, int high, int total)
	{
		if (crit > 0 && high > 0)
			return $"This package has {crit} critical and {high} high-severity concern{(crit + high == 1 ? "" : "s")}. Review the items below before granting trust.";
		if (crit > 0)
			return $"This package has {crit} critical concern{(crit == 1 ? "" : "s")}. Review carefully before granting trust.";
		if (high > 0)
		{
			var rest = total - high;
			return $"This package has {high} high-severity concern{(high == 1 ? "" : "s")} and {rest} lower-severity finding{(rest == 1 ? "" : "s")}.";
		}
		return $"This package has {total} lower-severity finding{(total == 1 ? "" : "s")}. None are critical.";
	}

	Widget BuildConcernCard(ConcernGroup g)
	{
		var sev = SeverityHex(g.WorstSeverity);

		var card = new Widget { Layout = Layout.Column() };
		card.Layout.Spacing = 4;
		card.Layout.Margin = 0;
		card.SetStyles(
			"background-color: #2b2b2f; "
			+ "border-radius: 4px; "
			+ "padding: 10px 12px 10px 14px;");

		var headerRow = card.Layout.AddRow();
		headerRow.Spacing = 8;

		// SEVERITY tag - text label so colour is not the sole signal.
		var sevTag = new Label(g.WorstSeverity.ToString().ToUpperInvariant());
		sevTag.SetStyles($"{CssChipBase} background-color: {sev};");
		headerRow.Add(sevTag);

		var title = new Label(g.Explanation.Title);
		title.SetStyles(CssDefaultTitle);
		title.TextSelectable = true;
		headerRow.Add(title);
		headerRow.AddStretchCell();

		var countTag = new Label(g.Findings.Count == 1
			? "1 finding"
			: $"{g.Findings.Count} findings");
		countTag.SetStyles(CssCountTag + $"border-left: 4px solid {sev};");
		headerRow.Add(countTag);

		var plain = new Label(g.Explanation.Plain);
		plain.SetStyles(CssDefaultBody);
		plain.WordWrap = true;
		plain.TextSelectable = true;
		card.Layout.Add(plain);

		// Up to 3 example locations - keep the card compact. Full list lives on the Advanced tab.
		var allDistinct = g.Findings
			.Where(f => !string.IsNullOrEmpty(f.Location))
			.Select(f => f.Location)
			.Distinct(StringComparer.Ordinal)
			.ToList();

		if (allDistinct.Count > 0)
		{
			card.Layout.AddSpacingCell(2);
			foreach (var loc in allDistinct.Take(3))
			{
				var locLabel = new Label("· " + loc);
				locLabel.SetStyles(CssLocation);
				locLabel.WordWrap = true;
				locLabel.TextSelectable = true;
				card.Layout.Add(locLabel);
			}
			if (allDistinct.Count > 3)
			{
				var more = new Label($"… and {allDistinct.Count - 3} more - see Advanced tab");
				more.SetStyles(CssSubtle);
				card.Layout.Add(more);
			}
		}

		return card;
	}

	sealed class ConcernGroup
	{
		public FindingTranslator.Explanation Explanation;
		public Severity WorstSeverity = Severity.Low;
		public List<Finding> Findings = new();
	}

	// --------------------------------------------------------------
	// Advanced tab: original per-finding card list.
	// --------------------------------------------------------------

	void PopulateAdvancedList()
	{
		var layout = _advancedScroll.Canvas.Layout;
		var sorted = _findings.OrderByDescending(f => f.Severity).ThenBy(f => f.RuleId);
		foreach (var f in sorted)
			layout.Add(BuildFindingCard(f));
		layout.AddStretchCell();
	}

	Widget BuildFindingCard(Finding f)
	{
		var sev = SeverityHex(f.Severity);

		var card = new Widget();
		card.Layout = Layout.Column();
		card.Layout.Spacing = 3;
		card.Layout.Margin = 0;
		// 4px colored left border via CSS. Padding-left is bumped to give the
		// content room beside the stripe.
		card.SetStyles(
			"background-color: #2b2b2f; "
			+ "border-radius: 4px; "
			+ "padding: 8px 10px 8px 12px;");

		// Compact header line: SEVERITY + ruleId + [finderId], severity tinted.
		var ruleId = new Label($"{f.Severity.ToString().ToUpperInvariant()}  {f.RuleId}  [{f.FinderId ?? "?"}]");
		ruleId.SetStyles(
			$"{CssRuleId} color: {sev};"
			+ $"border-left: 4px solid {sev};");
		ruleId.TextSelectable = true;
		card.Layout.Add(ruleId);

		var msg = new Label(f.Message ?? "");
		msg.SetStyles(CssMessage);
		msg.WordWrap = true;
		msg.TextSelectable = true;
		card.Layout.Add(msg);

		if (!string.IsNullOrEmpty(f.Location))
		{
			var loc = new Label(f.Location);
			loc.SetStyles(CssLocation);
			loc.WordWrap = true;
			loc.TextSelectable = true;
			card.Layout.Add(loc);
		}

		if (!string.IsNullOrEmpty(f.FixHint))
		{
			var hint = new Label("→ " + f.FixHint);
			hint.SetStyles(CssFixHint);
			hint.WordWrap = true;
			hint.TextSelectable = true;
			card.Layout.Add(hint);
		}

		return card;
	}

	// --------------------------------------------------------------
	// Footer (decision buttons) - unchanged.
	// --------------------------------------------------------------

	void BuildFooter()
	{
		var row = Layout.AddRow();
		row.Spacing = 8;

		var openHash = new Button("Copy hash");
		openHash.Icon = "content_copy";
		openHash.Clicked = () => { try { EditorUtility.Clipboard.Copy(_contentHash); } catch { } };
		row.Add(openHash);

		row.AddStretchCell();

		var cancel = new Button("Decide later");
		cancel.Clicked = () => { _onDecision(Decision.NotReviewed); Close(); };
		row.Add(cancel);

		var allowOnce = new Button("Allow this session");
		allowOnce.Clicked = () => { _onDecision(Decision.AllowOnce); Close(); };
		row.Add(allowOnce);

		var block = new Button("Block");
		block.Icon = "block";
		block.SetStyles("background-color: #6a1b1b; color: white;");
		block.Clicked = () => { _onDecision(Decision.Block); Close(); };
		row.Add(block);

		var trust = new Button.Primary("Trust this version");
		trust.Icon = "verified";
		trust.Clicked = () => { _onDecision(Decision.TrustAlways); Close(); };
		row.Add(trust);
	}

	static string SeverityHex(Severity s) => s switch
	{
		Severity.Critical => "#e53935",
		Severity.High     => "#fb8c00",
		Severity.Medium   => "#fdd835",
		Severity.Low      => "#90a4ae",
		_                 => "#607d8b",
	};
}