package repository

import (
	"context"
	"fmt"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/pkgv2/db"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/infra/repository/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type MaintenanceWindowRepoImpl struct {
	dbProvider db.Provider
}

func NewMaintenanceWindowRepo(dbProvider db.Provider) repository.MaintenanceWindowRepo {
	return &MaintenanceWindowRepoImpl{
		dbProvider: dbProvider,
	}
}

func (m *MaintenanceWindowRepoImpl) Save(ctx context.Context, en *entity.MaintenanceWindow) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "maintenanceWindowRepoImp.Save")
	defer span.End()

	wdb := m.dbProvider.WriteDB(ctx)
	model := cvtMaintenanceWindowEnToModel(en)

	if err := wdb.Save(model).Error; err != nil {
		return bizerr.WrapGormError(err, "save maintenance window failed")
	}

	en.ID = uint64(model.ID)

	return nil
}

func (m *MaintenanceWindowRepoImpl) LoadByProjectID(
	ctx context.Context,
	projectID uint64,
) (*entity.MaintenanceWindow, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "maintenanceWindowRepoImp.LoadByProjectID")
	defer span.End()

	rdb := m.dbProvider.ReadDB(ctx)
	var model schema.MaintenanceWindow
	if err := rdb.Where("project_id = ?", projectID).First(&model).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load maintenance window failed")
	}

	return cvtMaintenanceWindowModelToEn(&model), nil
}

type MaintenanceTaskRepoImpl struct {
	dbProvider db.Provider
}

func NewMaintenanceTaskRepo(dbProvider db.Provider) repository.MaintenanceTaskRepo {
	return &MaintenanceTaskRepoImpl{
		dbProvider: dbProvider,
	}
}

func (m *MaintenanceTaskRepoImpl) Save(ctx context.Context, maintenanceTask *entity.MaintenanceTask) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "maintenanceTaskRepoImp.Save")
	defer span.End()

	wdb := m.dbProvider.WriteDB(ctx)
	model := cvtMaintenanceTaskEnToModel(maintenanceTask)

	// To avoid overwriting created_at with nil, use Updates for update and Create for insert
	if model.ID == 0 {
		if err := wdb.Create(model).Error; err != nil {
			return bizerr.WrapGormError(err, "save maintenance task failed")
		}
	} else {
		if err := wdb.Omit("created_at").Save(model).Error; err != nil {
			return bizerr.WrapGormError(err, "save maintenance task failed")
		}
	}

	maintenanceTask.MaintenanceTaskId = uint64(model.ID)

	return nil
}

func (m *MaintenanceTaskRepoImpl) Query(
	ctx context.Context,
	filter *tidbmgmtservicev1.MaintenanceTaskFilter,
	timeRangeStartInclusive, timeRangeEndExclusive, applyTimeEqual *time.Time,
	excludeNotificationPhases []entity.NotificationScene,
	pager *base.Pager,
	sorters []*base.Sorter,
) ([]*entity.MaintenanceTask, int64, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "maintenanceTaskRepoImp.Query")
	defer span.End()

	var maintenanceTasks []*schema.MaintenanceTask
	tx := m.dbProvider.ReadDB(ctx).Model(&schema.MaintenanceTask{})

	if len(filter.GetMaintenanceTaskIds()) != 0 {
		tx = tx.Where("id in (?)", filter.GetMaintenanceTaskIds())
	}
	if len(filter.GetProjectIds()) != 0 {
		tx = tx.Where("project_id in (?)", filter.GetProjectIds())
	}
	if len(filter.GetStates()) != 0 {
		statuses := lo.Map(filter.GetStates(), func(each tidbmgmtservicev1.MaintenanceTask_State, _ int) string {
			return cvtMaintenanceTaskStatusEnToModel(each)
		})
		tx = tx.Where("status in (?)", statuses)
	}
	if timeRangeStartInclusive != nil {
		tx = tx.Where("scheduled_apply_time >= ?", timeRangeStartInclusive)
	}
	if timeRangeEndExclusive != nil {
		tx = tx.Where("scheduled_apply_time < ?", timeRangeEndExclusive)
	}
	if applyTimeEqual != nil {
		tx = tx.Where("scheduled_apply_time = ?", applyTimeEqual)
	}
	if len(excludeNotificationPhases) != 0 {
		phases := lo.Map(excludeNotificationPhases, func(each entity.NotificationScene, _ int) string {
			return cvtMaintenanceTaskNotificationPhaseEnToModel(each)
		})
		tx = tx.Where("notification_phase not in (?)", phases)
	}

	var count int64
	if err := tx.Count(&count).Error; err != nil {
		return nil, 0, bizerr.WrapGormError(err, "count maintenance task")
	}

	if pager != nil {
		offset := (pager.Page - 1) * pager.PageSize
		limit := pager.PageSize
		tx = tx.Offset(int(offset)).Limit(int(limit))
	}

	for _, sorter := range sorters {
		desc := true
		if sorter.Sort == base.Sort_SORT_ASC {
			desc = false
		}
		tx = tx.Order(clause.OrderByColumn{Column: clause.Column{Name: sorter.OrderBy}, Desc: desc})
	}

	if err := tx.Find(&maintenanceTasks).Error; err != nil {
		return nil, 0, bizerr.WrapGormError(err, "query maintenance task")
	}

	entities := lo.Map(maintenanceTasks, func(each *schema.MaintenanceTask, _ int) *entity.MaintenanceTask {
		return cvtMaintenanceTaskModelToEn(each)
	})

	return entities, count, nil
}

