package queries

import (
	"fmt"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
	"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"
)

var FINAL_UPDATE_STATUS = carupdatestatus.FINAL_UPDATE_STATUS

type CarUpdatesInterface interface {
	Count(filter *common.CarUpdate) (int, error)
	Delete(update *common.CarUpdate) (orm.Result, error)
	CountUpdateStatuses(carupdateid int64) (int, error)
	GetUpdateStatuses(carupdateid int64, paging *PageQueryOptions) ([]common.CarUpdateStatus, error)
	TruncateRequirementsAwaitForUpdate(carupdateid int64) (orm.Result, error)
	Insert(update *common.CarUpdate) (orm.Result, error)
	InsertAndCreateStatus(update *common.CarUpdate) (orm.Result, error) // Insert a car update and create the appropriate update_status entry
	Load(update *common.CarUpdate) error
	LogStatus(update *common.CarUpdate) (orm.Result, error)
	SelectByID(id int64) (*common.CarUpdate, error)
	SelectByManifestID(int64) ([]common.CarUpdate, error)
	SelectByVIN(vin string) ([]common.CarUpdate, error)
	SelectMostRecentByVINs(vins []string) ([]common.CarUpdate, error)
	SelectOrInsert(update *common.CarUpdate) (bool, error)
	Select(filter *common.CarUpdate, paging *PageQueryOptions) ([]common.CarUpdate, error)
	UpdateStatus(update *common.CarUpdate) (orm.Result, error)
	UpdateStatusIfNotRepeat(update *common.CarUpdate) (orm.Result, error)
	GetManifest(carupdateid int64) (*common.UpdateManifest, error)
	HasPendingUpdates(manifestID int64, vin string) (bool, error)
	HasPendingUpdatesFromAftersalesUser(manifestID int64, vin string) (updateID int64, pendingUpdateAftersales bool, err error) // Cancel any pending aftersales update
	InsertMissingFlashpack(vin string, flashpackVersion string) error // Flashpack Version = OS Version Number
}

// CarUpdate query methods
type CarUpdates struct {
	QueryBase
}

func (c *CarUpdates) selectFilter(query *orm.Query, filter *common.CarUpdate) {
	if filter.ID > 0 {
		query.Where("car_update.id = ?", filter.ID)
	}

	if filter.VIN != "" {
		query.Where("vin = ?", filter.VIN)
	}

	if filter.UpdateManifestID > 0 {
		query.Where("update_manifest_id = ?", filter.UpdateManifestID)
	}

	if filter.Status != "" {
		query.Where("status = ?", filter.Status)
	}

	if filter.UpdateSource != "" {
		query.Where("update_source = ?", filter.UpdateSource)
	}
}

// StatusHistoryFilter get CarUpdates with certain status' filtered out.
// When a status is filtered out, CarUpdates with that status will still
// be returned, but will contain their last status not ignored.
func StatusHistoryFilter(query *orm.Query, ignoreStatuses []string) {
	query.ColumnExpr("car_update.id")
	query.ColumnExpr("car_update.vin")
	query.ColumnExpr("car_update.created_at")
	query.ColumnExpr("car_update.updated_at")
	query.ColumnExpr("car_update.update_manifest_id")
	query.ColumnExpr("car_update.error_code")
	query.ColumnExpr("car_update.info")
	query.ColumnExpr("car_update.username")
	query.ColumnExpr("car_update_display_status.display_status AS status")
	query.Join(`LEFT JOIN car_update_display_status ON car_update.id = car_update_display_status.car_update_id`)
}

// CarUpdate select by car update id
func (c *CarUpdates) SelectByID(id int64) (*common.CarUpdate, error) {
	update := common.CarUpdate{ID: id}

	err := c.GetDBConn().Model(&update).WherePK().
		Relation("UpdateManifest").
		Select()

	return &update, errors.WithStack(err)
}

