package models

import (
	"encoding/base64"
	"fmt"
	"math"
	"strings"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/digitaltwin"
	"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"

	can "github.com/fiskerinc/cloud-services/pkg/can-go"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
	"github.com/pkg/errors"
)

type DBCVersionInterface interface {
	AddMsg(msg CANMessageInterface) DBCVersionInterface
	DecodeMessage(msg *kafka_grpc.GRPC_CANFrame) ([]descriptor.DecodedSignal, error)
	GenerateCANSignals(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, error)
	GetActiveDiagnosticFlags(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, bool, error)
	SetDiagnosticFlags(flags map[int]map[string]float64) DBCVersionInterface
	ParseState(id int, keyvaluesOut map[string]interface{}, vin string, timestampUSec int, incomingSignals []common.CANSignal, cached digitaltwin.DigitalTwinCacheInterface) error
	GetDatabase() *descriptor.Database
	Hash() string
}

func NewDBCVersion(dbc *descriptor.Database, hash string) DBCVersionInterface {
	if len(hash) < 3 {
		panic("NewDBCVersion hash cannot be less than 3 characters")
	}

	return &DBCVersion{
		dbc:  dbc,
		hash: hash,
	}
}

type DBCVersion struct {
	state           map[int]CANMessageInterface
	dbc             *descriptor.Database
	diagnosticFlags map[int]map[string]float64
	hash            string
}

func (d *DBCVersion) AddMsg(msg CANMessageInterface) DBCVersionInterface {
	m := d.getMsgMap()
	if _, ok := m[msg.GetID()]; ok {
		panic(fmt.Sprintf("CAN message %d already exists", msg.GetID()))
	}

	m[msg.GetID()] = msg

	return d
}

func (d *DBCVersion) getMsgMap() map[int]CANMessageInterface {
	if d.state == nil {
		d.state = map[int]CANMessageInterface{}
	}

	return d.state
}

func (d *DBCVersion) ParseState(id int, keyvaluesOut map[string]interface{}, vin string, timestampUSec int, incomingSignals []common.CANSignal, cached digitaltwin.DigitalTwinCacheInterface) error {
	m := d.getMsgMap()
	var msg CANMessageInterface
	var ok bool

	if msg, ok = m[id]; !ok {
		return nil
	}

	if i := len(incomingSignals); i != msg.GetLength() {
		return IncorrectSignalsLength(id, msg.GetLength(), i)
	}

	err := msg.ParseState(keyvaluesOut, vin, timestampUSec, incomingSignals, cached)

	// TODO run options here

	return err
}

func (d *DBCVersion) GenerateCANSignals(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, error) {
	var signals []common.CANSignal

	decodedSignals, err := d.DecodeMessage(msg)
	if err != nil {
		return signals, err
	}

	for _, signal := range decodedSignals {
		signals = append(signals, common.CANSignal{
			VIN:       vin,
			Timestamp: float64(msg.Epoch) * math.Pow(10, -6),
			ID:        int(msg.ID),
			Name:      signal.Signal.Name,
			Value:     signal.Value,
		})
	}

	return signals, nil
}

func (d *DBCVersion) DecodeMessage(msg *kafka_grpc.GRPC_CANFrame) ([]descriptor.DecodedSignal, error) {
	var decodedSignals []descriptor.DecodedSignal

	if msg.ID >= 2048 || msg.ID < 0 {
		if GetInvalidCANIDCache().Exists(d.hash, msg.ID) {
			return decodedSignals, nil
		}
		return decodedSignals, errors.Errorf("message id %v out of bounds", msg.ID)
	}

	message, ok := d.dbc.Message(uint32(msg.ID))
	if !ok {
		if GetInvalidCANIDCache().Exists(d.hash, msg.ID) {
			return decodedSignals, nil
		}

		return decodedSignals, errors.Errorf(
			"message id %v not found in %s, version: %s",
			msg.ID, d.dbc.SourceFile, d.hash)
	}

	strippedData := strings.TrimRight(string(msg.Value), "=")
	data, err := base64.RawStdEncoding.DecodeString(strippedData)
	if err != nil {
		return decodedSignals, errors.WithStack(err)
	}

	p := can.Payload{Data: data}
	decodedSignals = message.Decode(&p)
	return decodedSignals, nil
}

func (d *DBCVersion) GetActiveDiagnosticFlags(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, bool, error) {
	var flags []common.CANSignal

	diagnosticSignals, ok := d.diagnosticFlags[int(msg.ID)]
	if !ok {
		return flags, false, nil
	}

	decodedSignals, err := d.DecodeMessage(msg)
	if err != nil {
		return flags, false, err
	}

	flags = make([]common.CANSignal, 0)
	for _, signal := range decodedSignals {
		faultVal, ok := diagnosticSignals[signal.Signal.Name]

		if !ok {
			continue
		} else if signal.Value != faultVal {
			continue
		}

		flags = append(flags, common.CANSignal{
			VIN:       vin,
			Timestamp: float64(msg.Epoch) * math.Pow(10, -6),
			ID:        int(msg.ID),
			Name:      signal.Signal.Name,
			Value:     signal.Value,
		})
	}

	return flags, len(flags) != 0, nil
}

func (d *DBCVersion) Hash() string {
	return d.hash
}

// TODO this should be replaced by generated diagnostic alerts from the DBC
func (d *DBCVersion) SetDiagnosticFlags(flags map[int]map[string]float64) DBCVersionInterface {
	d.diagnosticFlags = flags
	return d
}

func (d *DBCVersion) GetDatabase() *descriptor.Database {
	return d.dbc
}
