package action

import (
	"context"
	"errors"
	"fmt"
	"slices"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	baseapi "github.com/tidbcloud/devops/api"
	privent "github.com/tidbcloud/devops/entity/privilege"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	larknotify "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/workqueue"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	"github.com/tidbcloud/devops/pkg/service/privilege/api"
	"github.com/tidbcloud/devops/pkg/service/privilege/repo"
)

type Objects struct {
	// Origin is the original instance object which is used to patch.
	//
	// NOTE: **DO NOT** modify it.
	Origin *privent.Privilege
	// Privilege is the copied instance object that is used in action.
	Privilege *privent.Privilege
}

type Components struct {
	Repo            repo.Repo
	ApprovalService apprapi.ApprovalServiceAPI
	UserService     userv1.UserServiceClient
	LarkClient      *larkcli.LarkClient
}

type Context struct {
	context.Context
	Components
	Objects
}

type PrivilegeActor interface {
	// GetContext returns the context of the actor.
	GetContext() *Context
	// Request request a privilege and send an approval.
	Request(param api.RequestPrivilegeParam) error
	// GrantIfApproved check the approval status and active the privilege.
	GrantIfApproved() error
	// ExpireIfNeeded if the privilege expired.
	ExpireIfNeeded() error
	// Revoke the privilege.
	Revoke() error
	// UnbindingUser unbind the user from the privilege.
	UnbindingUsers(emails []string) ([]string, error)
}

func NewPrivilegeActor(ctx context.Context, comps Components, priv *privent.Privilege) PrivilegeActor {
	var a PrivilegeActor
	var err error

	actx := &Context{
		Context:    ctx,
		Components: comps,
		Objects: Objects{
			Origin: priv,
		},
	}

	if actx.Origin == nil {
		return &actorCreate{
			baseActor: &baseActor{
				Context:    actx,
				defaultErr: errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "only support to create a new privilege"),
			},
		}
	}

	actx.Privilege, err = pointer.DeepCopy(actx.Origin) // deep copy to avoid modify origin instance directly
	if err != nil {
		return &baseActor{defaultErr: fmt.Errorf("copy privilege failed: %s", err)}
	}

	phase := actx.Origin.Phase
	base := &baseActor{
		Context: actx,
		defaultErr: errs.APIErrorf(baseapi.ErrCodeFailedPrecond,
			"unsupported action for privilege when phase is %s", actx.Origin.Phase,
		),
	}
	switch {
	case phase == privent.PrivilegePhaseApproving:
		a = &actorApproving{baseActor: base}
	case phase == privent.PrivilegePhaseActive:
		a = &actorActive{baseActor: base}
	case slices.Contains(privent.PrivilegePhaseFinishedValues, phase):
		// finished phase, can't do anything
		a = base
	default:
		base.defaultErr = fmt.Errorf("unsupported privilege phase: %s", actx.Origin.Phase)
		a = base
	}

	return a
}

type baseActor struct {
	*Context

	defaultErr error
}

func (a *baseActor) GetContext() *Context {
	return a.Context
}
func (a *baseActor) Request(param api.RequestPrivilegeParam) error {
	return a.defaultErr
}
func (a *baseActor) GrantIfApproved() error {
	return a.defaultErr
}
func (a *baseActor) ExpireIfNeeded() error {
	return a.defaultErr
}
func (a *baseActor) Revoke() error {
	return a.defaultErr
}
func (a *baseActor) UnbindingUsers(emails []string) ([]string, error) {
	return nil, a.defaultErr
}

func (a *baseActor) notifyUsers(param larknotify.Param, emails []string) error {
	ctx := a.Context
	priv := a.Privilege

	if emails == nil {
		emails = container.TransformSlice(priv.Bindings, func(b *privent.PrivilegeBinding) string { return b.User })
	}

	if len(emails) == 0 {
		return nil
	}

	// list all users we need
	//
	// NOTE: gorm can't execute the query concurrently in the transaction, so
	// we collect all users now.
	// Ref: https://github.com/go-gorm/gorm/issues/6765#issuecomment-1869906886
	usersResp, err := a.UserService.ListUsers(ctx, &userv1.ListUsersRequest{
		EmailIn: emails,
		Page:    1,
		PerPage: int32(len(emails)),
	})
	if err != nil {
		return fmt.Errorf("list users failed: %w", err)
	}

	// send notifications
	errList := container.NewSlice[error]()
	wq := workqueue.NewParallelizer(usersResp.Users, 10, 2)
	wq.Do(ctx, func(ctx context.Context, jobs []*coreuser.User) {
		for _, user := range jobs {
			if !user.Available {
				continue
			}

			_, err := a.LarkClient.Notify.SendMessage(ctx, larknotify.ReceiveTypeUser, user.LarkUserId, param)
			if err != nil {
				errList.Append(err)
				continue
			}
		}
	})
	return errors.Join(errList.Dump()...)
}