// SelectByVIN returns list of car updates by VIN
func (c *CarUpdates) SelectByVIN(vin string) ([]common.CarUpdate, error) {
	updates := []common.CarUpdate{}

	err := c.GetDBConn().Model(&updates).Where("vin = ?", vin).
		Relation("UpdateManifest").
		Select()

	return updates, errors.WithStack(err)
}

// SelectMostRecentByVINs returns a list of most recent car update for VINs
func (c *CarUpdates) SelectMostRecentByVINs(vins []string) ([]common.CarUpdate, error) {
	updates := []common.CarUpdate{}

	err := c.GetDBConn().Model(&updates).
		Where("vin IN (?)", pg.In(vins)).
		WhereGroup(func(q *pg.Query) (*pg.Query, error) {
			return q.Where("(car_update.created_at = (SELECT MAX(t2.created_at) FROM public.car_updates t2 WHERE t2.vin = car_update.vin))"), nil
		}).
		Relation("UpdateManifest").
		Select()

	return updates, errors.WithStack(err)
}

// SelectByManifestID returns list of car updates by package update id
func (c *CarUpdates) SelectByManifestID(manifest_id int64) ([]common.CarUpdate, error) {
	updates := []common.CarUpdate{}

	err := c.GetDBConn().Model(&updates).Where("update_manifest.id = ?", manifest_id).
		Relation("UpdateManifest").
		Select()

	return updates, errors.WithStack(err)
}

func (c *CarUpdates) SelectOrInsert(update *common.CarUpdate) (bool, error) {
	return c.insertSelectWithStack(c.GetDBConn().Model(update).Where("vin = ?vin AND update_manifest_id = ?update_manifest_id").SelectOrInsert())
}

// Select returns list of cars
func (c *CarUpdates) Select(filter *common.CarUpdate, paging *PageQueryOptions) ([]common.CarUpdate, error) {
	ups := []common.CarUpdate{}
	query := c.GetDBConn().Model(&ups)
	c.selectFilter(query, filter)
	StatusHistoryFilter(query, []string{"cleanup_succeeded"})
	c.pageQuery(query, paging)

	if filter.UpdateManifest != nil && filter.UpdateManifest.ManifestType > 0 {
		query.
			Relation("UpdateManifest", func(q *orm.Query) (*orm.Query, error) {
				return q.Where("manifest_type = ?", filter.UpdateManifest.ManifestType), nil
			})
	} else {
		query.Relation("UpdateManifest")
	}

	err := query.Select()

	return ups, errors.WithStack(err)
}

func (c *CarUpdates) Delete(update *common.CarUpdate) (orm.Result, error) {
	err := validator.ValidateIDField(update.ID)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	conn := c.GetDBConn()
	tx, err := conn.Begin()
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer func() {
		if err != nil {
			tx.Rollback()
		}
		tx.Close()
	}()

	_, err = tx.Model(&common.CarUpdateStatus{CarUpdateID: update.ID}).Where("car_update_id = ?car_update_id").Delete()
	if err != nil {
		return nil, errors.WithStack(err)
	}

	result, err := tx.Model(update).WherePK().Delete()
	if err != nil {
		return nil, errors.WithStack(err)
	}

	err = tx.Commit()

	return result, errors.WithStack(err)
}

func (c *CarUpdates) UpdateStatus(update *common.CarUpdate) (orm.Result, error) {
	result, err := c.UpdateCarUpdate(update)
	if err != nil {
		return result, err
	}

	_, err = c.LogStatus(update)

	return result, err
}

func (c *CarUpdates) UpdateStatusIfNotRepeat(update *common.CarUpdate) (orm.Result, error) {
	orm, err := c.LogStatusIfNotARepeat(update)
	if err != nil {
		return orm, err
	}
	result, err := c.UpdateCarUpdate(update)
	if err != nil {
		err = errors.WithStack(err)
	}
	return result, err
}

func (c *CarUpdates) UpdateCarUpdate(update *common.CarUpdate) (orm.Result, error) {
	err := validator.ValidateIDField(update.ID)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return c.resultWithStack(c.GetDBConn().Model(update).Column("status", "error_code").WherePK().Update())
}

