package common

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	v "github.com/fiskerinc/cloud-services/pkg/utils/vod"
	"github.com/fiskerinc/cloud-services/pkg/vindecoder"
	"github.com/fiskerinc/cloud-services/pkg/vod_decoder"

	"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/albenik/bcd"
	"github.com/jinzhu/copier"
	"github.com/pkg/errors"
)

// Had to copy getenv to stop import cycle
var defaultVOD string = envtool.GetEnv("DEFAULT_VOD", "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
var defaultUpdateDuration int = getDefaultUpdateDuration()

const (
	ecuICCName           string = "ICC"
	ManifestTypeStandard string = "standard"
	ManifestTypeForced   string = "forced"
	ManufacturerName     string = "FISKER"
	EnvCurrent           string = "current"
)

// If a change here will mess with a manifest environment migration, increment MIGRATION_VERSION in
// ota_update_go/handlers/update_manifest_migrate.go and create an adapter or return an error if it
// is incompatable
type UpdateManifest struct {
	ID              int64                `json:"id,omitempty"`
	Name            string               `json:"name,omitempty" pg:",unique:name_version" validate:"required,max=255"`
	Version         string               `json:"version,omitempty" pg:",unique:name_version" validate:"max=255"`
	Description     string               `json:"description,omitempty" validate:"max=5120"`
	ReleaseNotes    string               `json:"release_notes,omitempty" validate:"omitempty,max=32768,url"`
	ECUList         string               `json:"ecu_list,omitempty"`
	ECUs            []*UpdateManifestECU `json:"ecu_updates,omitempty" pg:"rel:has-many" validate:"omitempty,min=1,dive"`
	Fingerprint     string               `json:"fingerprint,omitempty" validate:"max=5000"`
	CarUpdateID     int64                `json:"car_update_id,omitempty" pg:"-"`
	RollbackEnabled bool                 `json:"rollback" pg:",use_zero"`
	Type            string               `json:"type,omitempty" validate:"max=100"`
	VOD             string               `json:"vod,omitempty" pg:"vod"`
	ManifestType    UpdateManifestType   `json:"manifest_type,omitempty" validate:"oneof=0 1 2 3 4" swaggerignore:"true"`
	Active          *bool                `json:"active,omitempty"` // Use a pointer so we know when we want to change the value
	Country         string               `json:"country,omitempty" pg:"country"`
	PowerTrain      string               `json:"powertrain,omitempty" pg:"powertrain"`
	Restraint       string               `json:"restraint,omitempty" pg:"restraint"`
	Model           string               `json:"model,omitempty" pg:"model"`
	Trim            string               `json:"trim,omitempty" pg:"trim"`
	Year            int                  `json:"year,omitempty" pg:"year"`
	BodyType        string               `json:"body_type,omitempty" pg:"body_type"`
	SUMS            string               `json:"sums,omitempty"`                                 // Software Update Management System
	Env             string               `json:"env,omitempty"`                                  // Environment of ECC keys to include
	UpdateDuration  int                  `json:"update_duration,omitempty" pg:"update_duration"` // Duration of update in minutes
	MaxAttempts     int                  `json:"max_attempts,omitempty" pg:"max_attempts"`
	dbbasemodel.DBModelBase
}

func (um *UpdateManifest) String() string {
	return fmt.Sprintf("UpdateManifest<%d %s %s>", um.ID, um.Name, um.Version)
}

// Scrub cleans data for sending to car
func (um *UpdateManifest) Scrub(ecuType Device) {
	um.ID = 0
	um.Env = ""
	um.ECUList = ""
	um.CreatedAt = nil
	um.UpdatedAt = nil
	um.ManifestType = 0
	um.Active = nil
	um.Country = ""
	um.PowerTrain = ""
	um.Restraint = ""
	um.Model = ""
	um.Trim = ""
	um.Year = 0
	um.BodyType = ""
	um.SUMS = ""

	// If the update has no duration, we can default it to 30
	if um.UpdateDuration == 0 {
		um.UpdateDuration = 30
	}

	vodIndex := -1 
	for x, ecu := range um.ECUs {
		ecu.Scrub(ecuType, true)
		if ecu.ECU == "VOD" {
			vodIndex = x
		}
	}

	if vodIndex != -1 {
		um.ECUs = append(um.ECUs[:vodIndex], um.ECUs[vodIndex+1:]..., )
	}


	if ecuType != HMI {
		um.Name = ""
		um.Version = ""
		um.Description = ""
		um.ReleaseNotes = ""
	}
}

func (um *UpdateManifest) RemoveParsedS19HexFiles() {
	for _, ecu := range um.ECUs {
		ecu.RemoveParsedS19HexFiles()
		for _, rollback := range ecu.Rollback {
			rollback.RemoveParsedS19HexFiles()
		}
	}
}

func (um *UpdateManifest) RemoveOriginalS19HexFiles() {
	for _, ecu := range um.ECUs {
		ecu.RemoveOriginalS19HexFiles()
		for _, rollback := range ecu.Rollback {
			rollback.RemoveOriginalS19HexFiles()
		}
	}
}

func (um *UpdateManifest) RemoveECCKeysFromECUs() {
	for _, ecu := range um.ECUs {
		ecu.ECCKeys = nil
	}
}

func (um *UpdateManifest) RemoveParsedS19HexFilesRollbacks() {
	for i := range um.ECUs {
		for j := range um.ECUs[i].Rollback {
			um.ECUs[i].Rollback[j].RemoveParsedS19HexFiles()
		}
	}
}

func (um *UpdateManifest) RemoveOriginalS19HexFilesRollbacks() {
	for i := range um.ECUs {
		for j := range um.ECUs[i].Rollback {
			um.ECUs[i].Rollback[j].RemoveOriginalS19HexFiles()
		}
	}
}

func (um *UpdateManifest) SortECUs() {
	// If this is an old update manifest without sorting info, we will fall back to the old sort
	nonZero := false
	for _, ecu := range um.ECUs {
		if ecu.InstallPriority > 0 {
			nonZero = true
			break
		}
	}

	// Have a non-zero value, sort by install priority
	if nonZero {
		// Sort in Ascending order
		sort.Slice(um.ECUs, func(i, j int) bool {
			return um.ECUs[i].InstallPriority < um.ECUs[j].InstallPriority
		})
	} else {
		// We do not have any install priorities, so fall back to the old sort method
		um.sortECUByMode()
	}
}

func (um *UpdateManifest) sortECUByMode() {
	sort.Slice(um.ECUs, func(i, j int) bool {
		return um.modePosition(um.ECUs[i]) < um.modePosition(um.ECUs[j])
	})
}

func (um *UpdateManifest) modePosition(ecu *UpdateManifestECU) int {

	if ecu.SelfDownload {
		return 4
	}

	if ecu.ECU == "TREX" {
		return 0
	}

	// Easy catch all case as PDU and OBC are interswappable
	if ecu.ECU == "PDU" || ecu.ECU == "OBC" {
		return 3
	}

	if ecu.Mode == "D" {
		return 2
	}

	return 1
}

func (um *UpdateManifest) HasComponent(name string) bool {
	match := strings.ToUpper(name)

	for _, ecu := range um.ECUs {
		if ecu.ECU == match {
			return true
		}
	}

	return false
}

func (um *UpdateManifest) HasSelfDownload() bool {
	for _, ecu := range um.ECUs {
		if ecu.SelfDownload {
			return true
		}
	}

	return false
}

func (um *UpdateManifest) Copy() *UpdateManifest {
	clone := *um
	ecus := make([]*UpdateManifestECU, len(um.ECUs))
	copier.CopyWithOption(&ecus, &um.ECUs, copier.Option{DeepCopy: true})
	clone.ECUs = ecus

	return &clone
}

func (um *UpdateManifest) GenerateFingerprint(curDate time.Time, serialSfx string) {
	if um.Fingerprint == "" {
		y, m, d := curDate.Date()
		y = y % 100
		um.Fingerprint = fmt.Sprintf("%02d%02d%02d%s%s", y, m, d, ManufacturerName, serialSfx)
		// 29 is the max length of the fingerprint
		um.Fingerprint = string([]byte(um.Fingerprint)[:29])
	}
}

// Pulls the vod field from cds, and then applies it to the updateManifest
// If the vod is not found, we insert the default vod
// We also remove the VOD field from the cds map
func (um *UpdateManifest) AddVOD(cds map[string]string) {
	if um.VOD == "" {
		if vod, ok := cds["VOD"]; ok && vod != "" {
			um.VOD = cds["VOD"]
		} else {
			um.VOD = defaultVOD
		}
	}

	delete(cds, "VOD")
}

// Applies the cds coding strings to the VOD field along with each
// ECU that is inside the update manifest
// Any extra cds does not create the ecu field
func (um *UpdateManifest) AddCDSToECUs(cds map[string]string) {
	um.AddVOD(cds)

	for key := range um.ECUs {
		um.ECUs[key].Configuration = cds[um.ECUs[key].ECU]
	}
}

// Will take the UpdateManifest's ECU's, and transform their names to send to the car
func (um *UpdateManifest) TransformECUNames() {
	for _, ecu := range um.ECUs {
		ecu.TransformECUName()
	}
}

// Will add the ecu's the set of ECU's if there are no ecu's already in the manifest
func (um *UpdateManifest) AddECUsFromCDSList(cds map[string]string) {
	if len(um.ECUs) == 0 {
		um.ECUs = make([]*UpdateManifestECU, 0)
		for key, value := range cds {
			ecu := UpdateManifestECU{
				UpdateManifestID: um.ID,
				ECU:              key,
				Configuration:    value,
			}
			um.ECUs = append(um.ECUs, &ecu)
		}
	}
}

// Fill out um.ECUList from um.ECUS
func (um *UpdateManifest) FillECUList() {
	str := ""
	for _, ecu := range um.ECUs {
		str = str + ecu.ECU + ","
	}
	str = strings.TrimRight(str, ",")
	um.ECUList = str
}

func (um *UpdateManifest) GetFileIDs() []string {
	ids := []string{}

	if len(um.ECUs) == 0 {
		return ids
	}

	for _, ecu := range um.ECUs {
		result := ecu.GetFileIDs()
		if len(result) > 0 {
			ids = append(ids, result...)
		}
	}

	return ids
}

// Becaue of the differences in the new hardware versioning, we still need to be able to migrate, so going to send filled old hw_version
// This should be deleted as soon as hardware versions list reaches all the way to production
func (um *UpdateManifest) MigratePrepareHardwareVersion() {
	for _, ecu := range um.ECUs {
		if len(ecu.HWVersions) >= 1 {
			ecu.HWVersion = ecu.HWVersions[0]
		}
	}
}

var dataLength int = 255
var vodHasCRCToRemovce bool = true // If the given VOD already has a CRC code at the end, remove it
var lengthInCRC bool = false       // True if the CRC includes the length bytes in its calculation
var crcInLength bool = false       // True if the length includes the extra 1 bit for crc
var lengthInLength bool = true     // True if the length includes its own two bytes
var choppedLengthOffset int = 2    // Because we are cutting off the front two bytes that would be the length

func (um *UpdateManifest) AddSUMSToVOD() (err error) {
	if um.VOD == "" {
		um.VOD = defaultVOD
	}
	vod, err := hex.DecodeString(um.VOD)
	if err != nil {
		return errors.WithStack(err)
	}

	//Removing the previous length
	vod = vod[2:]
	// Remove the previous CRC
	if vodHasCRCToRemovce {
		vod = vod[:len(vod)-1]
	}
	// Ensure that the vod byte's is long enough. Just incase it was truncated at the end
	// 2 byte length, 255 byte data, 1 byte CRC
	new_vod := make([]byte, dataLength)
	// copy vod excluding CRC last byte
	copy(new_vod, vod)

	// Now we will modify the bytes as so
	// I did double check this uint16 -> uint8 takes the last 8 bits correctly
	versionInfo, err := um.parseVersionNumber()
	if err != nil {
		return errors.WithStack(err)
	}
	//Field          Strt Byte   StrtBit  EndBit  byteLength Encoding
	//Cld. Mnfst.ID  200         bit 0    bit 15  2 bytes     BCD
	bcdEncoded := bcd.FromUint16(uint16(um.ID))
	new_vod[200-choppedLengthOffset] = bcdEncoded[0]
	new_vod[201-choppedLengthOffset] = bcdEncoded[1]
	//Year           202         bit 0    bit 15  2 bytes     BCD
	bcdEncoded = bcd.FromUint16(uint16(versionInfo.Year))
	new_vod[202-choppedLengthOffset] = bcdEncoded[0]
	new_vod[203-choppedLengthOffset] = bcdEncoded[1]
	//Month          204         bit 0    bit 7   1 byte      BCD
	var bcdByte byte
	bcdByte = bcd.FromUint8(uint8(versionInfo.Month))
	new_vod[204-choppedLengthOffset] = bcdByte
	//Major V.       205         bit 0    bit 7   1 byte      BCD
	bcdByte = bcd.FromUint8(uint8(versionInfo.MajorVersion))
	new_vod[205-choppedLengthOffset] = bcdByte
	//Minor V.       206         bit 0    bit 7   1 byte      BCD
	bcdByte = bcd.FromUint8(uint8(versionInfo.MinorVersion))
	new_vod[206-choppedLengthOffset] = bcdByte
	//Eng. Rls.      207         bit 0    bit 7   1 byte      binary: 0x00 Formal, 0x01 Engineering
	if versionInfo.EngineeringRelease {
		new_vod[207-choppedLengthOffset] = 0x01
	}

	helper := v.NewVODHelper(lengthInCRC,
		crcInLength,
		lengthInLength)
	new_vod = helper.AddLengthAndCRC(new_vod)
	um.VOD = hex.EncodeToString(new_vod)
	return
}

// Given 2023.05.01.02.E parses that up.
// No ID in beginning
type version struct {
	Year               int
	Month              int
	MajorVersion       int
	MinorVersion       int
	EngineeringRelease bool // True if .E is present
}

func (um *UpdateManifest) parseVersionNumber() (v version, err error) {
	vals := strings.Split(um.SUMS, ".")

	if len(vals) < 4 {
		err = errors.Errorf("invalid version %s", um.Version)
		return
	}

	v.Year, err = strconv.Atoi(vals[0])
	if err != nil {
		return
	}
	v.Month, err = strconv.Atoi(vals[1])
	if err != nil {
		return
	}
	v.MajorVersion, err = strconv.Atoi(vals[2])
	if err != nil {
		return
	}
	v.MinorVersion, err = strconv.Atoi(vals[3])
	if err != nil {
		return
	}

	if len(vals) > 4 && vals[4] == "E" {
		v.EngineeringRelease = true
	}
	return
}

func (um *UpdateManifest) GetECUs() []string {
	ecus := make([]string, len(um.ECUs))

	for i, ecu := range um.ECUs {
		ecus[i] = ecu.ECU
	}

	return ecus
}

func (um *UpdateManifest) getECU(name string) *UpdateManifestECU {
	for i := range um.ECUs {
		if um.ECUs[i].ECU == name {
			return um.ECUs[i]
		}
	}

	return nil
}

func (um *UpdateManifest) AddECUECCKeys(keys []ECCKeys) {
	for i, key := range keys {
		ecu := um.getECU(key.ECU)
		if ecu != nil {
			ecu.ECCKeys = &keys[i]
		}
	}
}

func (um *UpdateManifest) Clone() UpdateManifest {
	copy := UpdateManifest{}
	js, _ := json.Marshal(um)
	json.Unmarshal(js, &copy)

	return copy
}

// I think this code is a bit silly
func (um *UpdateManifest) ToUpdateConfigManifest() (configManifest UpdateConfigManifest) {
	configManifest = UpdateConfigManifest{
		CarUpdateID: um.CarUpdateID,
		ECUs:        []*UpdateConfigManifestECU{},
		Type:        um.Type,
		VOD:         um.VOD,
	}

	for _, ecu := range um.ECUs {
		v := ecu.ToUpdateConfigManifestECU()
		configManifest.ECUs = append(configManifest.ECUs, &v)
	}

	return
}

type UpdateManifestSearch struct {
	Search string `json:"search" validate:"max=1024"`
	UpdateManifest
}

func getDefaultUpdateDuration() int {
	i, err := strconv.Atoi(envtool.GetEnv("DEFAULT_UPDATE_DURATION", "30"))

	if err != nil {
		return 30
	}
	return i
}

// FilterCompatibleECUs Expects the UpdateManifest to have a filled VOD field
// If the VOD field is not filled, this will return improper results
func (um *UpdateManifest)FilterCompatibleECUs(vin string){
	fileFilter := ECUFileFilter{}

	info, ok := vindecoder.DecodeVIN(vin)
	if !ok {
		// Hanlde that we are not okay
		return
	}

	fileFilter.Trim, ok = vinTrimToFileTrim(info.Trim)
	if !ok {
		// Hanlde that we are not okay
		return
	}

	side, ok := vod_decoder.GetDriverSideFromVOD(um.VOD)
	if !ok {
		return
	}
	fileFilter.DriveSide, ok = vodDriveSideToFileDriveSide(side)
	if !ok {
		return
	}

	for _, ecu := range um.ECUs {
		ecu.FilterECUFiles(fileFilter)
	}
}