package controllers

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"

	"git.joco.dk/snr/fermentord/internal/configuration"
	"git.joco.dk/snr/fermentord/pkg/temperature"
	"github.com/getsentry/sentry-go"
)

type ChamberState int

const (
	ChamberStateIdle ChamberState = iota
	ChamberStateCooling
	ChamberStateHeating
)

var ChamberStateMap = map[ChamberState]string{
	ChamberStateIdle:    "IDLE",
	ChamberStateCooling: "COOLING",
	ChamberStateHeating: "HEATING",
}

type ChamberController struct {
	ConfigUpdates chan configuration.Configuration
	config        configuration.Configuration

	// Current state.
	chamberState           ChamberState
	lastChamberStateChange time.Time
	lastCoolerStateChange  time.Time
	C                      chan ChamberState
	isPaused               bool

	// Current temperature readings.
	ambientTemperature    float64
	chamberTemperature    float64
	wortTemperature       float64
	lastTemperatureUpdate time.Time

	chTemp  chan temperature.TemperatureReading
	chPause chan bool

	pid *PIDController
	hub *sentry.Hub
}

func NewChamberController(config configuration.Configuration) *ChamberController {
	return &ChamberController{
		C:             make(chan ChamberState),
		config:        config,
		pid:           NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd),
		chTemp:        make(chan temperature.TemperatureReading),
		chPause:       make(chan bool, 1),
		chamberState:  ChamberStateIdle,
		ConfigUpdates: make(chan configuration.Configuration, 1),
		hub:           sentry.CurrentHub().Clone(),
	}
}

func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	defer p.hub.Flush(10 * time.Second)

	ticker := time.NewTicker(1 * time.Second)

	for {
		select {
		case <-ticker.C:
			state := p.computeChamberState()
			p.setChamberState(state)

		case t := <-p.chTemp:
			p.ambientTemperature = t.Ambient
			p.chamberTemperature = t.Chamber
			p.wortTemperature = t.Wort
			p.lastTemperatureUpdate = time.Now()

		case pause := <-p.chPause:
			p.setPause(pause)

		case c := <-p.ConfigUpdates:
			p.config = c
			log.Printf("Fermentation temperature set to %v", p.config.FermentationTemperature)

		case <-ctx.Done():
			ticker.Stop()
			p.isPaused = false

			return
		}
	}
}

func (p *ChamberController) SetTemperature(t temperature.TemperatureReading) {
	p.chTemp <- t
}

// Pause is a thread-safe way to pause the controller.
func (p *ChamberController) Pause() {
	p.chPause <- true
}

// Resume is a thread-safe way to resume the controller.
func (p *ChamberController) Resume() {
	p.chPause <- false
}

func (p *ChamberController) setPause(pause bool) {
	if pause {
		p.setChamberState(ChamberStateIdle)
		p.isPaused = true
	} else {
		p.isPaused = false
		state := p.computeChamberState()
		p.setChamberState(state)
	}
}

func (p *ChamberController) setChamberState(state ChamberState) {
	if state == p.chamberState {
		return
	}

	if p.isPaused {
		return
	}

	//log.Printf("State changed from %v to %v", p.chamberState, state)

	if p.chamberState == ChamberStateCooling || state == ChamberStateCooling {
		p.lastCoolerStateChange = time.Now()
	}

	p.chamberState = state
	p.lastChamberStateChange = time.Now()
	p.C <- state
}

func (p *ChamberController) computeChamberState() ChamberState {
	//offset := p.pid.Compute(p.chamberTemperature, p.config.FermentationTemperature)
	//chamberTargetTemp := p.config.FermentationTemperature + offset

	// Stay in idle mode until first temperature update is received
	if p.lastTemperatureUpdate.IsZero() {
		return ChamberStateIdle
	}

	runtimeSecs := time.Since(p.lastChamberStateChange).Seconds()

	if p.chamberState == ChamberStateCooling {
		// Ensure compressor min. runtime
		if runtimeSecs < p.config.Limits.MinCoolerRuntimeSecs {
			return p.chamberState
		}

		// Limit compressor runtime
		if runtimeSecs > p.config.Limits.MaxCoolerRuntimeSecs {
			return ChamberStateIdle
		}

		// Limit chamber min. temp.
		if p.chamberTemperature < p.config.Limits.MinChamberTemperature {
			return ChamberStateIdle
		}

		// Limit chamber max. temp.
		if p.chamberTemperature > p.config.Limits.MaxChamberTemperature {
			return ChamberStateIdle
		}
	}

	var next ChamberState
	heater := p.config.HeaterEnabled && p.chamberTemperature < p.config.FermentationTemperature-p.config.DeltaTemperatureHeat //&&
	//p.chamberTemperature < chamberTargetTemp-p.config.DeltaTemperatureHeat
	cooler := p.config.CoolerEnabled && p.chamberTemperature > p.config.FermentationTemperature+p.config.DeltaTemperatureCool //&&
	//p.chamberTemperature > chamberTargetTemp+p.config.DeltaTemperatureCool

	if cooler && heater {
		// This should NOT happen!
		err := fmt.Errorf("heater and cooler activated at same time")
		p.hub.CaptureException(err)
		log.Print(err)
		next = ChamberStateIdle
	} else if !cooler && !heater {
		next = ChamberStateIdle
	} else if cooler {
		next = ChamberStateCooling
	} else if heater {
		next = ChamberStateHeating
	}

	// Ensure compressor cooldown
	if p.chamberState != ChamberStateCooling && next == ChamberStateCooling &&
		time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.MinCoolerCooldownSecs {
		return ChamberStateIdle
	}

	// Ensure that heater wont run just after cooling cycle
	if p.chamberState != ChamberStateHeating && next == ChamberStateHeating && time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.HeaterGraceTimeSecs {
		return ChamberStateIdle
	}

	// Force idle mode when temperature updates are missing
	if time.Since(p.lastTemperatureUpdate) > 1*time.Minute {
		err := fmt.Errorf("no temperature update on controller for 1 minute - setting IDLE mode")
		p.hub.CaptureException(err)
		log.Print(err)
		next = ChamberStateIdle
	}

	return next
}
