package instance

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	"github.com/tidbcloud/devops/api"
	apprent "github.com/tidbcloud/devops/entity/approval"
	entcommon "github.com/tidbcloud/devops/entity/common"
	"github.com/tidbcloud/devops/entity/event"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	actregistry "github.com/tidbcloud/devops/pkg/service/operation/actions/registry"
	"github.com/tidbcloud/devops/pkg/service/operation/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/repo"
	"github.com/tidbcloud/devops/pkg/service/tenant"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"

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

type CreateInstanceActionParam struct {
	Name            opent.ActionName        `json:"name"`
	StartPolicy     opent.ActionStartPolicy `json:"start_policy"`
	StartScheduleAt *int64                  `json:"start_schedule_at,omitempty"`
}

type CreateInstanceParam struct {
	Type           opent.OperationTarget       `json:"type"`
	Operation      string                      `json:"operation"`
	Reason         string                      `json:"reason"`
	Applicant      string                      `json:"applicant"`
	OperatorEntity string                      `json:"operator_entity"`
	Params         map[opent.ParamName]string  `json:"params,omitempty"`
	Actions        []CreateInstanceActionParam `json:"actions,omitempty"`  // actions to create, if empty, will not create actions
	IsDraft        bool                        `json:"is_draft,omitempty"` // if true, create a draft instance, otherwise create a normal instance

	ExecutionWindow *cmxapi.CreateOpsExecutionWindowParam `json:"execution_window,omitempty"`
}

func (p CreateInstanceParam) Validate() error {
	if p.ExecutionWindow != nil && len(p.Actions) != 0 {
		startScheduleAt := p.Actions[0].StartScheduleAt
		if startScheduleAt != nil {
			if *startScheduleAt < p.ExecutionWindow.StartAt || *startScheduleAt > p.ExecutionWindow.EndAt {
				return fmt.Errorf(
					"first action start_schedule_at %d must be within execution_window [%d, %d]",
					*startScheduleAt,
					p.ExecutionWindow.StartAt,
					p.ExecutionWindow.EndAt,
				)
			}
		}

	}

	return nil
}

type UpdateInstanceParam struct {
	Reason  *string                      `json:"reason,omitempty"`
	Params  *map[opent.ParamName]string  `json:"params,omitempty"`
	Actions *[]CreateInstanceActionParam `json:"actions,omitempty"` // actions to update, if empty, will not update actions
}

type FinishInstanceActionParam struct {
	// If true, allow to finish a manual action, and use Result and Output to update the action.
	AllowManual bool
	Result      opent.ActionStatus
	Output      string
}

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

type Components struct {
	Logger            logr.Logger
	Conf              config.OperationServiceConfig
	UserService       userv1.UserServiceClient
	ApprovalService   apprsvcapi.ApprovalServiceAPI
	MaintenanceSvc    cmxapi.ServiceAPI
	TenantService     tenant.TenantServiceAPI
	OperationRepo     repo.OperationRepo
	OperationRegistry *operation.Registry
	ActionRegistry    actregistry.Registry
	CoreSvcCli        *coresvccli.CoreSvcCli
}

type Context struct {
	context.Context
	Components
	Objects
}

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

type InstanceActor interface {
	// GetContext returns the context of the actor.
	GetContext() *Context
	// Create create a instance.
	Create(param CreateInstanceParam) error
	// Preview runs PreCheck and advisory checks without persisting anything.
	Preview(param CreateInstanceParam) ([]opent.Advisory, error)
	// Submit a draft instance.
	Submit() error
	// Update the instance, only supported in partial phases.
	Update(param UpdateInstanceParam) error
	// SendApproval send a approval to approve this operation.
	SendApproval() error
	// ContinueIfApproved check the approval status and transition the instance phase.
	ContinueIfApproved() error
	// Cancel cancel a instance.
	Cancel() error

	// StartAction start a action.
	StartAction(order opent.ActionOrder, maunal bool) error
	// FinishAction finish a action.
	//
	// If the action is a manual action, it will set the result and output.
	// If the action is a auto action, it will check the result.
	FinishAction(order opent.ActionOrder, param FinishInstanceActionParam) error
}

