package queries

import (
	"fmt"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/validator"

	"github.com/go-pg/pg/v10"
	"github.com/go-pg/pg/v10/orm"
	"github.com/pkg/errors"
)

type UpdateManifestsInterface interface {
	QueryBaseInterface
	Archive(ids []int64, active bool) (orm.Result, error)
	Count(filter common.UpdateManifest) (int, error)
	Delete(manifest *common.UpdateManifest) (orm.Result, error)
	Insert(manifest *common.UpdateManifest) (orm.Result, error)
	Select(filter *common.UpdateManifest, paging *PageQueryOptions) ([]common.UpdateManifest, error)
	SelectByVIN(vin string, paging *PageQueryOptions) ([]common.StatusManifest, error)
	Update(manifest *common.UpdateManifest) (orm.Result, error)
	Load(manifest *common.UpdateManifest) error
	Search(filter common.UpdateManifestSearch, paging *PageQueryOptions) ([]common.UpdateManifest, error)
	SearchCount(filter common.UpdateManifestSearch) (int, error)
	ECURollback(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error)
	ECUInsert(manifest *common.UpdateManifestECU) (orm.Result, error)
	FileInsert(manifest *common.UpdateManifestFile) (orm.Result, error)
	AddSUMSVersion(manifest *common.UpdateManifest) (orm.Result, error)
	SelectFlashPackByVersion(versionNumber string) (manifest common.UpdateManifest, err error)
}

// NewUpdateManifest returns UpdateManifestInterface.
// mode allows to use more flexible requests.
// If mode is nil, default mode is used.
func NewUpdateManifest(mode UpdateManifestMode) UpdateManifestsInterface {
	if mode == nil {
		return &UpdateManifests{
			mode: DefaultMode{},
		}
	}

	return &UpdateManifests{
		mode: mode,
	}
}

type UpdateManifests struct {
	mode UpdateManifestMode
	QueryBase
}

func (um *UpdateManifests) Count(filter common.UpdateManifest) (int, error) {
	query := um.GetDBConn().Model((*common.UpdateManifest)(nil))

	um.selectFilter(query, &filter)

	return query.Count()
}

func (um *UpdateManifests) Delete(manifest *common.UpdateManifest) (orm.Result, error) {
	total := ORMResults{}

	err := validator.ValidateIDField(manifest.ID)
	if err != nil {
		return nil, err
	}

	err = um.Load(manifest)
	if err != nil {
		return nil, err
	}

	tx, err := um.GetDBConn().Begin()
	if err != nil {
		return nil, err
	}

	_, err = um.manifestDelete(tx, &total, manifest)
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	err = tx.Commit()
	if err != nil {
		return nil, err
	}

	return &total, err
}

func (um *UpdateManifests) Insert(manifest *common.UpdateManifest) (orm.Result, error) {
	total := ORMResults{}

	tx, err := um.GetDBConn().Begin()
	if err != nil {
		err = errors.WithStack(err)
		return nil, err
	}

	_, err = um.manifestInsert(tx, &total, manifest)
	if err != nil {
		tx.Rollback()
		err = errors.WithStack(err)
		return nil, err
	}

	err = tx.Commit()
	if err != nil {
		err = errors.WithStack(err)
		return nil, err
	}

	return &total, nil
}

func (um *UpdateManifests) ECUInsert(ecu *common.UpdateManifestECU) (orm.Result, error) {
	total := ORMResults{}

	tx, err := um.GetDBConn().Begin()
	if err != nil {
		return nil, err
	}

	_, err = um.ecuInsert(tx, &total, ecu)
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	err = tx.Commit()
	if err != nil {
		return nil, err
	}

	return &total, nil
}

func (um *UpdateManifests) FileInsert(file *common.UpdateManifestFile) (orm.Result, error) {
	total := ORMResults{}

	tx, err := um.GetDBConn().Begin()
	if err != nil {
		return nil, err
	}

	_, err = um.fileInsert(tx, &total, file)
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	err = tx.Commit()
	if err != nil {
		return nil, err
	}

	return &total, nil
}

func (um *UpdateManifests) Select(filter *common.UpdateManifest, paging *PageQueryOptions) ([]common.UpdateManifest, error) {
	manifests := []common.UpdateManifest{}
	query := um.GetDBConn().Model(&manifests)

	um.selectFilter(query, filter)
	um.pageQuery(query, paging)

	err := query.Select()

	return manifests, errors.WithStack(err)
}