func (m *MaintenanceTaskRepoImpl) Load(ctx context.Context, id uint64) (*entity.MaintenanceTask, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "maintenanceTaskRepoImp.Load")
	defer span.End()

	rdb := m.dbProvider.ReadDB(ctx)

	var maintenanceTask schema.MaintenanceTask
	if err := rdb.Where("id = ?", id).First(&maintenanceTask).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load maintenance task")
	}

	return cvtMaintenanceTaskModelToEn(&maintenanceTask), nil
}

type MaintenanceTaskExecutionRepoImpl struct {
	dbProvider db.Provider
}

func NewMaintenanceTaskExecutionRepo(dbProvider db.Provider) repository.MaintenanceTaskExecutionRepo {
	return &MaintenanceTaskExecutionRepoImpl{
		dbProvider: dbProvider,
	}
}

func (m *MaintenanceTaskExecutionRepoImpl) Load(
	ctx context.Context,
	id uint64,
) (*entity.MaintenanceTaskExecution, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "maintenanceTaskExecutionRepoImp.Load")
	defer span.End()

	rdb := m.dbProvider.ReadDB(ctx)

	var maintenanceTaskExecution schema.MaintenanceTaskExecution
	if err := rdb.Where("id = ?", id).First(&maintenanceTaskExecution).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load maintenance task execution")
	}

	return cvtMaintenanceTaskExecutionModelToEn(&maintenanceTaskExecution), nil
}

func (m *MaintenanceTaskExecutionRepoImpl) Query(
	ctx context.Context,
	filter *tidbmgmtservicev1.QueryMaintenanceTaskExecutionsRequest_Filter,
	cancellationReasons []tidbmgmtservicev1.MaintenanceTaskExecution_CancellationReason,
) ([]*entity.MaintenanceTaskExecution, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "maintenanceTaskExecutionRepoImp.Query")
	defer span.End()

	tx := m.dbProvider.ReadDB(ctx)

	if len(filter.GetMaintenanceTaskExecutionIds()) != 0 {
		tx = tx.Where("id in (?)", filter.GetMaintenanceTaskExecutionIds())
	}
	if len(filter.GetClusterIds()) != 0 {
		tx = tx.Where("cluster_id in (?)", filter.GetClusterIds())
	}
	if len(filter.GetMaintenanceTaskIds()) != 0 {
		tx = tx.Where("maintenance_task_id in (?)", filter.GetMaintenanceTaskIds())
	}
	if len(filter.GetStates()) != 0 {
		statuses := lo.Map(
			filter.GetStates(),
			func(each tidbmgmtservicev1.MaintenanceTaskExecution_State, _ int) string {
				return cvtMaintenanceTaskExecutionStatusEnToModel(each)
			},
		)
		tx = tx.Where("status in (?)", statuses)
	}
	if len(cancellationReasons) != 0 {
		reasons := lo.Map(
			cancellationReasons,
			func(each tidbmgmtservicev1.MaintenanceTaskExecution_CancellationReason, _ int) string {
				return cvtMaintenanceTaskExecutionCancellationReasonEnToModel(each)
			},
		)
		tx = tx.Where("cancellation_reason in (?)", reasons)
	}

	var maintenanceTaskExecutions []*schema.MaintenanceTaskExecution

	if err := tx.Find(&maintenanceTaskExecutions).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "query maintenance task execution")
	}

	entities := lo.Map(
		maintenanceTaskExecutions,
		func(each *schema.MaintenanceTaskExecution, _ int) *entity.MaintenanceTaskExecution {
			return cvtMaintenanceTaskExecutionModelToEn(each)
		},
	)

	return entities, nil
}

