package aoiprxkit

import (
	"bufio"
	"context"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"strings"
	"time"
)

type MeterServer struct {
	meter *LiveMeter
	srv   *http.Server
}

func NewMeterServer(listenAddress string, meter *LiveMeter) *MeterServer {
	if meter == nil {
		meter = NewLiveMeter()
	}
	ms := &MeterServer{meter: meter}
	mux := http.NewServeMux()
	mux.HandleFunc("/", ms.handleIndex)
	mux.HandleFunc("/healthz", ms.handleHealth)
	mux.HandleFunc("/api/meter", ms.handleSnapshot)
	mux.HandleFunc("/ws/live", ms.handleWS)
	ms.srv = &http.Server{
		Addr:              listenAddress,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
	}
	return ms
}

func (m *MeterServer) Meter() *LiveMeter { return m.meter }

func (m *MeterServer) Start() error {
	go func() {
		_ = m.srv.ListenAndServe()
	}()
	return nil
}

func (m *MeterServer) Shutdown(ctx context.Context) error {
	return m.srv.Shutdown(ctx)
}

func (m *MeterServer) handleHealth(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}

func (m *MeterServer) handleSnapshot(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(m.meter.Snapshot())
}

func (m *MeterServer) handleIndex(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = io.WriteString(w, meterIndexHTML)
}

func (m *MeterServer) handleWS(w http.ResponseWriter, r *http.Request) {
	if !headerContainsToken(r.Header, "Connection", "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
		http.Error(w, "upgrade required", http.StatusUpgradeRequired)
		return
	}
	key := strings.TrimSpace(r.Header.Get("Sec-WebSocket-Key"))
	if key == "" {
		http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
		return
	}
	hj, ok := w.(http.Hijacker)
	if !ok {
		http.Error(w, "hijacking not supported", http.StatusInternalServerError)
		return
	}
	conn, rw, err := hj.Hijack()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	accept := computeWebSocketAccept(key)
	_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
	_, _ = rw.WriteString("Upgrade: websocket\r\n")
	_, _ = rw.WriteString("Connection: Upgrade\r\n")
	_, _ = rw.WriteString("Sec-WebSocket-Accept: " + accept + "\r\n\r\n")
	_ = rw.Flush()

	ch, unsubscribe := m.meter.Subscribe()
	defer unsubscribe()
	defer conn.Close()

	_ = conn.SetDeadline(time.Time{})
	for snap := range ch {
		payload, err := json.Marshal(snap)
		if err != nil {
			return
		}
		if err := writeWebSocketTextFrame(conn, payload); err != nil {
			return
		}
	}
}

func headerContainsToken(h http.Header, key, token string) bool {
	for _, v := range h.Values(key) {
		parts := strings.Split(v, ",")
		for _, part := range parts {
			if strings.EqualFold(strings.TrimSpace(part), token) {
				return true
			}
		}
	}
	return false
}

func computeWebSocketAccept(key string) string {
	const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
	sum := sha1.Sum([]byte(key + magic))
	return base64.StdEncoding.EncodeToString(sum[:])
}

func writeWebSocketTextFrame(conn net.Conn, payload []byte) error {
	bw := bufio.NewWriter(conn)
	header := []byte{0x81}
	switch {
	case len(payload) < 126:
		header = append(header, byte(len(payload)))
	case len(payload) <= 65535:
		header = append(header, 126, byte(len(payload)>>8), byte(len(payload)))
	default:
		header = append(header, 127,
			byte(uint64(len(payload))>>56), byte(uint64(len(payload))>>48), byte(uint64(len(payload))>>40), byte(uint64(len(payload))>>32),
			byte(uint64(len(payload))>>24), byte(uint64(len(payload))>>16), byte(uint64(len(payload))>>8), byte(uint64(len(payload))),
		)
	}
	if _, err := bw.Write(header); err != nil {
		return err
	}
	if _, err := bw.Write(payload); err != nil {
		return err
	}
	return bw.Flush()
}

const meterIndexHTML = `<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>aoiprxkit meter</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 20px; background: #111; color: #eee; }
    .meta { margin-bottom: 16px; color: #bbb; }
    .row { margin: 12px 0; }
    .label { margin-bottom: 4px; }
    .bar { width: 100%; height: 22px; background: #222; border-radius: 6px; overflow: hidden; }
    .fill { height: 100%; background: linear-gradient(90deg, #2ecc71, #f1c40f, #e74c3c); width: 0%; }
    .nums { font-size: 12px; color: #bbb; margin-top: 4px; }
  </style>
</head>
<body>
  <h1>aoiprxkit live meter</h1>
  <div id="meta" class="meta">waiting for frames…</div>
  <div id="meters"></div>
  <script>
    const meta = document.getElementById('meta');
    const root = document.getElementById('meters');
    const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/live');
    ws.onmessage = (ev) => {
      const snap = JSON.parse(ev.data);
      meta.textContent = (snap.source || 'unknown') + ' · ' + snap.sampleRateHz + ' Hz · ' + snap.channels + ' ch · ' + snap.updatedAt;
      root.innerHTML = '';
      (snap.meters || []).forEach((m, idx) => {
        const row = document.createElement('div');
        row.className = 'row';
        const label = document.createElement('div');
        label.className = 'label';
        label.textContent = 'Channel ' + (idx + 1);
        const bar = document.createElement('div');
        bar.className = 'bar';
        const fill = document.createElement('div');
        fill.className = 'fill';
        fill.style.width = Math.max(0, Math.min(100, m.peak * 100)).toFixed(1) + '%';
        bar.appendChild(fill);
        const nums = document.createElement('div');
        nums.className = 'nums';
        nums.textContent = 'RMS ' + m.rms.toFixed(4) + ' · Peak ' + m.peak.toFixed(4) + ' · Latest ' + m.latest.toFixed(4);
        row.appendChild(label);
        row.appendChild(bar);
        row.appendChild(nums);
        root.appendChild(row);
      });
    };
  </script>
</body>
</html>`
