package repo

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/tidbcloud/aws-shared-provider/pkg/auditlog/entity"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type AuditLogScheduleRepo interface {
	List(ctx context.Context, options *entity.ListAuditLogScheduleOptions) ([]*entity.AuditLogSchedule, error)
	// DUPLICATE will not throw err
	Create(ctx context.Context, schedule *entity.AuditLogSchedule) error
	Save(ctx context.Context, schedule *entity.AuditLogSchedule) error
	SoftDelete(ctx context.Context, schedule *entity.AuditLogSchedule) error
	Disable(ctx context.Context, schedule *entity.AuditLogSchedule) error
	UpdateDeleteTime(ctx context.Context, clusterId string, deleteTime *time.Time) error
	UpdateDisableTime(ctx context.Context, clusterId string, disableTime *time.Time) error
	Enable(ctx context.Context, schedule *entity.AuditLogSchedule) error
	// record not find will not throw err but return nil schedule
	GetByClusterId(ctx context.Context, clusterId string) (*entity.AuditLogSchedule, error)
}

type auditLogScheduleRepoImpl struct {
	dbProvider db.Provider
}

func NewAuditLogScheduleRepo(dbProvider db.Provider) AuditLogScheduleRepo {
	return &auditLogScheduleRepoImpl{
		dbProvider: dbProvider,
	}
}

func (s *auditLogScheduleRepoImpl) List(ctx context.Context,
	opts *entity.ListAuditLogScheduleOptions) (schedules []*entity.AuditLogSchedule, err error) {
	rdb := s.dbProvider.ReadDB(ctx)
	rdb = buildAuditLogScheduleFilter(opts, rdb)
	if err := rdb.Find(&schedules).Error; err != nil {
		return nil, err
	}
	return schedules, nil
}

func buildAuditLogScheduleFilter(opts *entity.ListAuditLogScheduleOptions, rdb *gorm.DB) *gorm.DB {
	if opts == nil {
		return rdb
	}
	if opts.State != "" {
		rdb = rdb.Where("state = ?", opts.State)
	}
	return rdb
}

func (s *auditLogScheduleRepoImpl) Create(ctx context.Context, schedule *entity.AuditLogSchedule) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Clauses(clause.OnConflict{DoNothing: true}).Create(schedule).Error
}

func (s *auditLogScheduleRepoImpl) Save(ctx context.Context, schedule *entity.AuditLogSchedule) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Save(schedule).Error
}

func (s *auditLogScheduleRepoImpl) SoftDelete(ctx context.Context, schedule *entity.AuditLogSchedule) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Model(schedule).Where("delete_time IS NOT NULL").
		Update("state", entity.AuditLogScheduleStateDeleted).Error
}

func (s *auditLogScheduleRepoImpl) Disable(ctx context.Context, schedule *entity.AuditLogSchedule) error {
	wdb := s.dbProvider.WriteDB(ctx)
	// must check disable_time is not null to avoid race. See Enable.
	return wdb.Model(schedule).Where("disable_time IS NOT NULL").
		Update("state", entity.AuditLogScheduleStateDisabled).Error
}

func (s *auditLogScheduleRepoImpl) Enable(ctx context.Context, schedule *entity.AuditLogSchedule) error {
	wdb := s.dbProvider.WriteDB(ctx)
	// must set disable_time to null to avoid race. See Disable.
	schedule.ResetRotationTime()
	return wdb.Model(schedule).
		Update("state", entity.AuditLogScheduleStateEnabled).
		Update("disable_time", nil).
		Update("rotation_time", schedule.RotationTime).Error
}

func (s *auditLogScheduleRepoImpl) UpdateDeleteTime(ctx context.Context, clusterId string,
	deleteTime *time.Time) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Model(&entity.AuditLogSchedule{}).Where("cluster_id = ?", clusterId).
		Update("delete_time", deleteTime).Error
}

func (s *auditLogScheduleRepoImpl) UpdateDisableTime(ctx context.Context, clusterId string,
	disableTime *time.Time) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Model(&entity.AuditLogSchedule{}).Where("cluster_id = ?", clusterId).
		Update("disable_time", disableTime).Error
}

func (s *auditLogScheduleRepoImpl) GetByClusterId(ctx context.Context,
	clusterId string) (*entity.AuditLogSchedule, error) {
	rdb := s.dbProvider.ReadDB(ctx)
	schedule := &entity.AuditLogSchedule{}
	if err := rdb.Where("cluster_id = ?", clusterId).First(schedule).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return schedule, nil
}