func (m *MaintenanceTaskExecutionRepoImpl) Save(
	ctx context.Context,
	maintenanceTaskExecution *entity.MaintenanceTaskExecution,
) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "maintenanceTaskExecutionRepoImp.Save")
	defer span.End()

	wdb := m.dbProvider.WriteDB(ctx)

	maintenanceTaskExecutionModel := cvtMaintenanceTaskExecutionEnToModel(maintenanceTaskExecution)

	// To avoid overwriting created_at with nil, use Updates for update and Create for insert
	if maintenanceTaskExecutionModel.ID == 0 {
		if err := wdb.Create(maintenanceTaskExecutionModel).Error; err != nil {
			return bizerr.WrapGormError(err, "save maintenance task execution")
		}
	} else {
		if err := wdb.Omit("created_at").Save(maintenanceTaskExecutionModel).Error; err != nil {
			return bizerr.WrapGormError(err, "save maintenance task execution")
		}
	}

	maintenanceTaskExecution.MaintenanceTaskExecutionId = uint64(maintenanceTaskExecutionModel.ID)

	return nil
}

func cvtMaintenanceWindowEnToModel(en *entity.MaintenanceWindow) *schema.MaintenanceWindow {
	return &schema.MaintenanceWindow{
		Model: gorm.Model{
			ID: uint(en.ID),
		},
		ProjectID:    en.GetProjectId(),
		DayOfWeek:    en.GetWeekDay(),
		HourOfDay:    en.GetDayHour(),
		MinuteOfHour: en.GetHourMinute(),
	}
}

func cvtMaintenanceWindowModelToEn(m *schema.MaintenanceWindow) *entity.MaintenanceWindow {
	return &entity.MaintenanceWindow{
		MaintenanceWindow: tidbmgmtservicev1.MaintenanceWindow{
			ProjectId:  m.ProjectID,
			WeekDay:    m.DayOfWeek,
			DayHour:    m.HourOfDay,
			HourMinute: m.MinuteOfHour,
		},
		ID: uint64(m.ID),
	}
}

func cvtMaintenanceTaskEnToModel(en *entity.MaintenanceTask) *schema.MaintenanceTask {
	var (
		deadline   *time.Time
		createTime time.Time
	)
	if en.GetDeadline() != nil {
		deadline = lo.ToPtr(time.Unix(en.GetDeadline().GetSeconds(), 0))
	}
	if en.GetCreateTime() != nil {
		createTime = time.Unix(en.GetCreateTime().GetSeconds(), 0)
	}

	return &schema.MaintenanceTask{
		Model: gorm.Model{
			ID:        uint(en.GetMaintenanceTaskId()),
			CreatedAt: createTime,
		},
		ProjectID:          en.GetProjectId(),
		Description:        en.GetDescription(),
		Status:             cvtMaintenanceTaskStatusEnToModel(en.GetState()),
		ScheduledApplyTime: time.Unix(en.GetScheduledApplyTime().GetSeconds(), 0),
		Deadline:           deadline,
		NotificationPhase:  cvtMaintenanceTaskNotificationPhaseEnToModel(en.NotificationPhase),
	}
}

func cvtMaintenanceTaskModelToEn(m *schema.MaintenanceTask) *entity.MaintenanceTask {
	var deadline *timestamppb.Timestamp
	if m.Deadline != nil {
		deadline = timestamppb.New(*m.Deadline)
	}

	return &entity.MaintenanceTask{
		MaintenanceTask: tidbmgmtservicev1.MaintenanceTask{
			Name:               fmt.Sprintf("maintenanceTasks/%d", m.ID),
			MaintenanceTaskId:  uint64(m.ID),
			ProjectId:          m.ProjectID,
			CreateTime:         timestamppb.New(m.CreatedAt),
			Description:        m.Description,
			State:              cvtMaintenanceTaskStatusModelToEn(m.Status),
			ScheduledApplyTime: timestamppb.New(m.ScheduledApplyTime),
			Deadline:           deadline,
		},
		NotificationPhase: cvtMaintenanceTaskNotificationPhaseModelToEn(m.NotificationPhase),
	}
}

func cvtMaintenanceTaskStatusEnToModel(en tidbmgmtservicev1.MaintenanceTask_State) string {
	switch en {
	case tidbmgmtservicev1.MaintenanceTask_PENDING:
		return "pending"
	case tidbmgmtservicev1.MaintenanceTask_RUNNING:
		return "running"
	case tidbmgmtservicev1.MaintenanceTask_DONE:
		return "done"
	default:
		return ""
	}
}

func cvtMaintenanceTaskStatusModelToEn(status string) tidbmgmtservicev1.MaintenanceTask_State {
	switch status {
	case "pending":
		return tidbmgmtservicev1.MaintenanceTask_PENDING
	case "running":
		return tidbmgmtservicev1.MaintenanceTask_RUNNING
	case "done":
		return tidbmgmtservicev1.MaintenanceTask_DONE
	default:
		return tidbmgmtservicev1.MaintenanceTask_STATE_UNSPECIFIED
	}
}

