package webui

import (
	"encoding/json"
	"errors"
	"fmt"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"bee/audit/internal/app"
	"bee/audit/internal/runtimeenv"
	gocharts "github.com/go-analyze/charts"
	"reanimator/chart/viewer"
	"reanimator/chart/web"
)

const defaultTitle = "Bee Hardware Audit"

func init() {
	// On some LiveCD ramdisk environments, /usr/share/mime/globs2 exists but
	// causes an I/O error mid-read. Go's mime package panics (not errors) in
	// that case, crashing the first HTTP goroutine that serves a static file.
	// Pre-trigger initialization here with recover so subsequent calls are safe.
	func() {
		defer func() { recover() }() //nolint:errcheck
		mime.TypeByExtension(".gz")
	}()
}

// HandlerOptions configures the web UI handler.
type HandlerOptions struct {
	Title       string
	AuditPath   string
	ExportDir   string
	App         *app.App
	RuntimeMode runtimeenv.Mode
}

// metricsRing holds a rolling window of live metric samples.
type metricsRing struct {
	mu    sync.Mutex
	vals  []float64
	times []time.Time
	size  int
}

func newMetricsRing(size int) *metricsRing {
	return &metricsRing{size: size, vals: make([]float64, 0, size), times: make([]time.Time, 0, size)}
}

func (r *metricsRing) push(v float64) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if len(r.vals) >= r.size {
		r.vals = r.vals[1:]
		r.times = r.times[1:]
	}
	r.vals = append(r.vals, v)
	r.times = append(r.times, time.Now())
}

func (r *metricsRing) snapshot() ([]float64, []string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	v := make([]float64, len(r.vals))
	copy(v, r.vals)
	now := time.Now()
	labels := make([]string, len(r.times))
	for i, t := range r.times {
		labels[i] = relAgeLabel(now.Sub(t))
	}
	return v, labels
}

func relAgeLabel(age time.Duration) string {
	if age <= 0 {
		return "0"
	}
	if age < time.Hour {
		m := int(age.Minutes())
		if m == 0 {
			return "-<1m"
		}
		return fmt.Sprintf("-%dm", m)
	}
	if age < 24*time.Hour {
		return fmt.Sprintf("-%dh", int(age.Hours()))
	}
	return fmt.Sprintf("-%dd", int(age.Hours()/24))
}

// gpuRings holds per-GPU ring buffers.
type gpuRings struct {
	Temp    *metricsRing
	Util    *metricsRing
	MemUtil *metricsRing
	Power   *metricsRing
}

// pendingNetChange tracks a network state change awaiting confirmation.
type pendingNetChange struct {
	iface string
	wasUp bool
	timer *time.Timer
	mu    sync.Mutex
}

// handler is the HTTP handler for the web UI.
type handler struct {
	opts        HandlerOptions
	mux         *http.ServeMux
	// server rings
	ringCPUTemp *metricsRing
	ringCPULoad *metricsRing
	ringMemLoad *metricsRing
	ringPower   *metricsRing
	ringFans    []*metricsRing
	fanNames    []string
	// per-GPU rings (index = GPU index)
	gpuRings    []*gpuRings
	ringsMu     sync.Mutex
	// install job (at most one at a time)
	installJob  *jobState
	installMu   sync.Mutex
	// pending network change (rollback on timeout)
	pendingNet   *pendingNetChange
	pendingNetMu sync.Mutex
}