func (um *UpdateManifests) Archive(ids []int64, active bool) (orm.Result, error) {
	manifests := []common.UpdateManifest{}
	return um.resultWithStack(
		um.GetDBConn().Model(&manifests).
			Set("active = ?", active).
			Where("id IN (?)", pg.In(ids)).
			Update(),
	)
}

func (um *UpdateManifests) SelectByVIN(vin string, paging *PageQueryOptions) ([]common.StatusManifest, error) {
	manifests := []common.StatusManifest{}
	query := um.GetDBConn().Model(&manifests)
	query.
		ColumnExpr("status_manifest.*").
		ColumnExpr("car_updates.status as status").
		ColumnExpr("car_updates.updated_at as status_updated").
		ColumnExpr("status_manifest.created_at as manifest_created").
		Join("LEFT JOIN car_updates ON car_updates.update_manifest_id=status_manifest.id").
		Where("vin = ?", vin)

	query = um.mode.SelectByVINCondition(query)
	query = um.pageQuery(query, paging)
	err := query.Select()

	return manifests, errors.WithStack(err)
}

func (um *UpdateManifests) Update(manifest *common.UpdateManifest) (orm.Result, error) {
	err := validator.ValidateIDField(manifest.ID)
	if err != nil {
		return nil, err
	}
	query := um.GetDBConn().Model(manifest).Column(
		"name",
		"release_notes",
		"type",
		"fingerprint",
		"rollback_enabled",
		"update_duration",
		"max_attempts",
	)
	if manifest.Active != nil {
		query = query.Column("active")
	}
	if manifest.Env != "" {
		query = query.Column("env")
	}
	if manifest.SUMS != "" {
		query = query.Column("sums")
	}

	return query.WherePK().Update()
}

func (um *UpdateManifests) Load(manifest *common.UpdateManifest) error {
	query := um.GetDBConn().Model(manifest)

	if manifest.ID > 0 {
		query.WherePK()
	} else if manifest.Name != "" && manifest.Version != "" {
		query.Where("name = ?name AND version = ?version")
	} else {
		return errors.New("no id, name or version")
	}

	// For Magna, we will add the manifest type, then we will only get it when it is equal to two
	if manifest.ManifestType > 0 {
		query.Where("manifest_type = ?", manifest.ManifestType)
	}

	if manifest.Env == "" {
		manifest.Env = common.EnvCurrent
	}

	return um.mode.LoadRelations(query)
}

func (um *UpdateManifests) Search(filter common.UpdateManifestSearch, paging *PageQueryOptions) ([]common.UpdateManifest, error) {
	manifests := []common.UpdateManifest{}
	query := um.GetDBConn().Model(&manifests)

	um.searchFilter(query, &filter)
	um.pageQuery(query, paging)

	err := query.Select()

	return manifests, errors.WithStack(err)
}

func (um *UpdateManifests) SearchCount(filter common.UpdateManifestSearch) (int, error) {
	query := um.GetDBConn().Model((*common.UpdateManifest)(nil))

	um.searchFilter(query, &filter)

	return query.Count()
}

// Used to get rollbacks for ecu's
func (um *UpdateManifests) ECURollback(man *common.UpdateManifestECU, vin string) (rollBackManifest []*common.UpdateManifestECU, err error) {
	// Use our new rollback system, if it fails return the old get for rollbacks
	// This may fail as the car_ecus system is a bit corrupted in data
	targetedRollback, err := um.getECURollbackSpecificToCar(man, vin)
	if err == nil && targetedRollback != nil {
		rollBackManifest = append(rollBackManifest, targetedRollback)
		return
	}

	return um.getRollbacksListOfPossible(man)
}

func (um *UpdateManifests) getECURollbackSpecificToCar(man *common.UpdateManifestECU, vin string) (rollbackManifest *common.UpdateManifestECU, err error) {
	conn := um.GetDBConn()
	targetCarECU := common.CarECU{}
	targetCarECU.VIN = vin
	targetCarECU.ECU = man.ECU
	// Using the ecu and vin, get the latest carECU for that specific car
	err = conn.Model(&targetCarECU).
		Where("vin = ?", targetCarECU.VIN).
		Where("ecu = ?", targetCarECU.ECU).
		Where("version != 'unavailable'"). // We have so many of these though, like its not something that you can even roll back to
		Order("epoch_usec DESC").
		Limit(1).
		Select()

	if err != nil {
		logger.Warn().Err(err).Msgf("failed to get car ecu for rollback for car %s  ecu %s", vin, man.ECU)
		return
	}

	if targetCarECU.Version == "" {
		logger.Warn().Interface("target car ecu", targetCarECU).Msgf("failed to fetch a previous ecu version for car %s ecu %s", vin, man.ECU)
		err = fmt.Errorf("getRollBackNew error")
		return
	}

	targetManifest := common.UpdateManifestECU{}
	err = conn.Model(&targetManifest).
		Where("version = ?", targetCarECU.Version).
		Where("ecu = ?", targetCarECU.ECU).
		Select()
	if err != nil {
		logger.Warn().Err(err).Msgf("failed to get ecu rollback update manifest ecu for car %s", vin)
		return
	}
	if targetManifest.ID == 0 {
		logger.Warn().Interface("target ecu", targetCarECU).Msgf("failed to get ecu rollback update manifest ecu for car %s", vin)
		err = fmt.Errorf("getRollBackNew error")
		return
	}
	return
}

