package common

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"

	"github.com/pkg/errors"
	"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
	"github.com/fiskerinc/cloud-services/pkg/vindecoder"
)

// Any changes need to be made to modules_go/db/queries/updatemanifests.go ECURollback() on getRollbacksOld
type UpdateManifestECU struct {
	ID                int64                 `json:"id,omitempty" pg:",pk"`
	UpdateManifestID  int64                 `json:"manifest_id,omitempty" pg:",unique:updatemanifestid_ecu_partnumber" validate:"required,gte=0"`
	ECU               string                `json:"name,omitempty" pg:",unique:updatemanifestid_ecu_partnumber" validate:"required,min=2,max=10"`
	Version           string                `json:"version" validate:"required,max=255"`
	CurrentVersion    string                `json:"current_version,omitempty" pg:"-"`
	HWVersion         string                `json:"hw_version,omitempty" pg:"-"`
	HWVersions        []string              `json:"hw_versions,omitempty" pg:"hw_versions,array"`
	ConfigurationMask string                `json:"configuration_mask,omitempty" validate:"max=5000"`
	Configuration     string                `json:"configuration,omitempty" validate:"max=5000"`
	SelfDownload      bool                  `json:"self_download,omitempty"`
	Mode              string                `json:"mode,omitempty" validate:"required,max=4"`
	Files             []*UpdateManifestFile `json:"files,omitempty" pg:"rel:has-many"`
	Rollback          []*UpdateManifestECU  `json:"rollback,omitempty" pg:"-"`
	ECCKeys           *ECCKeys              `json:"ecc_keys,omitempty" pg:"-"`
	InstallPriority   int                   `json:"install_priority,omitempty" pg:"install_priority"`
	dbbasemodel.DBModelBase
}

// Scrub cleans data for sending to car
// clearHWVersion: Determines wether or not the hw versions field should be cleared, by default should be true. Need this so the front end can see
// since scrub is called
func (ume *UpdateManifestECU) Scrub(ecuType Device, clearHWVersion bool) {
	// As a final catch all incase the transform was not called before sending the manifest
	ume.TransformECUName()

	ume.ID = 0
	ume.Mode = ""
	ume.UpdateManifestID = 0
	ume.CreatedAt = nil
	ume.UpdatedAt = nil

	if clearHWVersion {
		if ume.HWVersion == "" && len(ume.HWVersions) > 0 {
			ume.HWVersion = ume.HWVersions[0]
		}
		ume.HWVersions = nil
	}

	if ecuType == TRex && ume.SelfDownload {
		ume.Files = make([]*UpdateManifestFile, 0)
	}

	ume.scrubKeys(ecuType)

	ume.SortFiles()
	ume.ImageSignatureChecksumCheck()

	for _, file := range ume.Files {
		file.Scrub()
	}

	for _, ecu := range ume.Rollback {
		ecu.RollbackScrub(ecuType)
	}

	ume.InstallPriority = 0
}

func (ume *UpdateManifestECU) scrubKeys(ecu Device) {
	if ume.ECCKeys == nil || ecu == HMI || (ecu == TRex && ume.SelfDownload) {
		ume.ECCKeys = nil
		return
	}

	keys := ume.ECCKeys
	keys.ScrubForManifest()
}

func (ume *UpdateManifestECU) RollbackScrub(ecuType Device) {
	ume.ECU = ""
	ume.Mode = ""
	ume.HWVersion = ""
	ume.HWVersions = []string{}
	ume.ECCKeys = nil
	ume.Scrub(ecuType, true)
}

func (ume *UpdateManifestECU) SortFiles() {
	sort.Slice(ume.Files, func(i int, j int) bool {
		order1 := ume.Files[i].FileOrder
		order2 := ume.Files[j].FileOrder

		if order1 == order2 {
			return ume.sortFileByType(i, j)
		}

		return order1 < order2
	})
}

func (ume *UpdateManifestECU) sortFileByType(i int, j int) bool {
	order1 := fileSortOrder[ume.Files[i].FileType]
	order2 := fileSortOrder[ume.Files[j].FileType]

	return order1 < order2
}

