package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"github.com/simonvetter/modbus"
	"k8s.io/klog"
)

// daemon is the main service of the succdaemon.
type daemon struct {
	modbusClient *modbus.ModbusClient
	// adcPirani is the adc implementation returning the voltage of the Pfeiffer
	// Pirani gauge.
	adcPirani adc

	load atomic.Int64

	// mu guards the state below.
	mu sync.RWMutex
	daemonState
}

// daemonState contains all the state of the daemon. A copy of it can be
// requested for consumers, eg. the web view.
type daemonState struct {
	safety struct {
		// failsafe mode is enabled when the pirani gauge appears to be
		// disconnected, and is disabled only when an atmosphere is read.
		failsafe bool
		// highPressure mode is enabled when the pressure reading is above 1e-1
		// mbar, locking out the diffusion pump from being enabled.
		highPressure bool
	}

	piraniVolts100 ringbufferInput
	piraniMbar100  pfeifferVoltsToMbar
	piraniVolts3   ringbufferInput
	piraniMbar3    pfeifferVoltsToMbar

	rpOn bool
	dpOn bool

	vent       momentaryOutput
	pumpdown   momentaryOutput
	aboveRough thresholdOutput
	aboveHigh  thresholdOutput

	tempDPBottom float32
	tempDPTop    float32
	tempDPInlet  float32

	tempSEM     float32
	humiditySEM float32
}

func (d *daemonState) vacuumStatus() (rough, high bool) {
	rough = !d.aboveRough.output
	high = !d.aboveHigh.output
	return
}

// process runs the pain acquisition and control loop of succd.
func (d *daemon) process(ctx context.Context) {
	var lastRun time.Time
	hz := 100
	period := time.Second / time.Duration(hz)
	// Extra grace period for GC pauses and other non-realtime system jitter.
	periodGrace := period + time.Millisecond*5

	ticker := time.NewTicker(period)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			now := time.Now()
			if elapsed := now.Sub(lastRun); !lastRun.IsZero() && elapsed > periodGrace {
				klog.Warningf("Processing loop lag: took %s, want %s", elapsed, period)
			}
			lastRun = now
			if err := d.processOnce(ctx); err != nil {
				if errors.Is(err, ctx.Err()) {
					return
				} else {
					klog.Errorf("Processing error: %v", err)
					time.Sleep(time.Second * 10)
				}
			}
			runtime := time.Since(lastRun)
			load := int64(100 * runtime / period)
			d.load.Store(load)
		case <-ctx.Done():
			return
		}
	}
}

// processOnce runs the main loop step of succd.
func (d *daemon) processOnce(_ context.Context) error {
	v, err := d.adcPirani.Read()
	if err != nil {
		return fmt.Errorf("when reading ADC: %w", err)
	}
	d.mu.Lock()
	defer d.mu.Unlock()

	// Process pirani ringbuffers.
	d.piraniVolts3.process(v)
	d.piraniMbar3.process(d.piraniVolts3.avg)
	d.piraniVolts100.process(v)
	d.piraniMbar100.process(d.piraniVolts100.avg)

	d.pumpdown.process()
	d.vent.process()

	// Run safety checks based on small ringbuffer.
	if d.piraniVolts3.saturated() {
		mbar := d.piraniMbar3.mbar
		if !d.safety.failsafe && mbar < 4e-6 {
			// Unrealistic result, Pirani probe probably disconnected. Failsafe mode.
			d.safety.failsafe = true
			klog.Errorf("SAFETY: Pirani probe seems disconnected; enabling failsafe mode")
		}
		if d.safety.failsafe && mbar > 1e2 {
			d.safety.failsafe = false
			klog.Infof("SAFETY: Pirani probe value (%s) is plausible again; quitting failsafe mode", formatMbar(mbar))
		}

		if !d.safety.highPressure && mbar >= 1e-1 {
			d.safety.highPressure = true
			klog.Warningf("SAFETY: Pressure is too high (%s mbar); enabling diffusion pump lockout", formatMbar(mbar))
		}
		if d.safety.highPressure && mbar < (1e-1)-(1e-2) {
			d.safety.highPressure = false
			klog.Infof("SAFETY: Pressure is low enough (%s mbar) for diffusion pump operation; quitting diffusion pump lockout", formatMbar(mbar))
		}
	} else {
		d.safety.highPressure = true
	}

	// Control threhold/feedback values based on main pirani ringbuffer, failing
	// safe if not enough data is present.
	if d.piraniVolts100.saturated() {
		mbar := d.piraniMbar100.mbar
		d.aboveRough.process(float64(mbar))
		d.aboveHigh.process(float64(mbar))
	} else {
		d.aboveRough.output = true
		d.aboveHigh.output = true
	}

	// Apply safety overrides.
	if d.safety.failsafe {
		d.aboveRough.output = true
		d.aboveHigh.output = true
		d.dpOn = false
	}
	if d.safety.highPressure {
		d.dpOn = false
	}

	return nil
}