// NewHandler creates the HTTP mux with all routes.
func NewHandler(opts HandlerOptions) http.Handler {
	if strings.TrimSpace(opts.Title) == "" {
		opts.Title = defaultTitle
	}
	if strings.TrimSpace(opts.ExportDir) == "" {
		opts.ExportDir = app.DefaultExportDir
	}
	if opts.RuntimeMode == "" {
		opts.RuntimeMode = runtimeenv.ModeAuto
	}

	h := &handler{
		opts:        opts,
		ringCPUTemp: newMetricsRing(120),
		ringCPULoad: newMetricsRing(120),
		ringMemLoad: newMetricsRing(120),
		ringPower:   newMetricsRing(120),
	}
	globalQueue.startWorker(&opts)
	mux := http.NewServeMux()

	// ── Infrastructure ──────────────────────────────────────────────────────
	mux.HandleFunc("GET /healthz", h.handleHealthz)

	// ── Existing read-only endpoints (preserved for compatibility) ──────────
	mux.HandleFunc("GET /audit.json", h.handleAuditJSON)
	mux.HandleFunc("GET /runtime-health.json", h.handleRuntimeHealthJSON)
	mux.HandleFunc("GET /export/support.tar.gz", h.handleSupportBundleDownload)
	mux.HandleFunc("GET /export/file", h.handleExportFile)
	mux.HandleFunc("GET /export/", h.handleExportIndex)
	mux.HandleFunc("GET /viewer", h.handleViewer)

	// ── API ──────────────────────────────────────────────────────────────────
	// Audit
	mux.HandleFunc("POST /api/audit/run", h.handleAPIAuditRun)
	mux.HandleFunc("GET /api/audit/stream", h.handleAPIAuditStream)

	// SAT
	mux.HandleFunc("POST /api/sat/nvidia/run", h.handleAPISATRun("nvidia"))
	mux.HandleFunc("POST /api/sat/memory/run", h.handleAPISATRun("memory"))
	mux.HandleFunc("POST /api/sat/storage/run", h.handleAPISATRun("storage"))
	mux.HandleFunc("POST /api/sat/cpu/run", h.handleAPISATRun("cpu"))
	mux.HandleFunc("POST /api/sat/amd/run", h.handleAPISATRun("amd"))
	mux.HandleFunc("POST /api/sat/amd-stress/run",    h.handleAPISATRun("amd-stress"))
	mux.HandleFunc("POST /api/sat/memory-stress/run", h.handleAPISATRun("memory-stress"))
	mux.HandleFunc("POST /api/sat/sat-stress/run",    h.handleAPISATRun("sat-stress"))
	mux.HandleFunc("GET /api/sat/stream", h.handleAPISATStream)
	mux.HandleFunc("POST /api/sat/abort", h.handleAPISATAbort)

	// Tasks
	mux.HandleFunc("GET /api/tasks", h.handleAPITasksList)
	mux.HandleFunc("POST /api/tasks/cancel-all", h.handleAPITasksCancelAll)
	mux.HandleFunc("POST /api/tasks/{id}/cancel", h.handleAPITasksCancel)
	mux.HandleFunc("POST /api/tasks/{id}/priority", h.handleAPITasksPriority)
	mux.HandleFunc("GET /api/tasks/{id}/stream", h.handleAPITasksStream)

	// Services
	mux.HandleFunc("GET /api/services", h.handleAPIServicesList)
	mux.HandleFunc("POST /api/services/action", h.handleAPIServicesAction)

	// Network
	mux.HandleFunc("GET /api/network", h.handleAPINetworkStatus)
	mux.HandleFunc("POST /api/network/dhcp", h.handleAPINetworkDHCP)
	mux.HandleFunc("POST /api/network/static", h.handleAPINetworkStatic)
	mux.HandleFunc("POST /api/network/toggle", h.handleAPINetworkToggle)
	mux.HandleFunc("POST /api/network/confirm", h.handleAPINetworkConfirm)
	mux.HandleFunc("POST /api/network/rollback", h.handleAPINetworkRollback)

	// Export
	mux.HandleFunc("GET /api/export/list", h.handleAPIExportList)
	mux.HandleFunc("POST /api/export/bundle", h.handleAPIExportBundle)

	// Tools
	mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)

	// GPU presence
	mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)

	// System
	mux.HandleFunc("GET /api/system/ram-status", h.handleAPIRAMStatus)
	mux.HandleFunc("POST /api/system/install-to-ram", h.handleAPIInstallToRAM)

	// Preflight
	mux.HandleFunc("GET /api/preflight", h.handleAPIPreflight)

	// Install
	mux.HandleFunc("GET /api/install/disks", h.handleAPIInstallDisks)
	mux.HandleFunc("POST /api/install/run", h.handleAPIInstallRun)
	mux.HandleFunc("GET /api/install/stream", h.handleAPIInstallStream)

	// Metrics — SSE stream of live sensor data + server-side SVG charts
	mux.HandleFunc("GET /api/metrics/stream", h.handleAPIMetricsStream)
	mux.HandleFunc("GET /api/metrics/chart/", h.handleMetricsChartSVG)

	// Reanimator chart static assets (viewer template expects /static/*)
	mux.Handle("GET /static/", http.StripPrefix("/static/", web.Static()))

	// ── Pages ────────────────────────────────────────────────────────────────
	mux.HandleFunc("GET /", h.handlePage)

	h.mux = mux
	return mux
}

