//go:generate ${GOBIN}/mockgen -destination repo_mock.go -package=repo ${GO_MODULE}/pkg/service/release/repo Repo,TicketDriver,ReleaseDriver
package repo

import (
	"context"
	"fmt"

	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/entity/validation"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/service/release/repo/cvt"

	"github.com/tidbcloud/pkgv2/db"
)

type (
	PatchChange  func(current *relent.ChangeInRepo) error
	PatchRelease func(current *relent.ReleaseInRepo) error
	PatchTicket  func(current *relent.ReleaseTicketInRepo) error
)

type Repo interface {
	// ListChanges lists changes
	ListChanges(ctx context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error)
	// GetChange gets a change
	GetChange(ctx context.Context, id uint64, detail bool) (*relent.ChangeInRepo, error)
	// CreateChange creates a new change and save it to the repo
	//
	// NOTE: ONLY use partial fields needed
	CreateChange(ctx context.Context, change *relent.ChangeInRepo) (*relent.ChangeInRepo, error)
	// GetAndPatchChange get a change and patches it.
	GetAndPatchChange(ctx context.Context, id uint64, patch ...PatchChange) error
	// PatchChange patches a change
	PatchChange(ctx context.Context, current *relent.ChangeInRepo, patch ...PatchChange) error
	// CountChanges counts changes
	CountChanges(ctx context.Context, opts ...repoopt.Option) (int64, error)

	// Release
	ListReleases(ctx context.Context, opts ...repoopt.Option) ([]*relent.ReleaseInRepo, error)
	GetRelease(ctx context.Context, id uint64) (*relent.ReleaseInRepo, error)
	CreateRelease(ctx context.Context, release *relent.ReleaseInRepo) error
	PatchRelease(ctx context.Context, id uint64, patch PatchRelease) error
	UpdateRelease(ctx context.Context, release *relent.ReleaseInRepo) error
	CountReleases(ctx context.Context, opts ...repoopt.Option) (int64, error)

	// Tickets
	ListTickets(ctx context.Context, opts ...repoopt.Option) ([]*relent.ReleaseTicketInRepo, error)
	GetTicket(ctx context.Context, name string) (*relent.ReleaseTicketInRepo, error)
	CreateTicket(ctx context.Context, ticket *relent.ReleaseTicketInRepo) (*relent.ReleaseTicketInRepo, error)
	PatchTicket(ctx context.Context, current *relent.ReleaseTicketInRepo, patch ...PatchTicket) error
	CountTicket(ctx context.Context, opts ...repoopt.Option) (int64, error)
}

func NewRepo(dbProvider db.Provider) Repo {
	driver := &driver{
		cvter:      cvt.NewConverter(),
		dbProvider: dbProvider,
	}

	return &repo{
		changeDriver:  (*changeDriver)(driver),
		releaseDriver: (*releaseDriver)(driver),
		ticketDriver:  (*ticketDriver)(driver),
	}
}

func NewRepoWithDriver(c ChangeDriver, r ReleaseDriver, t TicketDriver) Repo {
	return &repo{
		changeDriver:  c,
		releaseDriver: r,
		ticketDriver:  t,
	}
}

type driver struct {
	cvter      *cvt.Converter
	dbProvider db.Provider
}

type repo struct {
	changeDriver  ChangeDriver
	releaseDriver ReleaseDriver
	ticketDriver  TicketDriver
}

func (r *repo) ListChanges(ctx context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error) {
	return r.changeDriver.Query(ctx, detail, options...)
}

func (r *repo) GetChange(ctx context.Context, id uint64, detail bool) (*relent.ChangeInRepo, error) {
	return r.changeDriver.Load(ctx, id, detail)
}

func (r *repo) CreateChange(ctx context.Context, change *relent.ChangeInRepo) (*relent.ChangeInRepo, error) {
	err := ValidateChangeCreate(change)
	if err != nil {
		return nil, err
	}

	newChange := ChangeWhenCreate(change)

	err = validation.Change.ChangeInRepo(newChange)
	if err != nil {
		return nil, err
	}

	return r.changeDriver.Save(ctx, newChange)
}

func (r *repo) PatchChange(ctx context.Context, current *relent.ChangeInRepo, patch ...PatchChange) error {
	for _, p := range patch {
		err := p(current)
		if err != nil {
			return err
		}
	}

	err := validation.Change.ChangeInRepo(current)
	if err != nil {
		return err
	}

	_, err = r.changeDriver.Save(ctx, current)
	if err != nil {
		return err
	}
	return err
}

func (r *repo) GetAndPatchChange(ctx context.Context, id uint64, patch ...PatchChange) error {
	current, err := r.changeDriver.Load(ctx, id, true)
	if err != nil {
		return err
	}

	return r.PatchChange(ctx, current, patch...)
}

func (r *repo) CountChanges(ctx context.Context, opts ...repoopt.Option) (int64, error) {
	return r.changeDriver.Count(ctx, opts...)
}

func (r *repo) ListReleases(ctx context.Context, opts ...repoopt.Option) ([]*relent.ReleaseInRepo, error) {
	return r.releaseDriver.Query(ctx, opts...)
}

func (r *repo) GetRelease(ctx context.Context, id uint64) (*relent.ReleaseInRepo, error) {
	return r.releaseDriver.Load(ctx, id)
}