func (c *CarUpdates) LogStatus(update *common.CarUpdate) (orm.Result, error) {
	// Should this also be updated to have the catch of double statuses
	status := common.CarUpdateStatus{
		CarUpdateID: update.ID,
		Status:      update.Status,
		ErrorCode:   update.ErrorCode,
		Info:        update.Info,
	}

	result, err := c.resultWithStack(c.GetDBConn().Model(&status).Insert())
	if err != nil {
		err = errors.WithStack(fmt.Errorf("LogStatus::%s. with status %d %s %d %s", err.Error(), status.CarUpdateID, status.Status, status.ErrorCode, status.Info))
		logger.Err(err)

	}
	return result, err
}

// If the last status that was inserted is the same as the one we are trying to insert, we do not do it
var RepeatedStatus = errors.New("RepeatedStatus")

func (c *CarUpdates) LogStatusIfNotARepeat(update *common.CarUpdate) (orm orm.Result, err error) {
	logger.Debug().Msgf("attempt to add new status %s for update %d", update.Status, update.ID) // CEC-5650 debugging
	status := common.CarUpdateStatus{
		CarUpdateID: update.ID,
		Status:      update.Status,
		ErrorCode:   update.ErrorCode,
		Info:        update.Info,
	}

	// This query insert's our status if the most recent status on the car_update_id does not match the new status
	// If it does match the new status, then we do not insert
	// 1: Update ID, 2: Status, 3: error_code, 4: The info, 5: UpdateID, 6: The status
	query := `INSERT INTO public.car_update_statuses(
		car_update_id, status, error_code, info)
		SELECT ?, ?, ?, ? WHERE NOT EXISTS(
		SELECT 1 FROM (SELECT id, car_update_id, status, error_code, created_at, updated_at, info
		FROM public.car_update_statuses WHERE car_update_id = ? ORDER BY created_at DESC LIMIT 1) AS temp WHERE status = ?)`
	orm, err = c.GetDBConn().Exec(query, status.CarUpdateID, status.Status, status.ErrorCode, status.Info, status.CarUpdateID, status.Status)
	if err != nil {
		err = errors.WithStack(fmt.Errorf("%s. with status %d %s %d %s", err.Error(), status.CarUpdateID, status.Status, status.ErrorCode, status.Info))
		logger.Err(err)
		return orm, err
	}
	if orm.RowsAffected() == 0 {
		return orm, RepeatedStatus
	}
	return
}

func (c *CarUpdates) Insert(update *common.CarUpdate) (orm.Result, error) {
	return c.insert(update)
}

func (c *CarUpdates) InsertAndCreateStatus(update *common.CarUpdate) (res orm.Result, err error) {
	res, err = c.insert(update)
	if err != nil {
		return
	}
	return c.LogStatus(update)
}

func (c *CarUpdates) Load(update *common.CarUpdate) error {
	query := c.GetDBConn().Model(update)

	if update.ID > 0 {
		query.WherePK()
	} else if update.VIN == "" && update.UpdateManifestID > 0 {
		query.Where("vin = ?vin AND update_manifest_id = ?update_manifest_id")
	} else {
		return errors.New("no id, vin, update_manifest_id")
	}

	if update.UpdateManifest != nil && update.UpdateManifest.ManifestType > 0 {
		query.
			Relation("UpdateManifest", func(q *orm.Query) (*orm.Query, error) {
				return q.Where("manifest_type = ?", update.UpdateManifest.ManifestType), nil
			})
	} else {
		query.Relation("UpdateManifest")
	}

	return errors.WithStack(query.
		Relation("UpdateManifest.ECUs").
		Relation("UpdateManifest.ECUs.Files").
		Relation("UpdateManifest.ECUs.Files.WriteRegion").
		Relation("UpdateManifest.ECUs.Files.EraseRegion").
		Select())
}

