package segments

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/http"
	"sort"
	"time"

	"github.com/jandedobbeleer/oh-my-posh/src/platform"
	"github.com/jandedobbeleer/oh-my-posh/src/properties"
)

// segment struct, makes templating easier
type Brewfather struct {
	props properties.Properties
	env   platform.Environment

	Batch
	TemperatureTrendIcon string
	StatusIcon           string
	DayIcon              string // populated from day_icon for use in template

	ReadingAge             int // age in hours of the most recent reading included in the batch, -1 if none
	DaysFermenting         uint
	DaysBottled            uint
	DaysBottledOrFermented *uint // help avoid chronic template logic - code will point this to one of above or be nil depending on status

	URL string // URL of batch page to open if hyperlink enabled on the segment and URL formatting used in template: «text»(link)
}

const (
	BFUserID  properties.Property = "user_id"
	BFAPIKey  properties.Property = "api_key"
	BFBatchID properties.Property = "batch_id"

	BFDoubleUpIcon      properties.Property = "doubleup_icon"
	BFSingleUpIcon      properties.Property = "singleup_icon"
	BFFortyFiveUpIcon   properties.Property = "fortyfiveup_icon"
	BFFlatIcon          properties.Property = "flat_icon"
	BFFortyFiveDownIcon properties.Property = "fortyfivedown_icon"
	BFSingleDownIcon    properties.Property = "singledown_icon"
	BFDoubleDownIcon    properties.Property = "doubledown_icon"

	BFPlanningStatusIcon     properties.Property = "planning_status_icon"
	BFBrewingStatusIcon      properties.Property = "brewing_status_icon"
	BFFermentingStatusIcon   properties.Property = "fermenting_status_icon"
	BFConditioningStatusIcon properties.Property = "conditioning_status_icon"
	BFCompletedStatusIcon    properties.Property = "completed_status_icon"
	BFArchivedStatusIcon     properties.Property = "archived_status_icon"

	BFDayIcon properties.Property = "day_icon"

	BFCacheTimeout properties.Property = "cache_timeout"

	BFStatusPlanning     string = "Planning"
	BFStatusBrewing      string = "Brewing"
	BFStatusFermenting   string = "Fermenting"
	BFStatusConditioning string = "Conditioning"
	BFStatusCompleted    string = "Completed"
	BFStatusArchived     string = "Archived"
)

// Returned from https://api.brewfather.app/v1/batches/batch_id/readings
type BatchReading struct {
	Comment     string  `json:"comment"`
	Gravity     float64 `json:"sg"`
	DeviceType  string  `json:"type"`
	DeviceID    string  `json:"id"`
	Temperature float64 `json:"temp"`      // celsius - need to add F conversion
	Timepoint   int64   `json:"timepoint"` // << check what these are...
	Time        int64   `json:"time"`      // <<
}
type Batch struct {
	// Json tagged values returned from https://api.brewfather.app/v1/batches/batch_id
	Status      string `json:"status"`
	BatchName   string `json:"name"`
	BatchNumber int    `json:"batchNo"`
	Recipe      struct {
		Name string `json:"name"`
	} `json:"recipe"`
	BrewDate         int64 `json:"brewDate"`
	FermentStartDate int64 `json:"fermentationStartDate"`
	BottlingDate     int64 `json:"bottlingDate"`

	MeasuredOg  float64 `json:"measuredOg"`
	MeasuredFg  float64 `json:"measuredFg"`
	MeasuredAbv float64 `json:"measuredAbv"`

	// copy of the latest BatchReading in here.
	Reading *BatchReading

	// Calculated values we need to cache because they require the rest query to reproduce
	TemperatureTrend float64 // diff between this and last, short term trend
}

func (bf *Brewfather) Template() string {
	return " {{ .StatusIcon }} {{ if .DaysBottledOrFermented }}{{ .DaysBottledOrFermented }}{{ .DayIcon }} {{ end }}{{ url .Recipe.Name .URL }} {{ printf \"%.1f\" .MeasuredAbv }}%{{ if and (.Reading) (eq .Status \"Fermenting\") }} {{ printf \"%.3f\" .Reading.Gravity }} {{ .Reading.Temperature }}\u00b0 {{ .TemperatureTrendIcon }}{{ end }} " //nolint:lll
}

