package main

import (
	"embed"
	"fmt"
	"html/template"
	"net/http"
	_ "net/http/pprof"
	"os"
	"strings"
	"time"

	"github.com/coder/websocket"
	"github.com/coder/websocket/wsjson"
	"k8s.io/klog"
)

type webServer struct {
	d daemonController
}

var (
	//go:embed index.html
	templateIndexText string
	templateIndex     = template.Must(template.New("index").Parse(templateIndexText))
	//go:embed succd.png
	favicon []byte
)

func (s *webServer) viewFavicon(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/png")
	w.Write(favicon)
}

func formatVolts(v float32) string {
	return fmt.Sprintf("%.4f V", v)
}

// apiData is the data model served to the user via HTTP/WebSockets
type apiData struct {
	// Safety interlocks.
	Safety struct {
		// Failsafe mode enabled - pirani gauge seems disconnected.
		Failsafe bool
		// HighPressure interlock enabled - pressure too high to run diffusion
		// pump.
		HighPressure bool
	}
	// Pirani gauge data.
	Pirani struct {
		// Volts read from the gauge (0-10).
		Volts string
		// Mbar read from the gauge, formatted as HTML.
		Mbar template.HTML
		// MbarFloat read from the gauge.
		MbarFloat float32
	}
	// Pump state.
	Pumps struct {
		// RPOn means the roughing pump is turned on.
		RPOn bool
		// DPOn means the diffusion pump is turned on.
		DPOn bool
	}
	// Temperature state.
	Temperatures struct {
		DPBottom float32
		DPTop    float32
		DPInlet  float32
		SEM      float32
	}
	// Humidity state.
	Humidity struct {
		SEM float32
	}
	// Pressure feedback into evacuation board.
	Feedback struct {
		// RoughReached is true when the system has reached a rough vacuum
		// stage.
		RoughReached bool
		// HighReached is true when the system has reached a high vacuum stage.
		HighReached bool
	}
	// LoopLoad is a percentage expressing how busy the processing loop is; 100
	// is full utilization; >100 is lag.
	LoopLoad int64
	// System junk.
	System struct {
		// Load of the system.
		Load string
		// Hostname of the system.
		Hostname string
	}
}

// apiData returns the user data model for the current state of the system. If
// skipSystem is set, the System subset is ignored (saves system load, and is
// not being served via websockets).
func (s *webServer) apiData(skipSystem bool) *apiData {
	state := s.d.snapshot()
	volts := state.piraniVolts100.avg
	mbar := state.piraniMbar100.mbar
	rough, high := state.vacuumStatus()

	var hostname, load string
	var err error

	if !skipSystem {
		hostname, err = os.Hostname()
		if err != nil {
			hostname = "unknown"
		}
		loadB, err := os.ReadFile("/proc/loadavg")
		load = "unknown"
		if err == nil {
			parts := strings.Fields(string(loadB))
			load = strings.Join(parts[:3], " ")
		}
	}

	ad := apiData{}
	ad.Safety.Failsafe = state.safety.failsafe
	ad.Safety.HighPressure = state.safety.highPressure
	ad.Pirani.Volts = formatVolts(volts)
	ad.Pirani.Mbar = formatMbarHTML(mbar)
	ad.Pirani.MbarFloat = mbar
	ad.Pumps.RPOn = state.rpOn
	ad.Pumps.DPOn = state.dpOn
	ad.Temperatures.DPBottom = state.tempDPBottom
	ad.Temperatures.DPTop = state.tempDPTop
	ad.Temperatures.DPInlet = state.tempDPInlet
	ad.Temperatures.SEM = state.tempSEM
	ad.Humidity.SEM = state.humiditySEM
	ad.Feedback.RoughReached = rough
	ad.Feedback.HighReached = high
	ad.LoopLoad = s.d.loopLoad()
	ad.System.Load = load
	ad.System.Hostname = hostname
	return &ad
}