func (c *CarUpdates) Count(filter *common.CarUpdate) (int, error) {
	query := c.GetDBConn().Model((*common.CarUpdate)(nil))

	c.selectFilter(query, filter)

	return c.countWithStack(query.Count())
}

func (c *CarUpdates) CountUpdateStatuses(carupdateid int64) (int, error) {
	query := c.GetDBConn().Model((*common.CarUpdateStatus)(nil))

	return c.countWithStack(query.Where("car_update_id = ?", carupdateid).Count())
}

func (c *CarUpdates) GetUpdateStatuses(carupdateid int64, paging *PageQueryOptions) ([]common.CarUpdateStatus, error) {
	result := []common.CarUpdateStatus{}
	query := c.GetDBConn().Model(&result)

	c.pageQuery(query, paging)

	err := query.Where("car_update_id = ?", carupdateid).Select()
	if err == nil && len(result) == 0 {
		return result, errors.WithStack(pg.ErrNoRows)
	}

	return result, errors.WithStack(err)
}

func (c *CarUpdates) TruncateRequirementsAwaitForUpdate(carupdateid int64) (orm.Result, error) {
	queryString := `with row_nums as (select *, row_number() over (partition by car_update_id order by updated_at) as row 
	from car_update_statuses where status  = 'requirements_await'
	and car_update_id = ?)
	delete from car_update_statuses where id in (
	select id from row_nums where (row != (select max(row) from row_nums)) and (row != 1)
	)`

	err := validator.ValidateIDField(carupdateid)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	conn := c.GetDBConn()
	tx, err := conn.Begin()
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer func() {
		if err != nil {
			tx.Rollback()
		}
		tx.Close()
	}()

	result, err := conn.Query(&common.CarUpdateStatus{}, queryString, carupdateid)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	err = tx.Commit()

	return result, errors.WithStack(err)
}

func (c *CarUpdates) GetManifest(carupdateid int64) (*common.UpdateManifest, error) {
	manifest := common.UpdateManifest{}

	err := c.GetDBConn().Model(&manifest).Join("JOIN car_updates").JoinOn("car_updates.update_manifest_id = update_manifest.id").JoinOn("car_updates.id = ?", carupdateid).Relation("ECUs").Relation("ECUs.Files").Select()

	return &manifest, errors.WithStack(err)
}

func (c *CarUpdates) HasPendingUpdates(manifestID int64, vin string) (bool, error) {
	count, err := c.GetDBConn().Model(&common.CarUpdate{}).Where("update_manifest_id = ? AND vin = ? AND status NOT IN (?)", manifestID, vin, pg.In(FINAL_UPDATE_STATUS)).Count()
	if err != nil {
		return false, err
	}

	return count > 0, nil
}

// An updateID of 0 means no pending update
func (c *CarUpdates) HasPendingUpdatesFromAftersalesUser(manifestID int64, vin string) (updateID int64, pendingUpdateAftersales bool, err error) {
	var carUpdates []common.CarUpdate
	err = c.GetDBConn().Model(&carUpdates).Where("update_manifest_id = ? AND vin = ? AND status NOT IN (?)", manifestID, vin, pg.In(FINAL_UPDATE_STATUS)).Select()
	if err != nil {
		return
	}
	if len(carUpdates) == 0 {
		return
	}

	if len(carUpdates) > 1 {
		logger.Error().Msgf("Have more than one pending update for manifestID %d on vin %s. This should not happen and our logic needs to change", manifestID, vin)
	}
	// This should only ever be one
	for _, u := range carUpdates {
		updateID = u.ID
		if u.UpdateSource == common.UPDATE_SOURCE_AFTERSALES {
			pendingUpdateAftersales = true
			break
		}
	}
	return
}

func (c *CarUpdates) InsertMissingFlashpack(vin string, flashpackVersion string) (err error) {
	mf := common.MissingFlashpack{
		VIN:              vin,
		FlashPackVersion: flashpackVersion,
	}
	_, err = c.insert(&mf)
	return
}