// ListenAndServe starts the HTTP server.
func ListenAndServe(addr string, opts HandlerOptions) error {
	return http.ListenAndServe(addr, NewHandler(opts))
}

// ── Infrastructure handlers ──────────────────────────────────────────────────

func (h *handler) handleHealthz(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok"))
}

// ── Compatibility endpoints ──────────────────────────────────────────────────

func (h *handler) handleAuditJSON(w http.ResponseWriter, r *http.Request) {
	data, err := loadSnapshot(h.opts.AuditPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			http.Error(w, "audit snapshot not found", http.StatusNotFound)
			return
		}
		http.Error(w, fmt.Sprintf("read audit snapshot: %v", err), http.StatusInternalServerError)
		return
	}
	// Re-apply SAT overlay on every request so that SAT results run after the
	// last audit always appear in the downloaded JSON without needing a re-audit.
	if overlaid, err := app.ApplySATOverlay(data); err == nil {
		data = overlaid
	}
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(data)
}

func (h *handler) handleRuntimeHealthJSON(w http.ResponseWriter, r *http.Request) {
	data, err := loadSnapshot(filepath.Join(h.opts.ExportDir, "runtime-health.json"))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			http.Error(w, "runtime health not found", http.StatusNotFound)
			return
		}
		http.Error(w, fmt.Sprintf("read runtime health: %v", err), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(data)
}

func (h *handler) handleSupportBundleDownload(w http.ResponseWriter, r *http.Request) {
	archive, err := app.BuildSupportBundle(h.opts.ExportDir)
	if err != nil {
		http.Error(w, fmt.Sprintf("build support bundle: %v", err), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "application/gzip")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(archive)))
	http.ServeFile(w, r, archive)
}

func (h *handler) handleExportFile(w http.ResponseWriter, r *http.Request) {
	rel := strings.TrimSpace(r.URL.Query().Get("path"))
	if rel == "" {
		http.Error(w, "path is required", http.StatusBadRequest)
		return
	}
	clean := filepath.Clean(rel)
	if clean == "." || strings.HasPrefix(clean, "..") {
		http.Error(w, "invalid path", http.StatusBadRequest)
		return
	}
	// Set Content-Type explicitly to avoid mime.TypeByExtension which panics on
	// LiveCD environments where /usr/share/mime/globs2 has an I/O read error.
	w.Header().Set("Content-Type", mimeByExt(filepath.Ext(clean)))
	http.ServeFile(w, r, filepath.Join(h.opts.ExportDir, clean))
}

// mimeByExt returns a Content-Type for known extensions, falling back to
// application/octet-stream. Used to avoid calling mime.TypeByExtension.
func mimeByExt(ext string) string {
	switch strings.ToLower(ext) {
	case ".json":
		return "application/json"
	case ".gz":
		return "application/gzip"
	case ".tar":
		return "application/x-tar"
	case ".log", ".txt":
		return "text/plain; charset=utf-8"
	case ".html":
		return "text/html; charset=utf-8"
	case ".svg":
		return "image/svg+xml"
	default:
		return "application/octet-stream"
	}
}