func (bf *Brewfather) Enabled() bool {
	data, err := bf.getResult()
	if err != nil {
		return false
	}
	bf.Batch = *data

	if bf.Batch.Reading != nil {
		readingDate := time.UnixMilli(bf.Batch.Reading.Time)
		bf.ReadingAge = int(time.Since(readingDate).Hours())
	} else {
		bf.ReadingAge = -1
	}

	bf.TemperatureTrendIcon = bf.getTrendIcon(bf.TemperatureTrend)
	bf.StatusIcon = bf.getBatchStatusIcon(data.Status)

	fermStartDate := time.UnixMilli(bf.Batch.FermentStartDate)
	bottlingDate := time.UnixMilli(bf.Batch.BottlingDate)

	switch bf.Batch.Status {
	case BFStatusFermenting:
		// in the fermenter now, so relative to today.
		bf.DaysFermenting = uint(time.Since(fermStartDate).Hours() / 24)
		bf.DaysBottled = 0
		bf.DaysBottledOrFermented = &bf.DaysFermenting
	case BFStatusConditioning, BFStatusCompleted, BFStatusArchived:
		bf.DaysFermenting = uint(bottlingDate.Sub(fermStartDate).Hours() / 24)
		bf.DaysBottled = uint(time.Since(bottlingDate).Hours() / 24)
		bf.DaysBottledOrFermented = &bf.DaysBottled
	default:
		bf.DaysFermenting = 0
		bf.DaysBottled = 0
		bf.DaysBottledOrFermented = nil
	}

	// URL property set to weblink to the full batch page
	batchID := bf.props.GetString(BFBatchID, "")
	if len(batchID) > 0 {
		bf.URL = fmt.Sprintf("https://web.brewfather.app/tabs/batches/batch/%s", batchID)
	}

	bf.DayIcon = bf.props.GetString(BFDayIcon, "d")

	return true
}

func (bf *Brewfather) getTrendIcon(trend float64) string {
	// Not a fan of this logic - wondering if Go lets us do something cleaner...
	if trend >= 0 {
		if trend > 4 {
			return bf.props.GetString(BFDoubleUpIcon, "↑↑")
		}

		if trend > 2 {
			return bf.props.GetString(BFSingleUpIcon, "↑")
		}

		if trend > 0.5 {
			return bf.props.GetString(BFFortyFiveUpIcon, "↗")
		}

		return bf.props.GetString(BFFlatIcon, "→")
	}

	if trend < -4 {
		return bf.props.GetString(BFDoubleDownIcon, "↓↓")
	}

	if trend < -2 {
		return bf.props.GetString(BFSingleDownIcon, "↓")
	}

	if trend < -0.5 {
		return bf.props.GetString(BFFortyFiveDownIcon, "↘")
	}

	return bf.props.GetString(BFFlatIcon, "→")
}

func (bf *Brewfather) getBatchStatusIcon(batchStatus string) string {
	switch batchStatus {
	case BFStatusPlanning:
		return bf.props.GetString(BFPlanningStatusIcon, "\uF8EA")
	case BFStatusBrewing:
		return bf.props.GetString(BFBrewingStatusIcon, "\uF7DE")
	case BFStatusFermenting:
		return bf.props.GetString(BFFermentingStatusIcon, "\uF499")
	case BFStatusConditioning:
		return bf.props.GetString(BFConditioningStatusIcon, "\uE372")
	case BFStatusCompleted:
		return bf.props.GetString(BFCompletedStatusIcon, "\uF7A5")
	case BFStatusArchived:
		return bf.props.GetString(BFArchivedStatusIcon, "\uF187")
	default:
		return ""
	}
}

