package repo

import (
	"context"
	"fmt"
	"time"

	planent "github.com/tidbcloud/devops/entity/k8supgradeplan"
	"github.com/tidbcloud/devops/models/base"
	planmod "github.com/tidbcloud/devops/models/k8supgradeplan"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"
)

type ListFilter struct {
	CloudProviders []string
	EOSVersions    []string
	Statuses       []planent.PlanStatus
	Page           int
	PerPage        int
}

type Repo interface {
	Create(ctx context.Context, p *planent.PlannedUpgrade) (*planent.PlannedUpgrade, error)
	Update(ctx context.Context, p *planent.PlannedUpgrade) error
	Get(ctx context.Context, id uint64) (*planent.PlannedUpgrade, error)
	GetByApprovalID(ctx context.Context, approvalID uint64) (*planent.PlannedUpgrade, error)
	List(ctx context.Context, f ListFilter) ([]*planent.PlannedUpgrade, int64, error)
	FindActiveByCloudVersion(ctx context.Context, cloud, version string) (*planent.PlannedUpgrade, error)
	ListByStatus(ctx context.Context, status planent.PlanStatus) ([]*planent.PlannedUpgrade, error)
	ListDueScheduled(ctx context.Context, now time.Time) ([]*planent.PlannedUpgrade, error)
}

type repo struct {
	dbp db.Provider
}

func NewRepo(dbp db.Provider) Repo {
	return &repo{dbp: dbp}
}

func (r *repo) Create(ctx context.Context, p *planent.PlannedUpgrade) (*planent.PlannedUpgrade, error) {
	m := entityToModel(p)
	if err := r.dbp.WriteDB(ctx).Create(m).Error; err != nil {
		return nil, fmt.Errorf("create plan: %w", err)
	}
	return modelToEntity(m), nil
}

func (r *repo) Update(ctx context.Context, p *planent.PlannedUpgrade) error {
	m := entityToModel(p)
	if p.ID != 0 {
		m.ID = base.ID(p.ID)
	}
	return r.dbp.WriteDB(ctx).Save(m).Error
}

func (r *repo) Get(ctx context.Context, id uint64) (*planent.PlannedUpgrade, error) {
	var m planmod.PlannedUpgrade
	if err := r.dbp.ReadDB(ctx).First(&m, id).Error; err != nil {
		return nil, err
	}
	return modelToEntity(&m), nil
}

func (r *repo) GetByApprovalID(ctx context.Context, approvalID uint64) (*planent.PlannedUpgrade, error) {
	var m planmod.PlannedUpgrade
	if err := r.dbp.ReadDB(ctx).Where("approval_id = ?", approvalID).First(&m).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		return nil, err
	}
	return modelToEntity(&m), nil
}

func (r *repo) List(ctx context.Context, f ListFilter) ([]*planent.PlannedUpgrade, int64, error) {
	q := r.dbp.ReadDB(ctx).Model(&planmod.PlannedUpgrade{})
	if len(f.CloudProviders) > 0 {
		q = q.Where("cloud_provider IN ?", f.CloudProviders)
	}
	if len(f.EOSVersions) > 0 {
		q = q.Where("eos_version IN ?", f.EOSVersions)
	}
	if len(f.Statuses) > 0 {
		st := make([]string, len(f.Statuses))
		for i, s := range f.Statuses {
			st[i] = string(s)
		}
		q = q.Where("status IN ?", st)
	}
	var total int64
	if err := q.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	page, perPage := f.Page, f.PerPage
	if page <= 0 {
		page = 1
	}
	if perPage <= 0 {
		perPage = 20
	}
	q = repoopt.Aggregate(&repoopt.Pager{Page: page, PerPage: perPage}, &repoopt.Order{Column: "created_at", Desc: true}).Apply(q)
	var mods []*planmod.PlannedUpgrade
	if err := q.Find(&mods).Error; err != nil {
		return nil, 0, err
	}
	return container.TransformSlice(mods, modelToEntity), total, nil
}

func (r *repo) FindActiveByCloudVersion(ctx context.Context, cloud, version string) (*planent.PlannedUpgrade, error) {
	var m planmod.PlannedUpgrade
	err := r.dbp.ReadDB(ctx).
		Where("cloud_provider = ? AND eos_version = ?", cloud, version).
		Where("status NOT IN ?", []string{
			string(planent.PlanStatusRejected),
			string(planent.PlanStatusCancelled),
		}).
		Order("created_at desc").
		First(&m).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return modelToEntity(&m), nil
}

func (r *repo) ListByStatus(ctx context.Context, status planent.PlanStatus) ([]*planent.PlannedUpgrade, error) {
	var mods []*planmod.PlannedUpgrade
	err := r.dbp.ReadDB(ctx).Where("status = ?", status).Find(&mods).Error
	if err != nil {
		return nil, err
	}
	return container.TransformSlice(mods, modelToEntity), nil
}

func (r *repo) ListDueScheduled(ctx context.Context, now time.Time) ([]*planent.PlannedUpgrade, error) {
	var mods []*planmod.PlannedUpgrade
	err := r.dbp.ReadDB(ctx).
		Where("status = ? AND trigger_time <= ?", planent.PlanStatusScheduled, now).
		Find(&mods).Error
	if err != nil {
		return nil, err
	}
	return container.TransformSlice(mods, modelToEntity), nil
}

func entityToModel(p *planent.PlannedUpgrade) *planmod.PlannedUpgrade {
	m := &planmod.PlannedUpgrade{
		Name:              p.Name,
		CloudProvider:     p.CloudProvider,
		EOSVersion:        p.EOSVersion,
		EOSDate:           p.EOSDate,
		Deadline:          time.Unix(p.Deadline, 0),
		TriggerTime:       time.Unix(p.TriggerTime, 0),
		MaxRestartTimes:   p.MaxRestartTimes,
		SelectorType:      p.SelectorType,
		SelectorValue:     p.SelectorValue,
		Status:            string(p.Status),
		SourceEventID:     p.SourceEventID,
		ApprovalID:        p.ApprovalID,
		LinkedTaskID:      p.LinkedTaskID,
		CreatedBy:         p.CreatedBy,
		EstimatedClusters: p.EstimatedClusters,
	}
	if p.ID != 0 {
		m.ID = base.ID(p.ID)
	}
	return m
}

func modelToEntity(m *planmod.PlannedUpgrade) *planent.PlannedUpgrade {
	var updated int64
	if m.UpdatedAt != nil {
		updated = m.UpdatedAt.Unix()
	}
	return &planent.PlannedUpgrade{
		ID:                m.ID.Uint64(),
		Name:              m.Name,
		CloudProvider:     m.CloudProvider,
		EOSVersion:        m.EOSVersion,
		EOSDate:           m.EOSDate,
		Deadline:          m.Deadline.Unix(),
		TriggerTime:       m.TriggerTime.Unix(),
		MaxRestartTimes:   m.MaxRestartTimes,
		SelectorType:      m.SelectorType,
		SelectorValue:     m.SelectorValue,
		Status:            planent.PlanStatus(m.Status),
		SourceEventID:     m.SourceEventID,
		ApprovalID:        m.ApprovalID,
		LinkedTaskID:      m.LinkedTaskID,
		CreatedBy:         m.CreatedBy,
		EstimatedClusters: m.EstimatedClusters,
		CreatedAt:         m.CreatedAt.Unix(),
		UpdatedAt:         updated,
	}
}