func cvtMaintenanceTaskNotificationPhaseEnToModel(en entity.NotificationScene) string {
	switch en {
	case entity.NSMaintenanceWindow2WeeksAgo:
		return schema.MaintenanceTaskNotificationPhase2WeeksAgo
	case entity.NSMaintenanceWindow72HoursAgo:
		return schema.MaintenanceTaskNotificationPhase72HoursAgo
	case entity.NSMaintenanceWindowStarted:
		return schema.MaintenanceTaskNotificationPhaseStarted
	case entity.NSMaintenanceWindowCompleted:
		return schema.MaintenanceTaskNotificationPhaseCompleted
	default:
		return schema.MaintenanceTaskNotificationPhaseNone
	}
}

func cvtMaintenanceTaskNotificationPhaseModelToEn(phase string) entity.NotificationScene {
	switch phase {
	case schema.MaintenanceTaskNotificationPhase2WeeksAgo:
		return entity.NSMaintenanceWindow2WeeksAgo
	case schema.MaintenanceTaskNotificationPhase72HoursAgo:
		return entity.NSMaintenanceWindow72HoursAgo
	case schema.MaintenanceTaskNotificationPhaseStarted:
		return entity.NSMaintenanceWindowStarted
	case schema.MaintenanceTaskNotificationPhaseCompleted:
		return entity.NSMaintenanceWindowCompleted
	default:
		return entity.NSMaintenanceWindowNone
	}
}

func cvtMaintenanceTaskExecutionModelToEn(execution *schema.MaintenanceTaskExecution) *entity.MaintenanceTaskExecution {
	return &entity.MaintenanceTaskExecution{
		MaintenanceTaskId: execution.MaintenanceTaskID,
		ClusterId:         execution.ClusterID,
		State:             cvtMaintenanceTaskExecutionStatusModelToEn(execution.Status),
		CancellationReason: cvtMaintenanceTaskExecutionCancellationReasonModelToEn(
			execution.CancellationReason,
		),
		MaintenanceTaskExecutionId: uint64(execution.ID),
	}
}

func cvtMaintenanceTaskExecutionEnToModel(execution *entity.MaintenanceTaskExecution) *schema.MaintenanceTaskExecution {
	return &schema.MaintenanceTaskExecution{
		Model: gorm.Model{
			ID: uint(execution.GetMaintenanceTaskExecutionId()),
		},
		MaintenanceTaskID:  execution.GetMaintenanceTaskId(),
		ClusterID:          execution.GetClusterId(),
		Status:             cvtMaintenanceTaskExecutionStatusEnToModel(execution.GetState()),
		CancellationReason: cvtMaintenanceTaskExecutionCancellationReasonEnToModel(execution.GetCancellationReason()),
	}
}

func cvtMaintenanceTaskExecutionStatusModelToEn(status string) tidbmgmtservicev1.MaintenanceTaskExecution_State {
	switch status {
	case "pending":
		return tidbmgmtservicev1.MaintenanceTaskExecution_PENDING
	case "running":
		return tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING
	case "done":
		return tidbmgmtservicev1.MaintenanceTaskExecution_DONE
	case "canceled":
		return tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED
	default:
		return tidbmgmtservicev1.MaintenanceTaskExecution_STATE_UNSPECIFIED
	}
}

func cvtMaintenanceTaskExecutionStatusEnToModel(en tidbmgmtservicev1.MaintenanceTaskExecution_State) string {
	switch en {
	case tidbmgmtservicev1.MaintenanceTaskExecution_PENDING:
		return "pending"
	case tidbmgmtservicev1.MaintenanceTaskExecution_RUNNING:
		return "running"
	case tidbmgmtservicev1.MaintenanceTaskExecution_DONE:
		return "done"
	case tidbmgmtservicev1.MaintenanceTaskExecution_CANCELED:
		return "canceled"
	default:
		return ""
	}
}

func cvtMaintenanceTaskExecutionCancellationReasonModelToEn(
	reason string,
) tidbmgmtservicev1.MaintenanceTaskExecution_CancellationReason {
	switch reason {
	case "ops_canceled":
		return tidbmgmtservicev1.MaintenanceTaskExecution_OPS_CANCELED
	case "cluster_deleted":
		return tidbmgmtservicev1.MaintenanceTaskExecution_CLUSTER_DELETED
	default:
		return tidbmgmtservicev1.MaintenanceTaskExecution_REASON_UNSPECIFIED
	}
}

func cvtMaintenanceTaskExecutionCancellationReasonEnToModel(
	reason tidbmgmtservicev1.MaintenanceTaskExecution_CancellationReason,
) string {
	switch reason {
	case tidbmgmtservicev1.MaintenanceTaskExecution_OPS_CANCELED:
		return "ops_canceled"
	case tidbmgmtservicev1.MaintenanceTaskExecution_CLUSTER_DELETED:
		return "cluster_deleted"
	default:
		return ""
	}
}
