package release

import (
	"context"
	"fmt"
	"slices"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/entity/event"
	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	timeutil "github.com/tidbcloud/devops/pkg/infra/utils/time"
	"github.com/tidbcloud/devops/pkg/service/release/action"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"
)

var (
	statusToSyncChange = []relent.ChangeStatus{
		relent.ChangeStatusCreated,
		relent.ChangeStatusApproving,
		relent.ChangeStatusObserving,
	}
)

func (s *svc) ListChanges(ctx context.Context, param relsvcapi.ListChangeParam) ([]*relent.Change, error) {
	opts := []repoopt.Option{}
	if len(param.Releases) != 0 {
		opts = append(opts, repo.ChangeOpts.ReleaseIn(param.Releases))
	}
	if len(param.Status) != 0 {
		opts = append(opts, repo.ChangeOpts.StatusIn(param.Status))
	}

	changesInRepo, err := s.repo.ListChanges(ctx, param.WithChangeDetail, opts...)
	if err != nil {
		return nil, err
	}

	changes := make([]*relent.Change, 0, len(changesInRepo))

	for _, changeInRepo := range changesInRepo {
		change := &relent.Change{
			ChangeInRepo: *changeInRepo,
		}

		err := s.fillInChange(ctx, change, param.GetChangeParam)
		if err != nil {
			return nil, err
		}

		changes = append(changes, change)
	}

	return changes, nil
}

func (s *svc) GetChange(ctx context.Context, id uint64, param relsvcapi.GetChangeParam) (*relent.Change, error) {
	changeInRepo, err := s.repo.GetChange(ctx, id, param.WithChangeDetail)
	if err != nil {
		return nil, err
	}

	change := &relent.Change{
		ChangeInRepo: *changeInRepo,
	}

	s.fillInChange(ctx, change, param)

	return change, nil
}

func (s *svc) CreateChange(ctx context.Context, param relsvcapi.CreateChangeParam) (*uint64, error) {
	var changeID *uint64

	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActor(ctx, s.actionComponents(ctx), nil)

		err := actor.Create(relsvcapi.CreateChangeParam(param))
		if err != nil {
			return err
		}
		newChange := actor.GetContext().Change

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeCreate)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		changeID = &newChange.ID
		return nil
	})

	// sync this change immediately
	if !param.IsDraft && changeID != nil {
		s.syncChangeCh <- *changeID
	}

	return changeID, err
}

func (s *svc) SubmitChange(ctx context.Context, id uint64) error {
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.Submit()
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeSubmit)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})

	// sync this change immediately
	if err == nil {
		s.syncChangeCh <- id
	}
	return err
}

func (s *svc) UpdateChangeBeforeDeploy(ctx context.Context, id uint64, update relsvcapi.UpdateChangeParam) error {
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.UpdateChangeBeforeDeploy(relsvcapi.UpdateChangeParam(update))
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeUpdateBeforeDeploy)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})

	// sync this change immediately
	if err == nil {
		s.syncChangeCh <- id
	}
	return err
}

func (s *svc) CancelChange(ctx context.Context, id uint64) error {
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.Cancel()
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeCancel)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) ExecuteDeployment(ctx context.Context, id uint64, order relent.ChangeDeployOrder, force bool) error {
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.ExecuteDeployment(order, force)
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeExecDeploy)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) VerifyDeployment(ctx context.Context, id uint64, order relent.ChangeDeployOrder, note string) error {
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.VerifyDeployment(order, note)
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeVerifyDeploy)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) CloseChange(ctx context.Context, id uint64, result relent.ChangeResult, summary string) error {
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := action.NewChangeActorByID(ctx, s.actionComponents(ctx), id)

		err := actor.Close(result, summary)
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.ChangeEventTypeClose)
		err = s.eventbus.ChangeEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) IsDeploymentReady(ctx context.Context, id uint64, order relent.ChangeDeployOrder) ([]string, error) {
	changeInRepo, err := s.repo.GetChange(ctx, id, true)
	if err != nil {
		return nil, err
	}
	deployment := changeInRepo.GetDeployment(order)
	if deployment == nil {
		return nil, errs.APIErrorf(api.ErrCodeNotFound, "deployment with order %d is not found", order)
	}

	return action.CheckDeploymentReady(ctx, s.actionComponents(ctx), changeInRepo, deployment)
}