func (um *UpdateManifests) getRollbacksListOfPossible(man *common.UpdateManifestECU) (rollBackManifest []*common.UpdateManifestECU, err error) {
	var manifests []*common.UpdateManifestECU
	conn := um.GetDBConn()
	sub := conn.Model(&manifests).ColumnExpr("version, max(created_at)").
		Where("ecu = ?", man.ECU).
		Where("hw_versions = ?", pg.Array(man.HWVersions)).
		Where("version != ?", man.Version).
		Group("version")
	err = conn.
		Model(&manifests).
		With("sub", sub).
		Column("update_manifest_ecu.version", "id").
		Join("INNER JOIN sub ON update_manifest_ecu.version = sub.version AND "+
			"update_manifest_ecu.created_at = sub.max").
		Where("ecu = ?", man.ECU).
		Where("hw_versions = ?", pg.Array(man.HWVersions)).
		Where("update_manifest_ecu.version != ?", man.Version).
		Relation("Files").
		Relation("Files.WriteRegion").
		Relation("Files.EraseRegion").
		Order("version").
		Select()
	if err != nil {
		errors.WithMessagef(err, "failed to do ECURollback() for manifest_id %d on ECU %s id: %d", man.UpdateManifestID, man.ECU, man.ID)
		return nil, err
	}

	return
}

func (um *UpdateManifests) manifestDelete(operation db.Operation, resultsTotal *ORMResults, manifest *common.UpdateManifest) (orm.Result, error) {
	// Removed manifest.ecuDelete as the deletes are now propagated by the database
	result, err := um.resultWithStack(operation.Model(manifest).WherePK().Delete())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}

	return resultsTotal, nil
}

func (um *UpdateManifests) manifestInsert(operation db.Operation, resultsTotal *ORMResults, manifest *common.UpdateManifest) (orm.Result, error) {
	result, err := um.resultWithStack(operation.Model(manifest).Insert())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}
	resultsTotal.SetModel(result.Model())

	if manifest.ECUs != nil {
		for i := range manifest.ECUs {
			ecu := manifest.ECUs[i]
			ecu.UpdateManifestID = manifest.ID
			result, err = um.ecuInsert(operation, resultsTotal, ecu)
			if um.hasErrorResult(resultsTotal, result, err) {
				return nil, err
			}
		}
	}

	return resultsTotal, nil
}

func (um *UpdateManifests) ecuDelete(operation db.Operation, resultsTotal *ORMResults, ecu *common.UpdateManifestECU) (orm.Result, error) {
	if ecu.Files != nil {
		for i := range ecu.Files {
			file := ecu.Files[i]
			result, err := um.fileDelete(operation, resultsTotal, file)
			if um.hasErrorResult(resultsTotal, result, err) {
				return nil, err
			}
		}
	}

	result, err := operation.Model(ecu).WherePK().Delete()
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}

	return resultsTotal, nil
}

func (um *UpdateManifests) ecuInsert(operation db.Operation, resultsTotal *ORMResults, ecu *common.UpdateManifestECU) (orm.Result, error) {
	result, err := um.resultWithStack(operation.Model(ecu).Insert())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}

	if ecu.Files != nil {
		for i := range ecu.Files {
			file := ecu.Files[i]
			file.UpdateManifestECUID = ecu.ID
			result, err := um.fileInsert(operation, resultsTotal, file)
			if um.hasErrorResult(resultsTotal, result, err) {
				return nil, err
			}
		}
	}

	return resultsTotal, nil
}