func (h *handler) handleExportIndex(w http.ResponseWriter, r *http.Request) {
	body, err := renderExportIndex(h.opts.ExportDir)
	if err != nil {
		http.Error(w, fmt.Sprintf("render export index: %v", err), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(body))
}

func (h *handler) handleViewer(w http.ResponseWriter, r *http.Request) {
	snapshot, _ := loadSnapshot(h.opts.AuditPath)
	body, err := viewer.RenderHTML(snapshot, h.opts.Title)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write(body)
}

func (h *handler) handleMetricsChartSVG(w http.ResponseWriter, r *http.Request) {
	path := strings.TrimPrefix(r.URL.Path, "/api/metrics/chart/")
	path = strings.TrimSuffix(path, ".svg")

	var datasets [][]float64
	var names []string
	var labels []string
	var title string
	var yMin, yMax *float64 // nil = auto; for load charts fixed 0-100

	switch {
	// ── Server sub-charts ─────────────────────────────────────────────────
	case path == "server-load":
		title = "CPU / Memory Load"
		vCPULoad, l := h.ringCPULoad.snapshot()
		vMemLoad, _ := h.ringMemLoad.snapshot()
		labels = l
		datasets = [][]float64{vCPULoad, vMemLoad}
		names = []string{"CPU Load %", "Mem Load %"}
		yMin = floatPtr(0)
		yMax = floatPtr(100)

	case path == "server-temp":
		title = "CPU Temperature"
		vCPUTemp, l := h.ringCPUTemp.snapshot()
		labels = l
		datasets = [][]float64{vCPUTemp}
		names = []string{"CPU Temp °C"}
		yMin = floatPtr(0)
		yMax = autoMax120(vCPUTemp)

	case path == "server-power":
		title = "Power & Fans"
		vPower, l := h.ringPower.snapshot()
		labels = l
		datasets = [][]float64{vPower}
		names = []string{"Power W"}
		h.ringsMu.Lock()
		for i, fr := range h.ringFans {
			fv, _ := fr.snapshot()
			datasets = append(datasets, fv)
			name := "Fan"
			if i < len(h.fanNames) {
				name = h.fanNames[i]
			}
			names = append(names, name+" RPM")
		}
		h.ringsMu.Unlock()
		yMin = floatPtr(0)
		yMax = autoMax120(datasets...)

	// ── GPU sub-charts ────────────────────────────────────────────────────
	case strings.HasPrefix(path, "gpu/"):
		rest := strings.TrimPrefix(path, "gpu/")
		// rest is either "{idx}-load", "{idx}-temp", "{idx}-power", or legacy "{idx}"
		sub := ""
		if i := strings.LastIndex(rest, "-"); i > 0 {
			sub = rest[i+1:]
			rest = rest[:i]
		}
		idx := 0
		fmt.Sscanf(rest, "%d", &idx)
		h.ringsMu.Lock()
		var gr *gpuRings
		if idx < len(h.gpuRings) {
			gr = h.gpuRings[idx]
		}
		h.ringsMu.Unlock()
		if gr == nil {
			http.NotFound(w, r)
			return
		}
		switch sub {
		case "load":
			vUtil, l := gr.Util.snapshot()
			vMemUtil, _ := gr.MemUtil.snapshot()
			labels = l
			title = fmt.Sprintf("GPU %d Load", idx)
			datasets = [][]float64{vUtil, vMemUtil}
			names = []string{"Load %", "Mem %"}
			yMin = floatPtr(0)
			yMax = floatPtr(100)
		case "temp":
			vTemp, l := gr.Temp.snapshot()
			labels = l
			title = fmt.Sprintf("GPU %d Temperature", idx)
			datasets = [][]float64{vTemp}
			names = []string{"Temp °C"}
			yMin = floatPtr(0)
			yMax = autoMax120(vTemp)
		default: // "power" or legacy (no sub)
			vPower, l := gr.Power.snapshot()
			labels = l
			title = fmt.Sprintf("GPU %d Power", idx)
			datasets = [][]float64{vPower}
			names = []string{"Power W"}
			yMin = floatPtr(0)
			yMax = autoMax120(vPower)
		}

	default:
		http.NotFound(w, r)
		return
	}

	buf, err := renderChartSVG(title, datasets, names, labels, yMin, yMax)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "image/svg+xml")
	w.Header().Set("Cache-Control", "no-store")
	_, _ = w.Write(buf)
}