// Given a set of files, figure out subset which are s19/hex files that are split. Order them, and remove the checksum
// Then have the signature only on the last file
func (ume *UpdateManifestECU) ImageSignatureChecksumCheck() (err error) {
	// Ideally the remove parsed and remove original will have already been called
	type FileIndexStruct struct {
		FileName string
		// Parsed   *bool // Cant use a pointer as it index's on pointer value
		HasParsed bool // True if the file has true or false for parsed
		Parsed    bool // True if HasParsed and Parsed
	}
	// First need to find the file names
	filesMap := make(map[FileIndexStruct][]*UpdateManifestFile, 0)

	// Prepare our files for sorting by putting them into a map
	for _, file := range ume.Files {
		if file.Signature != "" {
			file.Checksum = ""
		}
		fis := FileIndexStruct{}
		fis.FileName = file.Filename
		if file.Parsed == nil {
			fis.HasParsed = false
			fis.Parsed = false
		} else if !*file.Parsed {
			fis.HasParsed = true
			fis.Parsed = false
		} else {
			fis.HasParsed = true
			fis.Parsed = true
			// Need to parse the file name now, so we can match them up. It's okay if we have the parsed s19's with the bin at the end as they all will
			regexExp := regexp.MustCompile(`(\S*.(?:s19|hex))_(\d+).bin`)
			// Returns [ fullFileName.19_5.bin fullFileName.19 19]
			matches := regexExp.FindStringSubmatch(file.Filename)
			// Like a parsed file should always follow this file name format, but maybe someones files is in whacky form and has extra characters in it
			if len(matches) != 3 {
				return errors.New("Length of array was not 3")
			}
			fis.FileName = matches[1]
			file.FileOrder, err = strconv.Atoi(matches[2])
			if err != nil {
				return err
			}
		}
		// Add the file to its array
		filesArray, ok := filesMap[fis]
		if !ok {
			filesArray = make([]*UpdateManifestFile, 0)
		}
		filesArray = append(filesArray, file)
		filesMap[fis] = filesArray
	}

	// So this is going to be an array or array of files, so that we can run the sort by file type thing
	filesArrayArray := make([][]*UpdateManifestFile, 0)
	// Now we will sort our files, I really just care about the parsed ones right now.
	for index, filesArray := range filesMap {
		// If we are parsed files, then we need to order them, and then remove checksum field for just a single signature
		if index.HasParsed && index.Parsed {
			//Sort them in 0,1,2,3... order
			sort.Slice(filesArray, func(i, j int) bool {
				// This function is less f[i] < f[j]
				return filesArray[i].FileOrder < filesArray[j].FileOrder
			})

			// Check first if there is a signature, then remove all the checksums if there is
			// If there is checksums and no signature, we can ignore

			if filesArray[0].Signature != "" {
				// If there is a signature, only the last one gets it
				for x := 0; x < len(filesArray)-1; x++ {
					// Remove the checksum and signature from all but the last element
					filesArray[x].Checksum = ""
					filesArray[x].Signature = ""
				}
				filesArray[len(filesArray)-1].Checksum = ""
			}

		}

		// Now we add our sorted files back in
		// ume.Files = append(ume.Files, filesArray...)
		// Going to maintain our sorted files file type
		filesArrayArray = append(filesArrayArray, filesArray)
	}

	sort.Slice(filesArrayArray, func(i, j int) bool {
		order1 := fileSortOrder[filesArrayArray[i][0].FileType]
		order2 := fileSortOrder[filesArrayArray[j][0].FileType]

		return order1 < order2
	})

	ume.Files = make([]*UpdateManifestFile, 0)
	for _, filesArray := range filesArrayArray {
		ume.Files = append(ume.Files, filesArray...)
	}
	return nil
}

func (ume *UpdateManifestECU) String() string {
	return fmt.Sprintf("UpdateManifestECU<%s %s %s>", ume.ECU, ume.Version, ume.HWVersions)
}