type AuditLogTaskRepo interface {
	List(ctx context.Context, options *entity.ListAuditLogTaskOptions) ([]*entity.AuditLogTask, error)
	Create(ctx context.Context, task *entity.AuditLogTask) error
	// BatchUpdateState batch update tasks state from pending or failed to running
	BatchUpdateState(ctx context.Context, state entity.AuditLogTaskState, taskIds []string) error
	// UpdateRunningState update running task state to failed,succeed or corrupted
	UpdateRunningState(ctx context.Context, task *entity.AuditLogTask) error
	// BatchFailed batch update running task state to failed
	BatchFailed(ctx context.Context, taskIds []string, errMsg string) (int64, error)
	// delete outdated succeed tasks, default to 365 days
	Clean(ctx context.Context, before *time.Time) (int64, error)
}

type auditLogTaskRepoImpl struct {
	dbProvider db.Provider
}

func NewAuditLogTaskRepo(dbProvider db.Provider) AuditLogTaskRepo {
	return &auditLogTaskRepoImpl{
		dbProvider: dbProvider,
	}
}

func (a *auditLogTaskRepoImpl) List(ctx context.Context,
	opts *entity.ListAuditLogTaskOptions) (tasks []*entity.AuditLogTask, err error) {
	rdb := a.dbProvider.ReadDB(ctx)
	rdb = buildAuditLogTaskFilter(opts, rdb)
	if err := rdb.Find(&tasks).Error; err != nil {
		return nil, err
	}
	return tasks, nil
}

func buildAuditLogTaskFilter(opts *entity.ListAuditLogTaskOptions, rdb *gorm.DB) *gorm.DB {
	if opts == nil {
		return rdb
	}
	if len(opts.States) > 0 {
		rdb = rdb.Where("state in ?", opts.States)
	}
	if len(opts.TaskIds) > 0 {
		rdb = rdb.Where("task_id in ?", opts.TaskIds)
	}
	return rdb
}

func (s *auditLogTaskRepoImpl) Create(ctx context.Context, task *entity.AuditLogTask) error {
	wdb := s.dbProvider.WriteDB(ctx)
	return wdb.Create(task).Error
}

func (s *auditLogTaskRepoImpl) BatchUpdateState(ctx context.Context,
	state entity.AuditLogTaskState, taskIds []string) error {
	wdb := s.dbProvider.WriteDB(ctx)
	states := []entity.AuditLogTaskState{entity.AuditLogTaskStatePending, entity.AuditLogTaskStateFailed}
	result := wdb.Model(&entity.AuditLogTask{}).Where("task_id in ?", taskIds).
		Where("state in ?", states).
		Update("state", state)

	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected != int64(len(taskIds)) {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *auditLogTaskRepoImpl) UpdateRunningState(ctx context.Context, task *entity.AuditLogTask) error {
	wdb := s.dbProvider.WriteDB(ctx)
	updateColumns := map[string]interface{}{
		"state":         task.State,
		"error_msg":     task.ErrorMsg,
		"execute_count": task.ExecuteCount,
	}
	if task.CloudStorageType != "" {
		updateColumns["cloud_storage_type"] = task.CloudStorageType
	}
	if task.CloudStorage != nil {
		cloudStorageJson, err := json.Marshal(*task.CloudStorage)
		if err != nil {
			return errors.New("failed to marshal cloud storage")
		}
		updateColumns["cloud_storage"] = cloudStorageJson
	}
	result := wdb.Model(task).
		Where("task_id = ? and state = ?", task.TaskId, entity.AuditLogTaskStateRunning).
		Updates(updateColumns)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *auditLogTaskRepoImpl) BatchFailed(ctx context.Context, taskIds []string, errMsg string) (int64, error) {
	wdb := s.dbProvider.WriteDB(ctx)
	result := wdb.Model(&entity.AuditLogTask{}).Where("task_id in ?", taskIds).
		Where("state = ?", entity.AuditLogTaskStateRunning).
		Updates(map[string]interface{}{
			"state":         entity.AuditLogTaskStateFailed,
			"error_msg":     errMsg,
			"execute_count": gorm.Expr("execute_count + 1"),
		})
	return result.RowsAffected, result.Error
}

func (s *auditLogTaskRepoImpl) Clean(ctx context.Context, before *time.Time) (int64, error) {
	wdb := s.dbProvider.WriteDB(ctx)
	var cutoff time.Time
	if before != nil {
		cutoff = *before
	} else {
		cutoff = time.Now().AddDate(0, 0, -365)
	}
	result := wdb.Where("state = ? and created_at < ?", entity.AuditLogTaskStateSucceeded, cutoff).Delete(&entity.AuditLogTask{})
	if result.Error != nil {
		return 0, result.Error
	}
	return result.RowsAffected, nil
}