// floatPtr returns a pointer to a float64 value.
func floatPtr(v float64) *float64 { return &v }

// autoMax120 returns 0→max+20% Y-axis max across all datasets.
func autoMax120(datasets ...[]float64) *float64 {
	max := 0.0
	for _, ds := range datasets {
		for _, v := range ds {
			if v > max {
				max = v
			}
		}
	}
	if max == 0 {
		return nil // let library auto-scale
	}
	v := max * 1.2
	return &v
}

// renderChartSVG renders a line chart SVG with a fixed Y-axis range.
func renderChartSVG(title string, datasets [][]float64, names []string, labels []string, yMin, yMax *float64) ([]byte, error) {
	n := len(labels)
	if n == 0 {
		n = 1
		labels = []string{""}
	}
	for i := range datasets {
		if len(datasets[i]) == 0 {
			datasets[i] = make([]float64, n)
		}
	}
	sparse := sparseLabels(labels, 6)

	opt := gocharts.NewLineChartOptionWithData(datasets)
	opt.Title = gocharts.TitleOption{Text: title}
	opt.XAxis.Labels = sparse
	opt.Legend = gocharts.LegendOption{SeriesNames: names}
	if yMin != nil || yMax != nil {
		opt.YAxis = []gocharts.YAxisOption{{Min: yMin, Max: yMax}}
	}

	p := gocharts.NewPainter(gocharts.PainterOptions{
		OutputFormat: gocharts.ChartOutputSVG,
		Width:        1400,
		Height:       240,
	}, gocharts.PainterThemeOption(gocharts.GetTheme("grafana")))
	if err := p.LineChart(opt); err != nil {
		return nil, err
	}
	return p.Bytes()
}

func safeIdx(s []float64, i int) float64 {
	if i < len(s) {
		return s[i]
	}
	return 0
}

func sparseLabels(labels []string, n int) []string {
	out := make([]string, len(labels))
	step := len(labels) / n
	if step < 1 {
		step = 1
	}
	for i, l := range labels {
		if i%step == 0 {
			out[i] = l
		}
	}
	return out
}

// ── Page handler ─────────────────────────────────────────────────────────────

func (h *handler) handlePage(w http.ResponseWriter, r *http.Request) {
	page := strings.TrimPrefix(r.URL.Path, "/")
	if page == "" {
		page = "dashboard"
	}
	// Redirect old routes to new names
	switch page {
	case "tests":
		http.Redirect(w, r, "/validate", http.StatusMovedPermanently)
		return
	case "burn-in":
		http.Redirect(w, r, "/burn", http.StatusMovedPermanently)
		return
	}
	body := renderPage(page, h.opts)
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(body))
}

// ── Helpers ──────────────────────────────────────────────────────────────────

func loadSnapshot(path string) ([]byte, error) {
	if strings.TrimSpace(path) == "" {
		return nil, os.ErrNotExist
	}
	return os.ReadFile(path)
}

// writeJSON sends v as JSON with status 200.
func writeJSON(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	_ = json.NewEncoder(w).Encode(v)
}

// writeError sends a JSON error response.
func writeError(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