func (ume *UpdateManifestECU) GetFileIDs() []string {
	ids := make([]string, len(ume.Files))

	for i := range ume.Files {
		ids[i] = ume.Files[i].FileID
	}

	return ids
}

// 3 Cases for parsed: nil: original not s19. false: original s19, true: parsed s19
// Removes files from the list that are the parsed reference to the uploaded files
// For aftersales
func (ume *UpdateManifestECU) RemoveParsedS19HexFiles() {
	i := 0
	for _, file := range ume.Files {
		// This if is for keeping the file, if false its removed
		if file.Parsed == nil || !*file.Parsed {
			ume.Files[i] = file
			i++
		}
	}
	ume.Files = ume.Files[:i]
}

// Removes files from the list that are the original reference to the uploaded file
// For attendent
func (ume *UpdateManifestECU) RemoveOriginalS19HexFiles() {
	i := 0
	for _, file := range ume.Files {
		if file.Parsed == nil || *file.Parsed {
			ume.Files[i] = file
			i++
		}
	}
	ume.Files = ume.Files[:i]
}

// Removes files from the list that are not s19 or hex file related. So files that have null for parsed
func (ume *UpdateManifestECU) RemovedNonS19HexFiles() {
	i := 0
	for _, file := range ume.Files {
		if file.Parsed == nil {
			ume.Files[i] = file
			i++
		}
	}
	ume.Files = ume.Files[:i]
}

// For aftersales
func (ume *UpdateManifestECU) RemoveParsedS19HexFilesRollbacks() {
	for _, ecu := range ume.Rollback {
		ecu.RemoveParsedS19HexFiles()
	}
}

// For attendent
func (ume *UpdateManifestECU) RemoveOriginalS19HexFilesRollbacks() {
	for _, ecu := range ume.Rollback {
		ecu.RemoveOriginalS19HexFiles()
	}

}

func (ume *UpdateManifestECU) ToUpdateConfigManifestECU() (ucme UpdateConfigManifestECU) {
	ucme = UpdateConfigManifestECU{
		ECU:           ume.ECU,
		Configuration: ume.Configuration,
	}
	return
}

// In case we need to rename a ecu with files and data before sending it to a car.
// Also rename its roll backs. This really does need to happen before GetCDS is fired off
// I do not want to transform the ECU on import. These renames could change or could hopefully go away
func (ume *UpdateManifestECU) TransformECUName() {
	replacement, ok := ECUReplacement()[ume.ECU]
	if ok {
		ume.ECU = replacement
	}

	if ume.ECCKeys != nil {
		ume.ECCKeys.TransformECUName()
	}

	if ume.Rollback != nil {
		for _, rollbackECU := range ume.Rollback {
			rollbackECU.TransformECUName()
		}
	}
}

// Given a vin, extract its trim level
func (um *UpdateManifestECU) FilterECUFiles(filter ECUFileFilter) {
	offset := 0
	for _, file := range um.Files {
		if file.IsFileCompatible(filter) {
			um.Files[offset] = file
			offset++
		}
	}
	um.Files = um.Files[:offset]
}

type ECUFileFilter struct {
	Trim      CompatibleTrim
	DriveSide CompatibleDriveSide
}

func vinTrimToFileTrim(vinTrim string) (fileTrim CompatibleTrim, ok bool) {
	ok = true
	switch vinTrim {
	case vindecoder.TRIM_SPORT:
		return SPORT, ok
	case vindecoder.TRIM_ULTRA:
		return ULTRA, ok
	case vindecoder.TRIM_EXTREME:
		return EXTREME, ok
	case vindecoder.TRIM_OCEAN_ONE:
		return EXTREME, ok
	}
	ok = false
	return
}

func vodDriveSideToFileDriveSide(side string) (compatibleDriveSide CompatibleDriveSide, ok bool) {
	switch side {
	case "RIGHT":
		return RIGHT_HAND_DRIVE, true
	case "LEFT":
		return LEFT_HAND_DRIVE, true
	default:
		return "INVALID", false
	}
}