func (r *repo) CreateRelease(ctx context.Context, release *relent.ReleaseInRepo) error {
	// validate
	err := validation.Release.ReleaseInRepo(release)
	if err != nil {
		return fmt.Errorf("invalid arguments to create: %s", err)
	}

	newRelease := &relent.ReleaseInRepo{
		ID:            0,
		Name:          release.Name,
		Type:          release.Type,
		WindowStartAt: release.WindowStartAt,
		WindowEndAt:   release.WindowEndAt,
		ClosedAt:      nil,
		Status:        release.Status,
	}

	_, err = r.releaseDriver.Save(ctx, newRelease)
	return err
}

func (r *repo) PatchRelease(ctx context.Context, id uint64, patch PatchRelease) error {
	current, err := r.releaseDriver.Load(ctx, id)
	if err != nil {
		return err
	}

	err = patch(current)
	if err != nil {
		return err
	}

	// validate
	err = validation.Release.ReleaseInRepo(current)
	if err != nil {
		return fmt.Errorf("invalid arguments to update: %s", err)
	}

	_, err = r.releaseDriver.Save(ctx, current)
	return err
}

func (r *repo) UpdateRelease(ctx context.Context, release *relent.ReleaseInRepo) error {
	// validate
	err := validation.Release.ReleaseInRepo(release)
	if err != nil {
		return fmt.Errorf("invalid arguments to update: %s", err)
	}

	_, err = r.releaseDriver.Save(ctx, release)
	return err
}

func (r *repo) CountReleases(ctx context.Context, opts ...repoopt.Option) (int64, error) {
	return r.releaseDriver.Count(ctx, opts...)
}

func (r *repo) ListTickets(ctx context.Context, opts ...repoopt.Option) ([]*relent.ReleaseTicketInRepo, error) {
	return r.ticketDriver.Query(ctx, opts...)
}

func (r *repo) GetTicket(ctx context.Context, name string) (*relent.ReleaseTicketInRepo, error) {
	return r.ticketDriver.Load(ctx, name)
}

func (r *repo) CreateTicket(ctx context.Context, ticket *relent.ReleaseTicketInRepo) (*relent.ReleaseTicketInRepo, error) {
	err := validation.Release.TicketInRepo(ticket)
	if err != nil {
		return nil, err
	}

	return r.ticketDriver.Save(ctx, ticket)
}

func (r *repo) PatchTicket(ctx context.Context, current *relent.ReleaseTicketInRepo, patch ...PatchTicket) error {
	for _, p := range patch {
		err := p(current)
		if err != nil {
			return err
		}
	}

	err := validation.Release.TicketInRepo(current)
	if err != nil {
		return err
	}

	_, err = r.ticketDriver.Save(ctx, current)
	return err
}

func (r *repo) CountTicket(ctx context.Context, opts ...repoopt.Option) (int64, error) {
	return r.ticketDriver.Count(ctx, opts...)
}

// ValidateChangeCreate validates change fields that are used to create.
func ValidateChangeCreate(change *relent.ChangeInRepo) error {
	if change.ID != 0 {
		return fmt.Errorf("id must be 0")
	}

	switch change.Status {
	case relent.ChangeStatusCreated,
		relent.ChangeStatusDraft:
	default:
		return fmt.Errorf("invalid status when creation: %s", change.Status)
	}

	return nil
}

func ChangeWhenCreate(change *relent.ChangeInRepo) *relent.ChangeInRepo {
	newChange := &relent.ChangeInRepo{
		ID:            0,
		Name:          change.Name,
		WindowStartAt: change.WindowStartAt,
		WindowEndAt:   change.WindowEndAt,
		ClosedAt:      nil,
		Tier:          change.Tier,
		RiskLevel:     change.RiskLevel,
		Status:        change.Status,
		StatusReason:  change.StatusReason,
		Result:        nil,
		Type:          change.Type,

		Detail: &relent.ChangeDetail{
			ID:                0,
			NeedOutage:        change.Detail.NeedOutage,
			Note:              change.Detail.Note,
			RollbackPlan:      change.Detail.RollbackPlan,
			StagingTestReport: change.Detail.StagingTestReport,
			Summary:           nil,
		},
		Notification: &relent.ChangeNotification{ // create a corresponding notification
			ID:                 0,
			LarkTopicCreatedAt: nil,
			LarkTopicID:        nil,
		},

		ChangeItemNames: change.ChangeItemNames,
		ReleaseID:       change.ReleaseID,
		ApplicantEmail:  change.ApplicantEmail,
		ApplicationName: change.ApplicationName,
		ApprovalID:      nil,
	}

	for _, d := range change.Deployments {
		newChange.Deployments = append(newChange.Deployments, DeploymentWhenCreate(d))
	}

	return newChange
}

func DeploymentWhenCreate(d *relent.ChangeDeployment) *relent.ChangeDeployment {
	return &relent.ChangeDeployment{
		ID:         0,
		Order:      d.Order,
		Regions:    d.Regions,
		DeployedAt: nil,
		Status:     relent.ChangeDeployStatusCreated,
		Method:     d.Method,
		Info:       d.Info,
		Note:       "",
	}
}