func (um *UpdateManifests) fileDelete(operation db.Operation, resultsTotal *ORMResults, file *common.UpdateManifestFile) (orm.Result, error) {
	result, err := um.resultWithStack(operation.Model(file).WherePK().Delete())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}
	resultsTotal.SetModel(result.Model())

	result, err = um.resultWithStack(operation.Model(&common.MemoryRegion{ID: file.WriteRegionID}).WherePK().Delete())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}

	if file.EraseRegionID != 0 {
		result, err = um.resultWithStack(operation.Model(&common.MemoryRegion{ID: file.EraseRegionID}).WherePK().Delete())
		if um.hasErrorResult(resultsTotal, result, err) {
			return nil, err
		}
	}

	result, err = um.fileKeyDelete(operation, resultsTotal, file)
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}

	return resultsTotal, nil
}

func (um *UpdateManifests) fileKeyDelete(operation db.Operation, resultsTotal *ORMResults, file *common.UpdateManifestFile) (orm.Result, error) {
	fk := &common.FileKey{FileID: file.FileID}
	result, err := um.resultWithStack(operation.Model(fk).WherePK().Delete())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}
	return resultsTotal, err
}

func (um *UpdateManifests) fileInsert(operation db.Operation, resultsTotal *ORMResults, file *common.UpdateManifestFile) (orm.Result, error) {
	result, err := um.resultWithStack(operation.Model(&file.WriteRegion).Insert())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}
	file.WriteRegionID = file.WriteRegion.ID

	if file.EraseRegion != nil {
		result, err = um.resultWithStack(operation.Model(file.EraseRegion).Insert())
		if um.hasErrorResult(resultsTotal, result, err) {
			return nil, err
		}
		file.EraseRegionID = file.EraseRegion.ID
	}

	result, err = um.resultWithStack(operation.Model(file).Insert())
	if um.hasErrorResult(resultsTotal, result, err) {
		return nil, err
	}
	resultsTotal.SetModel(result.Model())

	return resultsTotal, nil
}

func (um *UpdateManifests) selectFilter(query *orm.Query, filter *common.UpdateManifest) {
	if filter.ID > 0 {
		query.Where("id = ?", filter.ID)
	}

	if filter.Name != "" {
		query.Where("name = ?", filter.Name)
	}

	if filter.Version != "" {
		query.Where("version = ?", filter.Version)
	}

	if filter.Description != "" {
		query.Where("description LIKE ?", filter.Description)
	}

	if filter.ManifestType > 0 {
		query.Where("manifest_type = ?", filter.ManifestType)
	}

	if filter.Active != nil {
		query.Where("active = ?", filter.Active)
	}

	if filter.Country != "" {
		query.Where("country = ? or country is null", filter.Country)
	}

	if filter.PowerTrain != "" {
		query.Where("powertrain = ? or powertrain is null", filter.PowerTrain)
	}

	if filter.Restraint != "" {
		query.Where("restraint = ? or restraint is null", filter.Restraint)
	}

	if filter.Model != "" {
		query.Where("model = ? or model is null", filter.Model)
	}

	if filter.Trim != "" {
		query.Where("trim = ? or trim is null", filter.Trim)
	}

	if filter.Year != 0 {
		query.Where("year = ? or year is null", filter.Year)
	}

	if filter.BodyType != "" {
		query.Where("body_type = ? or body_type is null", filter.BodyType)
	}
}

func (um *UpdateManifests) searchFilter(query *orm.Query, filter *common.UpdateManifestSearch) {
	if filter.Search != "" {
		query.Where("document @@ plainto_tsquery(?)", filter.Search)
	}

	if filter.ManifestType == common.SoftwareUpdateType {
		filter.ManifestType = 0
		query.WhereInMulti("manifest_type in (?)", common.SoftwareUpdateType, common.MagnaManifestUpdateType, common.AftersalesUpdateType)
	}

	um.selectFilter(query, &filter.UpdateManifest)
}

func (um *UpdateManifests) AddSUMSVersion(manifest *common.UpdateManifest) (orm.Result, error) {
	return um.resultWithStack(um.GetDBConn().Model(manifest).Column("sums").Where("id = ?id AND sums IS NULL").Update())
}

// Given a flashpack version number: 14.4, we will return the corresponding flashpack
func (um *UpdateManifests) SelectFlashPackByVersion(versionNumber string) (manifest common.UpdateManifest, err error) {
	// Prepare for Like Statement
	versionNumber = "%" + versionNumber
	err = um.GetDBConn().Model(&manifest).Where("manifest_type = ? AND name LIKE ?", common.AsBuiltUpdateType, versionNumber).Limit(1).Select()
	return
}