// httpIndex is the / view.
func (s *webServer) viewIndex(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	data := s.apiData(false)
	//data.Pirani.Mbar = html.St
	templateIndex.Execute(w, data)
}

// httpStream is the websocket clientwards data hose, returning a 10Hz update
// stream of pressure/voltage.
func (s *webServer) viewStream(w http.ResponseWriter, r *http.Request) {
	c, err := websocket.Accept(w, r, nil)
	if err != nil {
		return
	}
	defer c.CloseNow()

	t := time.NewTicker(time.Second / 10)
	defer t.Stop()

	ctx := c.CloseRead(r.Context())
	for {
		select {
		case <-ctx.Done():
			c.Close(websocket.StatusNormalClosure, "")
			return
		case <-t.C:
			v := s.apiData(true)
			if err := wsjson.Write(ctx, c, v); err != nil {
				klog.Errorf("Websocket write failed: %v", err)
				return
			}
		}
	}
}

func boolToFloat(b bool) float32 {
	if b {
		return 1.0
	} else {
		return 0.0
	}
}

// httpMetrics serves minimalistic Prometheus-compatible metrics.
func (s *webServer) viewMetrics(w http.ResponseWriter, r *http.Request) {
	// TODO(q3k): also serve Go stuff using the actual Prometheus metrics client
	// library.
	// TODO(q3k): serve the rest of the data model
	state := s.d.snapshot()

	// sem_pressure_mbar is meant to represent the fused pressure value
	// from all data sources once we have more vacuum sensors in the
	// system.  sem_pirani_mbar is just the reading from the pirani gauge.
	fmt.Fprintf(w, "# HELP sem_pressure_mbar Pressure in the SEM chamber, in millibar\n")
	fmt.Fprintf(w, "# TYPE sem_pressure_mbar gauge\n")
	fmt.Fprintf(w, "sem_pressure_mbar %f\n", state.piraniMbar100.mbar)

	fmt.Fprintf(w, "# HELP sem_pirani_mbar Pressure reading by the Pirani gauge, in millibar\n")
	fmt.Fprintf(w, "# TYPE sem_pirani_mbar gauge\n")
	fmt.Fprintf(w, "sem_pirani_mbar %f\n", state.piraniMbar100.mbar)

	fmt.Fprintf(w, "# HELP sem_pirani_volts Voltage output from the Pirani gauge, in volts\n")
	fmt.Fprintf(w, "# TYPE sem_pirani_volts gauge\n")
	fmt.Fprintf(w, "sem_pirani_volts %f\n", state.piraniVolts100.avg)

	fmt.Fprintf(w, "# HELP sem_pirani_failsafe_active Whether pirani gauge failsafe mode is active (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_pirani_failsafe_active gauge\n")
	fmt.Fprintf(w, "sem_pirani_failsafe_active %f\n", boolToFloat(state.safety.failsafe))

	fmt.Fprintf(w, "# HELP sem_dp_lockout_active Whether diffusion pump lockout is active (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_dp_lockout_active gauge\n")
	fmt.Fprintf(w, "sem_dp_lockout_active %f\n", boolToFloat(state.safety.highPressure))

	fmt.Fprintf(w, "# HELP sem_pump_diffusion_running Whether the diffusion pump is running (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_pump_diffusion_running gauge\n")
	fmt.Fprintf(w, "sem_pump_diffusion_running %f\n", boolToFloat(state.dpOn))

	fmt.Fprintf(w, "# HELP sem_pump_roughing_running Whether the roughing pump is running (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_pump_roughing_running gauge\n")
	fmt.Fprintf(w, "sem_pump_roughing_running %f\n", boolToFloat(state.rpOn))

	rough, high := state.vacuumStatus()
	fmt.Fprintf(w, "# HELP sem_vacuum_rough_reached Whether a rough vacuum has been reached (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_vacuum_rough_reached gauge\n")
	fmt.Fprintf(w, "sem_vacuum_rough_reached %f\n", boolToFloat(rough))

	fmt.Fprintf(w, "# HELP sem_vacuum_high_reached Whether a high vacuum has been reached (boolean)\n")
	fmt.Fprintf(w, "# TYPE sem_vacuum_high_reached gauge\n")
	fmt.Fprintf(w, "sem_vacuum_high_reached %f\n", boolToFloat(high))

	fmt.Fprintf(w, "# HELP sem_environment_temperature_celsius Environmental temperature of the SEM, in degrees celsius\n")
	fmt.Fprintf(w, "# TYPE sem_environment_temperature_celsius gauge\n")
	fmt.Fprintf(w, "sem_environment_temperature_celsius %f\n", state.tempSEM)

	fmt.Fprintf(w, "# HELP sem_environment_humidity_percent Environmental relative humidity of the SEM, in percent\n")
	fmt.Fprintf(w, "# TYPE sem_environment_humidity_percent gauge\n")
	fmt.Fprintf(w, "sem_environment_humidity_percent %f\n", state.humiditySEM)

	fmt.Fprintf(w, "# HELP sem_dp_bottom_temperature_celsius Temperature of the DP bottom, in degrees celsius\n")
	fmt.Fprintf(w, "# TYPE sem_dp_bottom_temperature_celsius gauge\n")
	fmt.Fprintf(w, "sem_dp_bottom_temperature_celsius %f\n", state.tempDPBottom)

	fmt.Fprintf(w, "# HELP sem_dp_top_temperature_celsius Temperature of the DP top, in degrees celsius\n")
	fmt.Fprintf(w, "# TYPE sem_dp_top_temperature_celsius gauge\n")
	fmt.Fprintf(w, "sem_dp_top_temperature_celsius %f\n", state.tempDPTop)

	fmt.Fprintf(w, "# HELP sem_dp_inlet_temperature_celsius Temperature of the DP inlet flange, in degrees celsius\n")
	fmt.Fprintf(w, "# TYPE sem_dp_inlet_temperature_celsius gauge\n")
	fmt.Fprintf(w, "sem_dp_inlet_temperature_celsius %f\n", state.tempDPInlet)
}