func (s *svc) DefaultChange(ctx context.Context, param relsvcapi.DefaultChangeParam) (*relent.Change, error) {
	change := &relent.Change{}

	// get related resources
	release, err := s.GetRelease(ctx, param.ReleaseID, relsvcapi.GetReleaseParam{
		WithTickets: false,
	})
	if err != nil {
		return nil, err
	}
	change.Release = release

	application, err := s.applSvc.GetApplication(ctx, param.ApplicationName)
	if err != nil {
		return nil, err
	}
	change.Application = application

	// default
	// default change name
	change.Name = fmt.Sprintf("%s/%s/%s", release.Name, application.Name, container.RandomString(6))
	// default window start/end time
	now := time.Now()
	switch release.Type {
	case relent.ReleaseTypePeriodic:
		change.WindowStartAt = time.Date(
			now.Year(),
			now.Month(),
			now.Day(),
			0,
			0,
			0,
			0,
			timeutil.ChinaTimeZone,
		).Unix()
		change.WindowEndAt = time.Date(
			now.Year(),
			now.Month(),
			now.Day(),
			24,
			0,
			0,
			0,
			timeutil.ChinaTimeZone,
		).Unix()
	}

	return change, nil
}

func (s *svc) actionComponents(ctx context.Context) action.Components {
	return action.Components{
		Conf:               s.conf,
		Logger:             log.FromContextOrDef(ctx, s.logger),
		UserService:        s.userSvc,
		ApplicationService: s.applSvc,
		ApprovalService:    s.apprSvc,
		ReleaseService:     s,
		GithubService:      s.ghSvc,
		ChangeRepo:         s.repo,
		LarkClient:         s.larkCli,
		FADClient:          s.fadCli,
		EEClient:           s.eeCli,
	}
}

func (s *svc) fillInChange(ctx context.Context, change *relent.Change, param relsvcapi.GetChangeParam) error {
	if param.WithChangeItems {
		changeItems := make([]*relent.ReleaseTicket, 0)
		if len(change.ChangeItemNames) > 0 {
			var err error
			changeItems, _, err = s.ListTickets(ctx, relsvcapi.ListTicketsParam{
				Names: change.ChangeItemNames,
			})
			if err != nil {
				return err
			}
		}
		change.ChangeItems = changeItems
	}

	if param.WithRelease {
		release, err := s.GetRelease(ctx, change.ReleaseID, relsvcapi.GetReleaseParam{
			WithTickets: false,
		})
		if err != nil {
			return err
		}
		change.Release = release
	}

	if param.WithApplicant {
		applicantResp, err := s.userSvc.GetUser(ctx, &userv1.GetUserRequest{Email: change.ApplicantEmail})
		if err != nil {
			return err
		}
		change.Applicant = applicantResp.User
	}

	if param.WithApplication {
		application, err := s.applSvc.GetApplication(ctx, change.ApplicationName)
		if err != nil {
			return err
		}
		change.Application = application
	}

	return nil
}

func (s *svc) WatchChanges(ctx context.Context) error {
	logger := log.FromContext(ctx)

	changes, err := s.repo.ListChanges(ctx, false,
		repo.ChangeOpts.SelectIDOnly(), // select id only
		repo.ChangeOpts.StatusIn(statusToSyncChange))
	if err != nil {
		return err
	}

	if len(changes) != 0 {
		logger.Info("syncing changes ...",
			"change_ids", container.TransformSlice(changes, func(i *relent.ChangeInRepo) uint64 { return i.ID }),
		)
	}

	for _, chg := range changes {
		s.syncChangeCh <- chg.ID
	}

	return nil
}

func (s *svc) SyncChange(ctx context.Context, id uint64) error {
	change, err := s.repo.GetChange(ctx, id, true) // MUST load details
	if err != nil {
		return err
	}
	if !slices.Contains(statusToSyncChange, change.Status) {
		return nil
	}

	ctx = log.WrapContext(ctx, "change_id", id, "change_status", change.Status)
	actor := action.NewChangeActor(ctx, s.actionComponents(ctx), change)

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		var err error
		var evType event.EventType

		switch change.Status {
		case relent.ChangeStatusCreated:
			err = actor.SendApprovalIfReady()
			evType = event.ChangeEventTypeSendApproval
		case relent.ChangeStatusApproving:
			aerr := actor.ContinueIfApproved()
			err = aerr
			evType = event.ChangeEventTypeFinishApproval
		case relent.ChangeStatusObserving:
			aerr := actor.NotifyToCloseIfNeeded()
			err = aerr
			evType = "" // no need to send event
		}

		if err == nil && evType != "" {
			ev := buildEvent(ctx, actor.GetContext(), evType)
			ev.OperatorEmail = consts.InternalVirtualEmail
			err = s.eventbus.ChangeEvent.Publish(ev)
			if err != nil {
				return fmt.Errorf("publish event failed: %s", err)
			}
		}

		return err
	})

	return err
}
