package ui

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"image/color"
	"sort"
	"strings"
	"sync"
	"time"

	"sysmon/internal/graph"
	"sysmon/internal/monitor"
	"sysmon/internal/profile"
	"sysmon/internal/scheduler"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"

	"github.com/shirou/gopsutil/v3/cpu"
)

// Build metadata (safe defaults). Optionally override via -ldflags.
// Example:
//
//	go build -ldflags "-X 'sysmon/internal/ui.AppVersion=2.1.0' -X 'sysmon/internal/ui.BuildInfo=2026-02-11'"
var AppTitle = "SysMon 2.0"
var AppVersion = "2.1.3"
var BuildInfo = "Developer"

type Snapshot struct {
	TS          time.Time      `json:"ts"`
	CPU         float64        `json:"cpu_pct"`
	RAM         float64        `json:"ram_pct"`
	NetUpBps    float64        `json:"net_up_bps"`
	NetDownBps  float64        `json:"net_down_bps"`
	Profile     map[string]any `json:"profile"`
	ProfileText string         `json:"profile_text,omitempty"`
}

func Build(a fyne.App) fyne.Window {
	w := a.NewWindow(AppTitle)

	// ------------------------------------------------------------
	// Lifecycle
	// ------------------------------------------------------------
	var cancel context.CancelFunc
	var wg sync.WaitGroup
	var running bool

	// ------------------------------------------------------------
	// Status LED + banner
	// ------------------------------------------------------------
	led := canvas.NewCircle(colorNRGBA(160, 160, 160, 255))
	led.Resize(fyne.NewSize(12, 12))
	statusLabel := widget.NewLabel("IDLE")
	warnBanner := widget.NewLabel("")
	warnBanner.Hide()

	setStatus := func(state string) {
		statusLabel.SetText(state)
		switch state {
		case "RUNNING":
			led.FillColor = colorNRGBA(0, 255, 198, 255)
		case "PAUSED":
			led.FillColor = colorNRGBA(153, 0, 0, 255)
		case "ALERT":
			led.FillColor = colorNRGBA(255, 170, 0, 255)
		default:
			led.FillColor = colorNRGBA(160, 160, 160, 255)
		}
		led.Refresh()
	}

	// ------------------------------------------------------------
	// Core meters
	// ------------------------------------------------------------
	cpuBar := widget.NewProgressBar()
	ramBar := widget.NewProgressBar()
	netLabel := widget.NewLabel("Net: 0 B/s down | 0 B/s up")

	// ------------------------------------------------------------
	// Histories + graph boxes
	// ------------------------------------------------------------
	cpuHist := graph.New(240)
	ramHist := graph.New(240)
	netUpHist := graph.New(240)
	netDownHist := graph.New(240)

	cpuGraphBox := container.NewWithoutLayout()
	memGraphBox := container.NewWithoutLayout()
	netGraphBox := container.NewWithoutLayout()

	// ------------------------------------------------------------
	// Snapshot storage (in-memory)
	// ------------------------------------------------------------
	var snapsMu sync.Mutex
	snaps := make([]Snapshot, 0, 512)

	// NEW: track selected snapshot (index into snaps slice)
	var selectedSnapIndex int = -1

	snapCount := widget.NewLabel("Snapshots: 0")
	refreshSnapCount := func() {
		snapsMu.Lock()
		n := len(snaps)
		snapsMu.Unlock()
		snapCount.SetText(fmt.Sprintf("Snapshots: %d", n))
	}

	// List view
	snapList := widget.NewList(
		func() int {
			snapsMu.Lock()
			defer snapsMu.Unlock()
			return len(snaps)
		},
		func() fyne.CanvasObject { return widget.NewLabel("snapshot") },
		func(i widget.ListItemID, o fyne.CanvasObject) {
			snapsMu.Lock()
			defer snapsMu.Unlock()
			idx := len(snaps) - 1 - i
			if idx < 0 || idx >= len(snaps) {
				o.(*widget.Label).SetText("")
				return
			}
			s := snaps[idx]
			o.(*widget.Label).SetText(
				fmt.Sprintf("%s | CPU %.1f%% | RAM %.1f%% | DN %.0f B/s | UP %.0f B/s",
					s.TS.Format("2006-01-02 15:04:05"),
					s.CPU, s.RAM, s.NetDownBps, s.NetUpBps,
				),
			)
		},
	)

	// NEW: selection mapping (list is reversed newest-first)
	snapList.OnSelected = func(id widget.ListItemID) {
		snapsMu.Lock()
		defer snapsMu.Unlock()
		if id < 0 || id >= len(snaps) {
			selectedSnapIndex = -1
			return
		}
		selectedSnapIndex = len(snaps) - 1 - id
	}
	snapList.OnUnselected = func(id widget.ListItemID) {
		snapsMu.Lock()
		selectedSnapIndex = -1
		snapsMu.Unlock()
	}

	// FIX: make snapshots list readable (not 1-line tall)
	snapListScroll := container.NewVScroll(snapList)
	snapListScroll.SetMinSize(fyne.NewSize(700, 420))

	// ------------------------------------------------------------
	// Per-core view
	// ------------------------------------------------------------
	showPerCore := false
	perCoreBars := make([]*widget.ProgressBar, 0, 32)
	perCoreBox := container.NewVBox()
	perCoreBox.Hide()

	rebuildPerCoreBars := func(n int) {
		perCoreBars = make([]*widget.ProgressBar, 0, n)
		rows := make([]fyne.CanvasObject, 0, n)
		for i := 0; i < n; i++ {
			pb := widget.NewProgressBar()
			perCoreBars = append(perCoreBars, pb)
			rows = append(rows, container.NewVBox(
				widget.NewLabel(fmt.Sprintf("Core %d", i)),
				pb,
			))
		}
		perCoreBox.Objects = rows
		perCoreBox.Refresh()
	}

	// ------------------------------------------------------------
	// Profile engine (System tab)
	// ------------------------------------------------------------
	var profileMu sync.Mutex
	profileText := "(press Refresh to collect profile)"
	useSudo := false
	useSecurity := false

	// Snapshot/export controls (Settings tab)
	includeProfileText := true
	includeRawInProfileJSON := false

	sysTitle := widget.NewLabelWithStyle(AppTitle, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
	sysBuild := widget.NewLabel(fmt.Sprintf("Version: %s   |   Build: %s", AppVersion, BuildInfo))

	sysEntry := widget.NewMultiLineEntry()
	sysEntry.SetText(profileText)
	sysEntry.Wrapping = fyne.TextWrapWord
	sysEntry.SetPlaceHolder("")

	sysScroll := container.NewVScroll(sysEntry)
	sysScroll.SetMinSize(fyne.NewSize(700, 420))

	useSudoCheck := widget.NewCheck("Run profile with sudo (requires `sudo -v`)", func(v bool) { useSudo = v })
	useSecurityCheck := widget.NewCheck("Enable security introspection (ports, firewall, services)", func(v bool) { useSecurity = v })

	refreshProfileBtn := widget.NewButton("Refresh Profile", nil)
	refreshProfileBtn.OnTapped = func() {
		refreshProfileBtn.Disable()
		sysEntry.SetText("Collecting system profile...\n\nThis may take a few seconds depending on installed tools.")

		go func() {
			rep := profile.Collect(profile.Options{UseSudo: useSudo, Security: useSecurity})
			out := rep.String()

			profileMu.Lock()
			profileText = out
			profileMu.Unlock()

			// No CallOnMain (matches your baseline behavior)
			sysEntry.SetText(out)
			refreshProfileBtn.Enable()
		}()
	}

	// ------------------------------------------------------------
	// Telemetry targets (smoothed)
	// ------------------------------------------------------------
	var targetsMu sync.Mutex
	var targetCPU, targetRAM float64
	var targetUpBps, targetDownBps float64
	var lastStats monitor.Stats

	var curCPU, curRAM float64
	var curUpBps, curDownBps float64

	// ------------------------------------------------------------
	// Snapshot builder (REUSED: manual + scheduler)
	// ------------------------------------------------------------
	buildAndStoreSnapshot := func() bool {
		targetsMu.Lock()
		s := lastStats
		targetsMu.Unlock()
		if s.TS.IsZero() {
			return false
		}

		profileMu.Lock()
		p := profileText
		profileMu.Unlock()

		snap := Snapshot{
			TS:         s.TS,
			CPU:        s.CPU,
			RAM:        s.RAM,
			NetUpBps:   s.NetUpBps,
			NetDownBps: s.NetDownBps,
			Profile:    parseProfileText(p, includeRawInProfileJSON),
		}
		if includeProfileText {
			snap.ProfileText = p
		}

		snapsMu.Lock()
		snaps = append(snaps, snap)
		snapsMu.Unlock()

		refreshSnapCount()
		snapList.Refresh()
		return true
	}

	// ------------------------------------------------------------
	// Buttons
	// ------------------------------------------------------------
	startBtn := widget.NewButton("Start", nil)
    pauseBtn := widget.NewButton("Pause", nil)
    snapBtn := widget.NewButton("Snapshot", nil)
    exportJSONBtn := widget.NewButton("Export JSON", nil)
    exportCSVBtn := widget.NewButton("Export CSV", nil)
    exportHTMLBtn := widget.NewButton("Export HTML", nil)
    bulkExportBtn := widget.NewButton("Export Selected...", nil)
    quitBtn := widget.NewButton("Quit", nil)

	pauseBtn.Disable()
	snapBtn.Disable()
	exportJSONBtn.Disable()
	exportCSVBtn.Disable()
	exportHTMLBtn.Disable()

	// ------------------------------------------------------------
	// Render loop (30fps smoothing)
	// ------------------------------------------------------------
	go func() {
		ticker := time.NewTicker(33 * time.Millisecond)
		defer ticker.Stop()

		const easing = 0.18
		for range ticker.C {
			targetsMu.Lock()
			tCPU := targetCPU
			tRAM := targetRAM
			tUp := targetUpBps
			tDown := targetDownBps
			targetsMu.Unlock()

			curCPU += (tCPU - curCPU) * easing
			curRAM += (tRAM - curRAM) * easing
			curUpBps += (tUp - curUpBps) * easing
			curDownBps += (tDown - curDownBps) * easing

			cpuBar.SetValue(curCPU / 100.0)
			ramBar.SetValue(curRAM / 100.0)
			netLabel.SetText(fmt.Sprintf("Net: %.0f B/s down | %.0f B/s up", curDownBps, curUpBps))

			cpuSpark := graph.DrawSparkline(cpuHist.Values(), 620, 120, 100)
			if cpuSpark != nil {
				cpuGraphBox.Objects = []fyne.CanvasObject{cpuSpark}
				cpuGraphBox.Refresh()
			}

			memSpark := graph.DrawSparkline(ramHist.Values(), 620, 120, 100)
			if memSpark != nil {
				memGraphBox.Objects = []fyne.CanvasObject{memSpark}
				memGraphBox.Refresh()
			}

			netSpark := graph.DrawDualSparkline(netDownHist.Values(), netUpHist.Values(), 620, 120, 0, 0)
			if netSpark != nil {
				netGraphBox.Objects = []fyne.CanvasObject{netSpark}
				netGraphBox.Refresh()
			}
		}
	}()

	// ------------------------------------------------------------
	// Start / Resume (monitor)
	// ------------------------------------------------------------
	startBtn.OnTapped = func() {
		if running {
			return
		}

		running = true
		startBtn.Disable()
		pauseBtn.Enable()
		snapBtn.Enable()
		exportJSONBtn.Enable()
		exportCSVBtn.Enable()
		exportHTMLBtn.Enable()
		warnBanner.Hide()
		setStatus("RUNNING")

		statsChan := make(chan monitor.Stats)
		ctx, c := context.WithCancel(context.Background())
		cancel = c

		wg.Add(1)
		go func() {
			defer wg.Done()
			monitor.Start(ctx, statsChan)
			close(statsChan)
		}()

		wg.Add(1)
		go func() {
			defer wg.Done()
			for s := range statsChan {
				targetsMu.Lock()
				targetCPU = s.CPU
				targetRAM = s.RAM
				targetUpBps = s.NetUpBps
				targetDownBps = s.NetDownBps
				lastStats = s
				targetsMu.Unlock()

				cpuHist.Add(s.CPU)
				ramHist.Add(s.RAM)
				netUpHist.Add(s.NetUpBps)
				netDownHist.Add(s.NetDownBps)
			}
		}()

		if showPerCore {
			wg.Add(1)
			go func() {
				defer wg.Done()
				t := time.NewTicker(500 * time.Millisecond)
				defer t.Stop()
				for {
					select {
					case <-ctx.Done():
						return
					case <-t.C:
						cores, err := cpu.Percent(0, true)
						if err != nil || len(cores) == 0 {
							continue
						}
						if len(perCoreBars) != len(cores) {
							rebuildPerCoreBars(len(cores))
						}
						for i := 0; i < len(cores) && i < len(perCoreBars); i++ {
							perCoreBars[i].SetValue(cores[i] / 100.0)
						}
					}
				}
			}()
		}
	}

	// ------------------------------------------------------------
	// Pause (monitor)
	// ------------------------------------------------------------
	pauseBtn.OnTapped = func() {
		if !running {
			return
		}
		running = false
		pauseBtn.Disable()
		startBtn.Enable()
		setStatus("PAUSED")
		if cancel != nil {
			cancel()
		}
		wg.Wait()
	}

	// ------------------------------------------------------------
	// Snapshot (manual)
	// ------------------------------------------------------------
	snapBtn.OnTapped = func() {
		_ = buildAndStoreSnapshot()
	}

	// ------------------------------------------------------------
	// Exports
	// ------------------------------------------------------------
	snapshotsCopy := func() []Snapshot {
		snapsMu.Lock()
		defer snapsMu.Unlock()
		out := make([]Snapshot, len(snaps))
		copy(out, snaps)
		sort.Slice(out, func(i, j int) bool { return out[i].TS.Before(out[j].TS) })
		return out
	}
	
	bulkExportBtn.OnTapped = func() {
    data := snapshotsCopy()
    ShowBulkExportDialog(w, data)
    }


	exportJSONBtn.OnTapped = func() {
		data := snapshotsCopy()
		if len(data) == 0 {
			dialog.ShowInformation("Export JSON", "No snapshots to export.", w)
			return
		}

		// NEW: filename uses selected snapshot TS (as shown in list)
		snapsMu.Lock()
		if selectedSnapIndex < 0 || selectedSnapIndex >= len(snaps) {
			snapsMu.Unlock()
			dialog.ShowInformation("Export JSON", "No snapshot selected.", w)
			return
		}
		selTS := snaps[selectedSnapIndex].TS
		snapsMu.Unlock()

		d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
			if err != nil || uc == nil {
				return
			}
			defer uc.Close()
			enc := json.NewEncoder(uc)
			enc.SetIndent("", "  ")
			_ = enc.Encode(data)
		}, w)

		d.SetFileName(fmt.Sprintf("sysmon_snapshot_%s.json", selTS.Format("20060102_150405")))
		d.Show()
	}

	exportCSVBtn.OnTapped = func() {
		data := snapshotsCopy()
		if len(data) == 0 {
			dialog.ShowInformation("Export CSV", "No snapshots to export.", w)
			return
		}
		flatten := func(s string) string {
			s = strings.ReplaceAll(s, "\r\n", "\n")
			s = strings.ReplaceAll(s, "\n", " | ")
			s = strings.Join(strings.Fields(s), " ")
			return s
		}

		// NEW: filename uses selected snapshot TS (as shown in list)
		snapsMu.Lock()
		if selectedSnapIndex < 0 || selectedSnapIndex >= len(snaps) {
			snapsMu.Unlock()
			dialog.ShowInformation("Export CSV", "No snapshot selected.", w)
			return
		}
		selTS := snaps[selectedSnapIndex].TS
		snapsMu.Unlock()

		d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
			if err != nil || uc == nil {
				return
			}
			defer uc.Close()
			wr := csv.NewWriter(uc)
			_ = wr.Write([]string{"ts", "cpu_pct", "ram_pct", "net_up_bps", "net_down_bps", "profile_json", "profile_text"})
			for _, s := range data {
				_ = wr.Write([]string{
					s.TS.Format(time.RFC3339),
					fmt.Sprintf("%.3f", s.CPU),
					fmt.Sprintf("%.3f", s.RAM),
					fmt.Sprintf("%.0f", s.NetUpBps),
					fmt.Sprintf("%.0f", s.NetDownBps),
					string(mustJSON(s.Profile)),
					flatten(s.ProfileText),
				})
			}
			wr.Flush()
		}, w)

		d.SetFileName(fmt.Sprintf("sysmon_snapshot_%s.csv", selTS.Format("20060102_150405")))
		d.Show()
	}

	exportHTMLBtn.OnTapped = func() {
		data := snapshotsCopy()
		if len(data) == 0 {
			dialog.ShowInformation("Export HTML", "No snapshots to export.", w)
			return
		}

		// NEW: filename uses selected snapshot TS (as shown in list)
		snapsMu.Lock()
		if selectedSnapIndex < 0 || selectedSnapIndex >= len(snaps) {
			snapsMu.Unlock()
			dialog.ShowInformation("Export HTML", "No snapshot selected.", w)
			return
		}
		selTS := snaps[selectedSnapIndex].TS
		snapsMu.Unlock()

		profileMu.Lock()
		p := profileText
		profileMu.Unlock()
		d := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
			if err != nil || uc == nil {
				return
			}
			defer uc.Close()
			var b strings.Builder
			b.WriteString("<!doctype html><html><head><meta charset='utf-8'>")
			b.WriteString("<title>SysMon Snapshots</title>")
			b.WriteString("<style>")
			b.WriteString("body{font-family:system-ui,Segoe UI,Arial;background:#0c0c0e;color:#e6e6e6;padding:16px}")
			b.WriteString("table{border-collapse:collapse;width:100%;margin-top:12px}")
			b.WriteString("th,td{border:1px solid #333;padding:8px;text-align:left;vertical-align:top}")
			b.WriteString("th{background:#111217}")
			b.WriteString("pre{background:#111217;border:1px solid #333;padding:12px;white-space:pre-wrap}")
			b.WriteString(".accent{color:#00ffc6}")
			b.WriteString("</style></head><body>")
			b.WriteString("<h1>" + htmlEscape(AppTitle) + " Snapshots</h1>")
			b.WriteString("<div class='accent'>Version: " + htmlEscape(AppVersion) + " | Build: " + htmlEscape(BuildInfo) + "</div>")
			b.WriteString("<div class='accent'>Generated: " + time.Now().Format("2006-01-02 15:04:05") + "</div>")
			b.WriteString("<h2>System Profile</h2><pre>" + htmlEscape(p) + "</pre>")
			b.WriteString("<h2>Snapshot</h2>")
			b.WriteString("<table><thead><tr>")
			b.WriteString("<th>Timestamp</th><th>CPU %</th><th>RAM %</th><th>Net Down (B/s)</th><th>Net Up (B/s)</th>")
			b.WriteString("</tr></thead><tbody>")
			for _, s := range data {
				b.WriteString("<tr>")
				b.WriteString("<td>" + s.TS.Format("2006-01-02 15:04:05") + "</td>")
				b.WriteString(fmt.Sprintf("<td>%.2f</td>", s.CPU))
				b.WriteString(fmt.Sprintf("<td>%.2f</td>", s.RAM))
				b.WriteString(fmt.Sprintf("<td>%.0f</td>", s.NetDownBps))
				b.WriteString(fmt.Sprintf("<td>%.0f</td>", s.NetUpBps))
				b.WriteString("</tr>")
			}
			b.WriteString("</tbody></table></body></html>")
			_, _ = uc.Write([]byte(b.String()))
		}, w)

		d.SetFileName(fmt.Sprintf("sysmon_snapshot_%s.html", selTS.Format("20060102_150405")))
		d.Show()
	}

	// ------------------------------------------------------------
	// Scheduler (real snapshot + readable expanding log)
	// ------------------------------------------------------------
	schedStatusLabel := widget.NewLabel("OFF")

	// IMPORTANT: do NOT Disable() or it becomes grey-on-grey.
	schedLog := widget.NewMultiLineEntry()
	schedLog.Wrapping = fyne.TextWrapWord
	schedLog.SetText("Logging events will appear here.\n")

	schedScroll := container.NewVScroll(schedLog)
	schedScroll.SetMinSize(fyne.NewSize(700, 420))

	appendSchedLog := func(line string) {
		cur := schedLog.Text
		if len(cur) > 60000 {
			cur = cur[len(cur)-32000:]
		}
		schedLog.SetText(cur + line)
	}

	// local notes so the callback can log “snapshot captured / skipped”
	uiNotes := make(chan string, 256)

	sched := scheduler.New(func(ctx context.Context) error {
		ok := buildAndStoreSnapshot()
		if ok {
			select {
			case uiNotes <- "Snapshot Captured":
			default:
			}
		} else {
			select {
			case uiNotes <- "snapshot skipped (no telemetry yet)":
			default:
			}
		}
		return nil
	})

	go func() {
		for {
			select {
			case ev, ok := <-sched.Events():
				if !ok {
					return
				}
				if ev.Status.Running {
					schedStatusLabel.SetText(fmt.Sprintf("RUNNING (%d)", ev.Status.Count))
				} else {
					schedStatusLabel.SetText("OFF")
				}
				appendSchedLog(fmt.Sprintf("[%s] %s\n", ev.When.Format("15:04:05"), ev.Note))

			case note := <-uiNotes:
				appendSchedLog(fmt.Sprintf("[%s] %s\n", time.Now().Format("15:04:05"), note))
			}
		}
	}()

	intervalEntry := widget.NewEntry()
	intervalEntry.SetText("60")

	stopAfterEntry := widget.NewEntry()
	stopAfterEntry.SetText("")

	runOnStartCheck := widget.NewCheck("Run snapshot immediately on start", func(bool) {})
	runOnStartCheck.SetChecked(true)

	schedStartBtn := widget.NewButton("Start Logging", func() {
		var n int
		if _, err := fmt.Sscanf(strings.TrimSpace(intervalEntry.Text), "%d", &n); err != nil || n <= 0 {
			dialog.ShowInformation("Logging", "Invalid interval seconds.", w)
			return
		}

		stopAfter := time.Duration(0)
		if strings.TrimSpace(stopAfterEntry.Text) != "" {
			var sn int
			if _, err := fmt.Sscanf(strings.TrimSpace(stopAfterEntry.Text), "%d", &sn); err != nil || sn <= 0 {
				dialog.ShowInformation("Logging", "Invalid stop-after seconds.", w)
				return
			}
			stopAfter = time.Duration(sn) * time.Second
		}

		cfg := scheduler.Config{
			Mode: scheduler.ModeInterval,
			Interval: scheduler.IntervalConfig{
				Every:      time.Duration(n) * time.Second,
				StopAfter:  stopAfter,
				RunOnStart: runOnStartCheck.Checked,
			},
		}

		if err := sched.Start(cfg); err != nil {
			dialog.ShowError(err, w)
			return
		}
		appendSchedLog(fmt.Sprintf("[%s] log scheduler started (every %ds)\n", time.Now().Format("15:04:05"), n))
	})

	schedStopBtn := widget.NewButton("Stop Logging", func() {
		sched.Stop()
		appendSchedLog(fmt.Sprintf("[%s] Logging stopped\n", time.Now().Format("15:04:05")))
	})

	schedulerTop := container.NewVBox(
		widget.NewLabelWithStyle("Snapshot Logging", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		widget.NewSeparator(),
		widget.NewLabel("Status:"),
		schedStatusLabel,
		widget.NewSeparator(),
		widget.NewLabel("Interval (seconds)"),
		intervalEntry,
		widget.NewLabel("Stop after (seconds, optional)"),
		stopAfterEntry,
		runOnStartCheck,
		container.NewHBox(schedStartBtn, schedStopBtn),
		widget.NewSeparator(),
		widget.NewLabel("System Log"),
	)

	// Border makes log consume remaining height (fixes your screenshots)
	loggingTabInner := container.NewBorder(
		schedulerTop,
		nil,
		nil,
		nil,
		schedScroll,
	)

	// ------------------------------------------------------------
	// Quit
	// ------------------------------------------------------------
	quitBtn.OnTapped = func() {
		// stop scheduler if running
		sched.Stop()

		if running && cancel != nil {
			cancel()
			wg.Wait()
		}
		a.Quit()
	}

	// ------------------------------------------------------------
	// Tabs
	// ------------------------------------------------------------
	perCoreToggle := widget.NewCheck("Show Per-Core CPU", func(v bool) {
		showPerCore = v
		if showPerCore {
			perCoreBox.Show()
		} else {
			perCoreBox.Hide()
		}
	})

	systemTabInner := container.NewVBox(
		sysTitle,
		sysBuild,
		widget.NewSeparator(),
		container.NewVBox(
			useSudoCheck,
			useSecurityCheck,
			container.NewHBox(refreshProfileBtn),
		),
		widget.NewSeparator(),
		sysScroll,
	)

	cpuTabInner := container.NewVBox(
		widget.NewLabel("CPU"),
		cpuBar,
		widget.NewLabel("CPU History"),
		cpuGraphBox,
		widget.NewSeparator(),
		perCoreToggle,
		perCoreBox,
	)
	cpuTab := container.NewVScroll(cpuTabInner)
	cpuTab.SetMinSize(fyne.NewSize(720, 520))

	memTabInner := container.NewVBox(
		widget.NewLabel("Memory"),
		ramBar,
		widget.NewLabel("Memory History"),
		memGraphBox,
	)

	netTabInner := container.NewVBox(
		widget.NewLabel("Network"),
		netLabel,
		widget.NewLabel("Network History (down=red, up=cyan)"),
		netGraphBox,
	)

	// Snapshots tab: Border makes list consume remaining height
	snapsHeader := container.NewVBox(
    container.NewHBox(snapBtn, exportJSONBtn, exportCSVBtn, exportHTMLBtn, bulkExportBtn),
    widget.NewSeparator(),
    )

	snapsTabInner := container.NewBorder(
		snapsHeader,
		nil,
		nil,
		nil,
		snapListScroll,
	)

	// Settings: now real controls (not a placeholder tab)
	includeTextCheck := widget.NewCheck("Include profile_text in snapshots (raw text)", func(v bool) {
		includeProfileText = v
	})
	includeTextCheck.SetChecked(includeProfileText)

	includeRawCheck := widget.NewCheck("Include raw profiler dump inside profile JSON", func(v bool) {
		includeRawInProfileJSON = v
	})
	includeRawCheck.SetChecked(includeRawInProfileJSON)

	wrapCheck := widget.NewCheck("Wrap profiler text (word wrap)", func(v bool) {
		if v {
			sysEntry.Wrapping = fyne.TextWrapWord
		} else {
			sysEntry.Wrapping = fyne.TextWrapOff
		}
		sysEntry.Refresh()
	})
	wrapCheck.SetChecked(true)

	settingsTabInner := container.NewVBox(
		widget.NewLabelWithStyle("Control Center", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
		widget.NewLabel("Snapshot + export toggles (affects JSON/CSV output)"),
		widget.NewSeparator(),
		includeTextCheck,
		includeRawCheck,
		widget.NewSeparator(),
		widget.NewLabel("UI controls"),
		wrapCheck,
		perCoreToggle, // duplicate toggle here for convenience
	)

	tabs := container.NewAppTabs(
		container.NewTabItem("System", systemTabInner),
		container.NewTabItem("Logging", loggingTabInner),
		container.NewTabItem("CPU", cpuTab),
		container.NewTabItem("Memory", memTabInner),
		container.NewTabItem("Network", netTabInner),
		container.NewTabItem("Snapshots", snapsTabInner),
		container.NewTabItem("Settings", settingsTabInner),
	)
	tabs.SetTabLocation(container.TabLocationTop)

	// ------------------------------------------------------------
	// Header + global controls (fixed bottom)
	// ------------------------------------------------------------
	setStatus("IDLE")
	refreshSnapCount()

	header := container.NewVBox(
		container.NewHBox(
			led,
			widget.NewLabel("  "),
			statusLabel,
			widget.NewLabel("   "+AppTitle),
			widget.NewLabel("    "),
			snapCount,
		),
		warnBanner,
	)

	// THESE are your buttons. They are back. Nothing removed.
	header = container.NewVBox(
		container.NewHBox(
			led,
			widget.NewLabel("  "),
			statusLabel,
			widget.NewLabel("   "+AppTitle),
			widget.NewLabel("    "),
			snapCount,
			widget.NewLabel("        "),
			startBtn,
			pauseBtn,
			quitBtn,
		),
		warnBanner,
	)

	w.SetContent(container.NewBorder(header, nil, nil, nil, tabs))
	w.Resize(fyne.NewSize(820, 860))
	return w

}

// parseProfileText converts the human-readable profiler output (rep.String())
// into a nested JSON-friendly structure for snapshots.
//
// It is intentionally tolerant: unknown lines are preserved under "raw_lines".
// Set includeRaw=true to embed the full raw dump as well.
func parseProfileText(s string, includeRaw bool) map[string]any {
	out := map[string]any{
		"raw_lines": []string{},
	}
	if includeRaw {
		out["raw"] = s
	}

	cpuM := map[string]any{}
	memM := map[string]any{}
	diskM := map[string]any{}
	netM := map[string]any{}
	secM := map[string]any{}
	general := map[string]any{}

	lines := strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		parts := strings.SplitN(line, ":", 2)
		if len(parts) != 2 {
			out["raw_lines"] = append(out["raw_lines"].([]string), line)
			continue
		}

		k := strings.TrimSpace(parts[0])
		v := strings.TrimSpace(parts[1])
		kl := strings.ToLower(k)

		switch {
		case strings.HasPrefix(kl, "cpu "):
			cpuM[strings.TrimSpace(k[4:])] = v
		case strings.Contains(kl, "ram") || strings.Contains(kl, "memory"):
			memM[k] = v
		case strings.HasPrefix(kl, "disk"):
			diskM[k] = v
		case strings.HasPrefix(kl, "net") || strings.Contains(kl, "network"):
			netM[k] = v
		case strings.Contains(kl, "port") || strings.Contains(kl, "firewall") || strings.Contains(kl, "service"):
			secM[k] = v
		default:
			general[k] = v
		}
	}

	if len(general) > 0 {
		out["general"] = general
	}
	if len(cpuM) > 0 {
		out["cpu"] = cpuM
	}
	if len(memM) > 0 {
		out["memory"] = memM
	}
	if len(diskM) > 0 {
		out["disk"] = diskM
	}
	if len(netM) > 0 {
		out["network"] = netM
	}
	if len(secM) > 0 {
		out["security"] = secM
	}

	return out
}

func mustJSON(v any) []byte {
	b, err := json.Marshal(v)
	if err != nil {
		return []byte("{}")
	}
	return b
}

func htmlEscape(s string) string {
	s = strings.ReplaceAll(s, "&", "&amp;")
	s = strings.ReplaceAll(s, "<", "&lt;")
	s = strings.ReplaceAll(s, ">", "&gt;")
	return s
}

func colorNRGBA(r, g, b, a uint8) color.NRGBA {
	return color.NRGBA{R: r, G: g, B: b, A: a}
}