func (s *webServer) viewRoughingPumpEnable(w http.ResponseWriter, r *http.Request) {
	s.d.rpSet(true)
	fmt.Fprintf(w, "succ on\n")
}

func (s *webServer) viewRoughingPumpDisable(w http.ResponseWriter, r *http.Request) {
	s.d.rpSet(false)
	fmt.Fprintf(w, "succ off\n")
}

func (s *webServer) viewDiffusionPumpEnable(w http.ResponseWriter, r *http.Request) {
	s.d.dpSet(true)
	fmt.Fprintf(w, "deep succ on\n")
}

func (s *webServer) viewDiffusionPumpDisable(w http.ResponseWriter, r *http.Request) {
	s.d.dpSet(false)
	fmt.Fprintf(w, "deep succ off\n")
}

func (s *webServer) viewButtonPumpDown(w http.ResponseWriter, r *http.Request) {
	s.d.pumpDownPress()
}

func (s *webServer) viewButtonVent(w http.ResponseWriter, r *http.Request) {
	s.d.ventPress()
}

//go:embed static/*
var static embed.FS

func (s *webServer) setupViews() {
	http.HandleFunc("/", s.viewIndex)
	http.HandleFunc("/favicon.png", s.viewFavicon)
	http.Handle("/static/", http.FileServer(http.FS(static)))
	http.HandleFunc("/stream", s.viewStream)
	http.HandleFunc("/metrics", s.viewMetrics)
	http.HandleFunc("/button/vent", s.viewButtonVent)
	http.HandleFunc("/button/pumpdown", s.viewButtonPumpDown)
	http.HandleFunc("/rp/on", s.viewRoughingPumpEnable)
	http.HandleFunc("/rp/off", s.viewRoughingPumpDisable)
	http.HandleFunc("/dp/on", s.viewDiffusionPumpEnable)
	http.HandleFunc("/dp/off", s.viewDiffusionPumpDisable)
}