func NewInstanceActor(ctx context.Context, comps Components, instance *opent.Instance) InstanceActor {
	var a InstanceActor
	var err error

	actx := &Context{
		Context:    ctx,
		Components: comps,
		Objects: Objects{
			Origin:      instance,
			EventDetail: &event.OperationEventDetail{},
		},
	}

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

	actx.Instance, err = pointer.DeepCopy(actx.Origin) // deep copy to avoid modify origin instance directly
	if err != nil {
		return &baseActor{defaultErr: fmt.Errorf("copy instance 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 == opent.InstancePhaseDraft:
		a = &actorWhenDraft{baseActor: base}
	case phase == opent.InstancePhaseCreated:
		a = &actorWhenCreated{baseActor: base}
	case phase == opent.InstancePhaseApproving:
		a = &actorWhenApproving{baseActor: base}
	case phase == opent.InstancePhasePending:
		a = &actorWhenPending{baseActor: base}
	case phase == opent.InstancePhaseRunning:
		a = &actorWhenRunning{baseActor: base}
	case phase == opent.InstancePhaseWithdrawn:
		a = &actorWhenWithdrawn{baseActor: base}
	case phase == opent.InstancePhaseFailed:
		a = &actorWhenFailed{baseActor: base}
	case slices.Contains(opent.InstancePhaseCanNotRetryValues, phase):
		// finished phase, can't do anything
		a = base
	default:
		base.defaultErr = fmt.Errorf("unsupported instance 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 CreateInstanceParam) error {
	return a.defaultErr
}

func (a *baseActor) Preview(param CreateInstanceParam) ([]opent.Advisory, error) {
	return nil, a.defaultErr
}

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

func (a *baseActor) Update(param UpdateInstanceParam) error {
	return a.defaultErr
}

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

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

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

func (a *baseActor) StartAction(order opent.ActionOrder, maunal bool) error {
	return a.defaultErr
}

func (a *baseActor) FinishAction(order opent.ActionOrder, param FinishInstanceActionParam) error {
	return a.defaultErr
}

// cancel transition status to 'Closed' and result is 'Cancel'.
func (a *baseActor) cancel() error {
	ctx := a.Context

	return a.PatchInstance(ctx,
		func(current *opent.Instance) error {
			// cancel actions
			for i := len(current.Actions) - 1; i >= 0; i-- {
				action := current.Actions[i]

				if action.StartedAt == nil {
					// not started, skip
					continue
				}

				actionObj, is := a.Context.ActionRegistry.GetAsAuto(action.Name)
				if actionObj == nil {
					return fmt.Errorf("action '%s' not found in registry", action.Name)
				}
				if !is {
					// not an auto action, skip
					continue
				}

				// cancel the action
				canceled, err := actionObj.OnCancel(a.Context, a.Context.Instance)
				if err != nil {
					return fmt.Errorf("cancel action '%s' failed: %w", action.Name, err)
				}

				if canceled {
					action.Status = opent.ActionStatusCanceled
				}
			}

			// transition status to 'Cancel'
			current.Phase = opent.InstancePhaseCanceled
			current.ClosedAt = pointer.Pointer(time.Now().Unix())

			return nil
		})
}

// sendApproval send a approvel for the current instance.
func (a *baseActor) sendApproval() (*uint64, error) {
	ctx := a.Context
	ins := a.Instance

	op := a.OperationRegistry.Get(ins.Operation)
	if op == nil {
		return nil, fmt.Errorf("unknown operation '%s'", ins.Operation)
	}

	// build overview: human-readable summary for Lark notifications
	overview := map[string]string{
		"Operation":   op.Alias,
		"Target Type": string(ins.Type),
	}
	if op.ApprovalTips != "" {
		overview["Tips"] = op.ApprovalTips
	}
	a.injectTargetOverview(ctx, ins, overview)

	// build parameters: full operation params, used by fromParam/evalCondition for approval routing
	parameters := make(map[string]string, len(ins.Params))
	for k, v := range ins.Params {
		parameters[string(k)] = v
	}
	parameters["ID"] = strconv.FormatUint(a.Instance.ID, 10)
	parameters["Operation"] = op.Alias
	parameters["Target Type"] = string(ins.Type)
	// build param
	paramBase := apprsvcapi.CreateApprovalParamBase{
		Name:       "Cloud Operation",
		Applicant:  ins.Applicant,
		DetailLink: actions.InstanceDetailLink(a.Instance.ID),
		Overview:   overview,
		Parameters: parameters,
		Reason:     ins.Reason,
	}
	operatorType, _, _ := entcommon.ParseOperatorEntity(ins.OperatorEntity)
	if operatorType == consts.CtxOperatorTypeAPIKey {
		paramBase.Configuration.SkipApplicantNotification = true
	}

	var mutates []func(param *apprsvcapi.CreateApprovalParam)
	maintenanceWindowMutate, err := a.buildMaintenanceWindowApprovalMutate()
	if err != nil {
		return nil, fmt.Errorf("build maintenance window approval mutate failed: %w", err)
	}
	if maintenanceWindowMutate != nil {
		mutates = append(mutates, maintenanceWindowMutate)
	}

	// send approval
	apprID, err := a.ApprovalService.CreateApprovalFromTemplate(ctx, apprsvcapi.CreateApprovalFromTemplateParam{
		CreateApprovalParamBase: paramBase,
		FromTemplate:            op.Approval.ID,
	}, mutates...)
	if err != nil {
		return nil, fmt.Errorf("send approval failed: %w", err)
	}

	return apprID, nil
}

func (a *baseActor) buildMaintenanceWindowApprovalMutate() (func(param *apprsvcapi.CreateApprovalParam), error) {
	if a.Instance.Type != opent.OperationTargetCluster {
		return nil, nil
	}

	clusterID, ok := a.Instance.ExistsTargetID()
	if !ok {
		return nil, nil
	}

	var executeTimes []time.Time
	if a.Instance.ExecutionWindowID != nil {
		executionWindow, err := a.MaintenanceSvc.GetOpsExecutionWindow(a.Context, *a.Instance.ExecutionWindowID)
		if err != nil {
			return nil, err
		}
		executeTimes = append(
			executeTimes,
			time.Unix(executionWindow.StartAt, 0),
			time.Unix(executionWindow.EndAt, 0),
		)
	}

	inWindow, window, err := a.MaintenanceSvc.IsInClusterOpsMaintenanceWindow(a.Context, clusterID, executeTimes)
	if err != nil {
		return nil, err
	}
	if inWindow || window == nil || len(window.Approvers) == 0 {
		return nil, nil
	}

	// not in maintenance window, append extra approvers.
	return func(param *apprsvcapi.CreateApprovalParam) {
		param.Nodes = append(param.Nodes, &apprent.ApproverNode{
			Name:   "Cluster Is Outside Ops Maintenance Window",
			Method: apprent.ApproveMethodAny,
			Approver: &apprent.Approver{
				Emails: window.Approvers,
			},
		})
	}, nil
}

func (a *baseActor) ensureInExecutionWindow() error {
	ins := a.Instance
	if ins.ExecutionWindowID == nil {
		return nil
	}

	windowID := *ins.ExecutionWindowID
	window, err := a.MaintenanceSvc.GetOpsExecutionWindow(a.Context, windowID)
	if err != nil {
		return fmt.Errorf("get ops execution window %d failed: %w", windowID, err)
	}

	now := time.Now()
	startAt := time.Unix(window.StartAt, 0)
	endAt := time.Unix(window.EndAt, 0)
	if now.Before(startAt) || !now.Before(endAt) {
		return errs.APIErrorf(api.ErrCodeFailedPrecond, "current time is outside execute window")
	}

	return nil
}

func (a *baseActor) injectTargetOverview(ctx context.Context, ins *opent.Instance, overview map[string]string) {
	targetID, ok := ins.ExistsTargetID()
	if !ok {
		return
	}

	switch ins.Type {
	case opent.OperationTargetCluster:
		clusterID, err := strconv.ParseUint(targetID, 10, 64)
		if err != nil {
			return
		}
		cluster, err := a.Components.CoreSvcCli.Mgmt.GetCluster(ctx, clusterID)
		if err != nil {
			return
		}
		overview["Cluster Name"] = cluster.GetClusterName()
		overview["Cloud Provider"] = cluster.GetCloudProvider()
		overview["Logical Region"] = cluster.GetRegion()
		if orgID := cluster.GetOrganizationId(); orgID != 0 {
			if tnt, err := a.Components.TenantService.GetTenant(ctx, orgID); err == nil {
				overview["Tenant Name"] = tnt.Name
			}
		}
		if cluster.GetDeployType() == mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN {
			if rpIDStr := cluster.GetNextGenCluster().GetSharedPoolId(); rpIDStr != "" {
				if rp, err := a.Components.CoreSvcCli.Mgmt.GetResourcePoolString(ctx, rpIDStr); err == nil {
					rpp := rp.GetResourcePool()
					overview["Resource Pool Name"] = rpp.GetName()
					overview["Resource Pool Region"] = rp.GetRegion().GetLogicalName()
				}
			}
		}

	case opent.OperationTargetTenant:
		tenantID, err := strconv.ParseUint(targetID, 10, 64)
		if err != nil {
			return
		}
		tnt, err := a.Components.TenantService.GetTenant(ctx, tenantID)
		if err != nil {
			return
		}
		overview["Tenant Name"] = tnt.Name

	case opent.OperationTargetResourcePool:
		rp, err := a.Components.CoreSvcCli.Mgmt.GetResourcePoolString(ctx, targetID)
		if err != nil {
			return
		}
		rpp := rp.GetResourcePool()
		overview["Resource Pool Name"] = rpp.GetName()
		overview["Logical Region"] = rp.GetRegion().GetLogicalName()
		overview["Cloud Provider"] = rp.GetRegion().GetCloudProviderId().String()
		if rpp.GetOwnerType() == nextgenv1.ResourcePool_ORGANIZATION {
			if tnt, err := a.Components.TenantService.GetTenant(ctx, rpp.GetOwnerId()); err == nil {
				overview["Tenant Name"] = tnt.Name
			}
		}
	}

}

func (a *baseActor) cancelApproval(appr *apprent.Approval) error {
	ctx := a.Context

	if appr.Status == apprent.ApprovalStatusPending {
		err := a.ApprovalService.CancelApproval(ctx, appr.ID)
		if err != nil {
			return err
		}
	}

	return nil
}

func (a *baseActor) startAction(order opent.ActionOrder, manual bool, startCondition func(action *opent.InstanceAction) error) error {
	ctx := a.Context
	ins := a.Instance

	action := ins.GetAction(order)
	if action == nil {
		return errs.APIErrorf(api.ErrCodeNotFound, "action with order %d is not found", order)
	}

	if err := startCondition(action); err != nil {
		return errs.APIErrorf(api.ErrCodeFailedPrecond, "action can't be started, %v", err)
	}

	reasons := CheckActionReady(ins, action, manual)
	if len(reasons) > 0 {
		return errs.RetriableErrorf("%s", strings.Join(reasons, ","))
	}

	actionObj, exist := a.Context.ActionRegistry.Get(action.Name)
	if !exist {
		return fmt.Errorf("action %s not found in registry", action.Name)
	}

	result, err := actionObj.Start(a.Context, a.Context.Instance)
	if err != nil {
		return err
	}

	// record event detail
	a.EventDetail.StartAction = &event.OperationEventStartAction{
		OperationEventAction: event.OperationEventAction{
			Order: action.Order,
			Name:  action.Name,
		},
		Manual: manual,
	}

	// update status
	return a.PatchInstance(ctx, UpdateStatusAfterStartAction(action.Order, result))
}