func (bf *Brewfather) getResult() (*Batch, error) {
	getFromCache := func(key string) (*Batch, error) {
		val, found := bf.env.Cache().Get(key)
		// we got something from the cache
		if found {
			var result Batch
			err := json.Unmarshal([]byte(val), &result)
			if err == nil {
				return &result, nil
			}
		}
		return nil, errors.New("no data in cache")
	}

	putToCache := func(key string, batch *Batch, cacheTimeout int) error {
		cacheJSON, err := json.Marshal(batch)
		if err != nil {
			return err
		}

		bf.env.Cache().Set(key, string(cacheJSON), cacheTimeout)

		return nil
	}

	userID := bf.props.GetString(BFUserID, "")
	if len(userID) == 0 {
		return nil, errors.New("missing Brewfather user id (user_id)")
	}

	apiKey := bf.props.GetString(BFAPIKey, "")
	if len(apiKey) == 0 {
		return nil, errors.New("missing Brewfather api key (api_key)")
	}

	batchID := bf.props.GetString(BFBatchID, "")
	if len(batchID) == 0 {
		return nil, errors.New("missing Brewfather batch id (batch_id)")
	}

	authString := fmt.Sprintf("%s:%s", userID, apiKey)
	authStringb64 := base64.StdEncoding.EncodeToString([]byte(authString))
	authHeader := fmt.Sprintf("Basic %s", authStringb64)
	batchURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s", batchID)
	batchReadingsURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s/readings", batchID)

	httpTimeout := bf.props.GetInt(properties.HTTPTimeout, properties.DefaultHTTPTimeout)
	cacheTimeout := bf.props.GetInt(BFCacheTimeout, 5)

	if cacheTimeout > 0 {
		if data, err := getFromCache(batchURL); err == nil {
			return data, nil
		}
	}

	// batch
	addAuthHeader := func(request *http.Request) {
		request.Header.Add("authorization", authHeader)
	}
	body, err := bf.env.HTTPRequest(batchURL, nil, httpTimeout, addAuthHeader)
	if err != nil {
		return nil, err
	}

	var batch Batch
	err = json.Unmarshal(body, &batch)
	if err != nil {
		return nil, err
	}

	// readings
	body, err = bf.env.HTTPRequest(batchReadingsURL, nil, httpTimeout, addAuthHeader)
	if err != nil {
		return nil, err
	}

	var arr []*BatchReading
	err = json.Unmarshal(body, &arr)
	if err != nil {
		return nil, err
	}

	if len(arr) > 0 {
		// could just take latest reading using their API, but that won't allow us to see trend - get 'em all and sort by time,
		// using two most recent for trend
		sort.Slice(arr, func(i, j int) bool {
			return arr[i].Time > arr[j].Time
		})

		// Keep the latest one
		batch.Reading = arr[0]

		if len(arr) > 1 {
			batch.TemperatureTrend = arr[0].Temperature - arr[1].Temperature
		}
	}

	if cacheTimeout > 0 {
		_ = putToCache(batchURL, &batch, cacheTimeout)
	}

	return &batch, nil
}

// Unit conversion functions available to template.
func (bf *Brewfather) DegCToF(degreesC float64) float64 {
	return math.Round(10*((degreesC*1.8)+32)) / 10 // 1 decimal place
}

func (bf *Brewfather) DegCToKelvin(degreesC float64) float64 {
	return math.Round(10*(degreesC+273.15)) / 10 // 1 decimal place, only addition, but just to be sure
}

func (bf *Brewfather) SGToBrix(sg float64) float64 {
	// from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2
	return math.Round(100*((182.4601*sg*sg*sg)-(775.6821*sg*sg)+(1262.7794*sg)-669.5622)) / 100
}

func (bf *Brewfather) SGToPlato(sg float64) float64 {
	// from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2
	return math.Round(100*((135.997*sg*sg*sg)-(630.272*sg*sg)+(1111.14*sg)-616.868)) / 100 // 2 decimal places
}

func (bf *Brewfather) Init(props properties.Properties, env platform.Environment) {
	bf.props = props
	bf.env = env
}
