package executorgrpc

import (
	"context"
	"fmt"

	execv1pb "github.com/tidbcloud/cluster-service-ng/api/executor/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor"
	"google.golang.org/grpc/codes"
	grpcstatus "google.golang.org/grpc/status"
)

type ExecutorGrpcWrapper struct {
	execuotrs map[string]*executor.Executor
}

func NewExecutorGrpcWrapper() (*ExecutorGrpcWrapper, error) {
	wrapper := new(ExecutorGrpcWrapper)
	wrapper.execuotrs = make(map[string]*executor.Executor)
	return wrapper, nil
}

func (wrapper *ExecutorGrpcWrapper) AddExecutor(scope string, exec *executor.Executor) {
	wrapper.execuotrs[scope] = exec
}

// implement the execv1pb.ExecutorServiceServer interface
func (wrapper *ExecutorGrpcWrapper) Submit(ctx context.Context, req *execv1pb.SubmitRequest) (*execv1pb.SubmitResponse, error) {
	if req.GetScope() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "Scope is empty")
	}
	if req.GetExecId() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "ExecId is empty")
	}
	if req.GetPlanName() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "PlaneName is empty")
	}
	exec, ok := wrapper.execuotrs[req.GetScope()]
	if !ok {
		return nil, grpcstatus.Errorf(codes.FailedPrecondition, "Scope %s is not supported", req.GetScope())
	}
	actionArgs, err := buildActionArgsFromPB(req.GetArgs())
	if err != nil {
		return nil, grpcstatus.Error(codes.Internal, err.Error())
	}
	if err := exec.Submit(req.GetPlanName(), actionArgs, req.GetExecId()); err != nil {
		return nil, grpcstatus.Error(codes.Internal, err.Error())
	}
	return &execv1pb.SubmitResponse{ExecId: req.GetExecId()}, nil
}

func (wrapper *ExecutorGrpcWrapper) Abort(ctx context.Context, req *execv1pb.AbortRequest) (*execv1pb.AbortResponse, error) {
	if req.GetScope() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "Scope is empty")
	}
	if req.GetExecId() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "ExecId is empty")
	}
	exec, ok := wrapper.execuotrs[req.GetScope()]
	if !ok {
		return nil, grpcstatus.Errorf(codes.FailedPrecondition, "Scope %s is not supported", req.GetScope())
	}
	if err := exec.Abort(req.GetExecId()); err != nil {
		return nil, grpcstatus.Error(codes.Internal, err.Error())
	}
	execStatusStg := exec.GetExecStatusStorage()
	if err := execStatusStg.SetTaskFinished(req.GetExecId()); err != nil {
		return nil, grpcstatus.Error(codes.Internal, err.Error())
	}
	return &execv1pb.AbortResponse{ExecId: req.GetExecId()}, nil
}

func (wrapper *ExecutorGrpcWrapper) QueryRunnings(ctx context.Context, req *execv1pb.QueryRunningsRequest) (*execv1pb.QueryRunningsResponse, error) {
	if req.GetScope() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "Scope is empty")
	}
	if req.GetPlanName() == "" {
		return nil, grpcstatus.Error(codes.InvalidArgument, "PlaneName is empty")
	}
	out := new(execv1pb.QueryRunningsResponse)
	out.ExecTaskIdStatus = make(map[string]*execv1pb.ExecTaskStatusEx)
	exec, ok := wrapper.execuotrs[req.GetScope()]
	if !ok {
		return nil, grpcstatus.Errorf(codes.FailedPrecondition, "scope %s is not supported", req.GetScope())
	}

	// Build match function from match_args for server-side filtering
	matchFunc := buildMatchFuncFromPB(req.GetMatchArgs())
	taskIdStatus, err := exec.QueryRunnings(req.GetPlanName(), matchFunc)
	if err != nil {
		return nil, grpcstatus.Error(codes.Internal, err.Error())
	}
	for id, statusEx := range taskIdStatus {
		statusExPB := new(execv1pb.ExecTaskStatusEx)
		statusExPB.State = string(statusEx.State)
		execTask := statusEx.ExecTask
		if execTask != nil {
			statusExPB.ExecTask = new(execv1pb.ExecutorTask)
			statusExPB.ExecTask.RootName = execTask.RootName
			statusExPB.ExecTask.ExecId = execTask.ExecId
			argsPB, err := actions.BuildPBFromActionArgs(execTask.Args)
			if err != nil {
				return nil, grpcstatus.Error(codes.Internal, err.Error())
			}
			statusExPB.ExecTask.Args = argsPB
		}
		out.ExecTaskIdStatus[id] = statusExPB
	}
	return out, nil
}

// buildMatchFuncFromPB converts proto MatchArgs into a QueryMatchFunc.
// If matchArgs is empty, returns an always-match function.
// If matchArgs is non-empty but all entries have empty/nil values, returns a match-none function
// to avoid accidentally matching all tasks.
func buildMatchFuncFromPB(matchArgs map[string]*execv1pb.MatchArgValues) executor.QueryMatchFunc {
	if len(matchArgs) == 0 {
		return func(args actions.ActionArgs) (bool, error) {
			return true, nil
		}
	}
	filter := executor.NewMatchFilter()
	for key, values := range matchArgs {
		if values == nil || len(values.GetValues()) == 0 {
			// Any entry with empty values means the constraint can never be
			// satisfied (tasks must match *all* entries), so short-circuit
			// to match-none.
			return func(args actions.ActionArgs) (bool, error) {
				return false, nil
			}
		}
		filter.Add(key, values.GetValues())
	}
	return filter.ToMatchFunc()
}

func buildActionArgsFromPB(in map[string]*execv1pb.ActionArg) (actions.ActionArgs, error) {
	out := make(actions.ActionArgs)
	for k, v := range in {
		switch t := v.GetArg().(type) {
		case *execv1pb.ActionArg_ActionArgInt:
			out[k] = actions.ActionArgInt(int(v.GetActionArgInt()))
		case *execv1pb.ActionArg_ActionArgString:
			out[k] = actions.ActionArgString(v.GetActionArgString())
		default:
			return nil, fmt.Errorf("unsupported action args pb type: %T", t)
		}
	}
	return out, nil
}
