package authactor

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

	"github.com/tidbcloud/devops/api"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	timeutil "github.com/tidbcloud/devops/pkg/infra/utils/time"
	"github.com/tidbcloud/devops/pkg/infra/utils/workqueue"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	jmpsvcapi "github.com/tidbcloud/devops/pkg/service/jump/api"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/types"
	"github.com/tidbcloud/devops/pkg/service/jump/jumpservercli"
	"github.com/tidbcloud/devops/pkg/service/jump/repo"
	privapi "github.com/tidbcloud/devops/pkg/service/privilege/api"
	"tailscale.com/client/tailscale/v2"

	"github.com/go-logr/logr"
)

type Objects struct {
	// Origin is the original instance object which is used to patch.
	//
	// NOTE: **DO NOT** modify it.
	Origin *jmpent.Authorization
	// Instance is the copied instance object that is used in action.
	JumpAuthorization *jmpent.Authorization
	// EventDetail contains the event detail of the instance.
	EventDetail any
}

type Components struct {
	Logger                logr.Logger
	Conf                  config.JumpServiceConfig
	JumpAuthorizationRepo repo.JumpRepo
	JumpPodManager        jumppod.JumpPodManager
	JumpServerCli         jumpservercli.JumpServerCli
	ApprovalService       apprapi.ApprovalServiceAPI
	PrivilegeService      privapi.PrivilegeServiceAPI
	Tailscale             *tailscale.Client
}

type Context struct {
	context.Context
	Components
	Objects
}

func (c *Context) PatchAuthorization(ctx context.Context, patch ...repo.PatchAuthorization) error {
	return c.JumpAuthorizationRepo.PatchAuthorization(ctx, c.Origin, patch...)
}

type AuthorizationActor interface {
	// GetContext returns the context of the actor.
	GetContext() *Context
	// Create create a instance.
	Create(param jmpsvcapi.CreateAuthorizationParam) error
	// SendApproval send a approval to approve this operation.
	SendApproval() error
	// ContinueIfApproved check the approval status and transition the instance phase.
	ContinueIfApproved() error
	// ContinueIfPodReady check if Jump Pod is ready.
	ContinueIfPodReady() error
	// Check if the authorization expired or need to be refreshed.
	ExpireOrRefresh() error
	// Cancel cancel a instance.
	Cancel(param jmpsvcapi.CancelAuthorizaitonParam) error
}

func NewAuthorizationActor(ctx context.Context, comps Components, auth *jmpent.Authorization) AuthorizationActor {
	var a AuthorizationActor
	var err error

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

	if actx.Origin == nil {
		return &actorInitAuthorization{
			baseActor: &baseActor{
				Context:    actx,
				defaultErr: errs.APIErrorf(api.ErrCodeFailedPrecond, "only support to create a new jump authorization"),
			},
		}
	}

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

	phase := actx.Origin.Phase
	base := &baseActor{
		Context: actx,
		defaultErr: errs.APIErrorf(api.ErrCodeFailedPrecond,
			"unsupported action for instance when phase is %s", actx.Origin.Phase,
		),
	}
	switch {
	case phase == jmpent.AuthorizationPhaseInitialized:
		a = &actorInitAuthorization{baseActor: base}
	case phase == jmpent.AuthorizationPhaseApproving:
		a = &actorWhenApproving{baseActor: base}
	case phase == jmpent.AuthorizationPhaseCreating:
		a = &actorWhenCreating{baseActor: base}
	case phase == jmpent.AuthorizationPhaseCreated:
		a = &actorWhenCreated{baseActor: base}
	case slices.Contains(jmpent.AuthorizationPhaseFinishedValues, phase):
		// finished phase, can't do anything
		a = base
	default:
		base.defaultErr = fmt.Errorf("unsupported authorization 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) Create(param jmpsvcapi.CreateAuthorizationParam) error {
	return a.defaultErr
}

func (a *baseActor) SendApproval() error {
	return a.defaultErr
}

func (a *baseActor) ContinueIfApproved() error {
	return a.defaultErr
}

func (a *baseActor) ContinueIfPodReady() error {
	return a.defaultErr
}

func (a *baseActor) ExpireOrRefresh() error {
	return a.defaultErr
}

func (a *baseActor) Cancel(param jmpsvcapi.CancelAuthorizaitonParam) error {
	return a.defaultErr
}

func (a *baseActor) finalizeJumps(ctx context.Context, force bool, timeout time.Duration) error {
	ins := a.JumpAuthorization

	pod, err := a.JumpPodManager.GetJumpPod(ctx, EmailToJumpPodName(ins.Applicant))
	if err != nil && !jumppod.IsNotFound(err) {
		return fmt.Errorf("get jump pod failed: %w", err)
	}

	if jumppod.IsNotFound(err) {
		a.Logger.Info("skip to finalize jumps cause jump pod is not found")
		return nil
	}

	if force {
		a.Logger.Info("skip finalizing jumps because force is enabled")
		return nil
	}

	if timeout != 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, timeout)
		defer cancel()
	}

	errs := container.NewSlice[error]()
	wq := workqueue.NewParallelizer(ins.Jumps, 10, 1)
	wq.Do(ctx, func(ctx context.Context, jobs []*jmpent.Jump) {
		for _, jump := range jobs {
			err := pod.FinalizeJump(ctx, jump.Destination.Extract(), jump.Attributes)
			if err != nil {
				errs.Append(err)
			}
		}
	})

	err = errors.Join(errs.Dump()...)
	if err != nil {
		return fmt.Errorf("finalize jumps failed: %w", err)
	}

	return nil
}

func (a *baseActor) refreshJumps(ctx context.Context) error {
	ins := a.JumpAuthorization
	now := time.Now()

	jp, err := a.JumpPodManager.GetJumpPod(ctx, EmailToJumpPodName(ins.Applicant))
	if err != nil && !jumppod.IsNotFound(err) {
		return fmt.Errorf("get jump pod failed: %w", err)
	}

	// recreate the jump pod if it is not found
	if jumppod.IsNotFound(err) {
		jp, err = a.JumpPodManager.SetUpJumpPod(ctx, EmailToJumpPodName(ins.Applicant))
		if err != nil {
			return fmt.Errorf("recreate jump pod failed: %w", err)
		}

		a.Logger.Info("recreate the jump pod")
	}

	updatedJumps := []*jmpent.Jump{}
	for _, jump := range ins.Jumps {
		updatedJumps = append(updatedJumps, jump)

		// refresh jump token if needed
		needRefresh := jump.Attributes.K8s != nil &&
			now.Add(time.Hour).After(time.Unix(jump.Attributes.K8s.TokenExpireAt, 0))

		if needRefresh {
			// refresh jump token
			rParam := &types.RefreshJumpParam{
				GenerateTokenParam: types.GenerateTokenParam{
					ExpireAt: *timeutil.TsToTime(ins.ExpireAt),
				},
			}
			jumpParam, err := jp.RefreshJump(ctx, jump.Destination.Extract(), jump.Attributes, rParam)
			if err != nil {
				return fmt.Errorf("refresh jump token failed: %w", err)
			}

			jump.Method = jumpParam.Method
			jump.Attributes = jumpParam.Attr
			a.Logger.Info("refreshed the jump token")
		}
	}

	err = a.PatchAuthorization(ctx, func(current *jmpent.Authorization) error {
		current.Jumps = updatedJumps
		return nil
	})
	if err != nil {
		return fmt.Errorf("patch token expire time failed: %w", err)
	}

	return nil
}
