package service

import (
	"context"
	"fmt"
	"strings"

	"github.com/pingcap/errors"
	"google.golang.org/grpc/codes"

	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"
	providergcp "github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
)

const (
	gkeHubAPIService                 = "gkehub.googleapis.com"
	anthosPolicyControllerAPIService = "anthospolicycontroller.googleapis.com"

	gkePolicyControlStepEnableServices         = "enable-services"
	gkePolicyControlStepEnableFleet            = "enable-fleet"
	gkePolicyControlStepEnablePolicyController = "enable-policy-controller"
	gkePolicyControlStepCompleted              = "completed"
)

var gkePolicyControlRequiredServices = []string{
	gkeHubAPIService,
	anthosPolicyControllerAPIService,
}

type gkePolicyControlRequest interface {
	GetClusterId() string
	GetProjectId() string
	GetLocation() string
	GetGkeClusterName() string
	GetFleetProjectId() string
	GetMembershipId() string
}

func (s *AgentService) EnableGKEPolicyControl(ctx context.Context, req *apiv1.EnableGKEPolicyControlRequest) (*apiv1.GKEPolicyControl, error) {
	clusterID, target, cli, err := s.resolveGKEPolicyControlRequest(ctx, req)
	if err != nil {
		return nil, toStatusError(err)
	}

	out, err := s.describeGKEPolicyControl(ctx, clusterID, target, cli)
	if err != nil {
		return nil, toStatusError(gkePolicyControlIAMPreconditionError(err))
	}
	if out.State == apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLED ||
		out.State == apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_DEGRADED {
		return out, nil
	}

	switch out.CurrentStep {
	case gkePolicyControlStepEnableServices:
		if len(out.MissingServices) == 0 {
			return out, nil
		}
		ops, err := cli.StartEnableServices(ctx, target.FleetProjectID, out.MissingServices)
		if err != nil {
			return nil, toStatusError(gkePolicyControlIAMPreconditionError(err))
		}
		out.OperationNames = ops
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
		out.Message = "waiting for required services to become enabled"
		return out, nil

	case gkePolicyControlStepEnableFleet:
		ops, err := cli.StartEnableFleet(ctx, target.ProjectID, target.Location, target.ClusterName, target.FleetProjectID)
		if err != nil {
			return nil, toStatusError(gkePolicyControlIAMPreconditionError(err))
		}
		out.OperationNames = ops
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
		out.Message = "waiting for gke cluster fleet membership"
		return out, nil

	case gkePolicyControlStepEnablePolicyController:
		if out.Membership == "" {
			out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
			out.CurrentStep = gkePolicyControlStepEnableFleet
			out.Message = "waiting for gke cluster fleet membership"
			return out, nil
		}
		feature, ops, err := cli.StartEnablePolicyController(ctx, target.FleetProjectID, out.Membership)
		if err != nil {
			return nil, toStatusError(gkePolicyControlIAMPreconditionError(err))
		}
		if feature != "" {
			out.Feature = feature
		}
		out.OperationNames = ops
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
		out.Message = "waiting for policy controller feature to become enabled"
		return out, nil
	default:
		return out, nil
	}
}

func (s *AgentService) GetGKEPolicyControl(ctx context.Context, req *apiv1.GetGKEPolicyControlRequest) (*apiv1.GKEPolicyControl, error) {
	clusterID, target, cli, err := s.resolveGKEPolicyControlRequest(ctx, req)
	if err != nil {
		return nil, toStatusError(err)
	}
	out, err := s.describeGKEPolicyControl(ctx, clusterID, target, cli)
	if err != nil {
		return nil, toStatusError(gkePolicyControlIAMPreconditionError(err))
	}
	return out, nil
}

func (s *AgentService) resolveGKEPolicyControlRequest(ctx context.Context, req gkePolicyControlRequest) (string, gkePolicyControlTarget, GKEPolicyControlClient, error) {
	clusterID, err := s.validateGKEClusterAgentRequest(req.GetClusterId(), "gke policy control APIs")
	if err != nil {
		return "", gkePolicyControlTarget{}, nil, err
	}
	target, err := normalizeGKEPolicyControlTarget(req.GetProjectId(), req.GetLocation(), req.GetGkeClusterName(), req.GetFleetProjectId(), req.GetMembershipId())
	if err != nil {
		return "", gkePolicyControlTarget{}, nil, err
	}
	cli, err := s.getGKEPolicyControlClient(ctx)
	if err != nil {
		return "", gkePolicyControlTarget{}, nil, err
	}
	target, err = s.enforceConfiguredGKEPolicyControlTarget(ctx, cli, target)
	if err != nil {
		return "", gkePolicyControlTarget{}, nil, err
	}
	if err := validateGKEPolicyControlTargetProject(ctx, cli, target); err != nil {
		return "", gkePolicyControlTarget{}, nil, err
	}
	return clusterID, target, cli, nil
}

func (s *AgentService) describeGKEPolicyControl(ctx context.Context, clusterID string, target gkePolicyControlTarget, cli GKEPolicyControlClient) (*apiv1.GKEPolicyControl, error) {
	out := &apiv1.GKEPolicyControl{
		ClusterId:      clusterID,
		ProjectId:      target.ProjectID,
		Location:       target.Location,
		GkeClusterName: target.ClusterName,
		FleetProjectId: target.FleetProjectID,
	}
	enabledServices, missingServices, err := cli.GetServicesStatus(ctx, target.FleetProjectID, gkePolicyControlRequiredServices)
	if err != nil {
		return nil, err
	}
	out.EnabledServices = enabledServices
	out.MissingServices = missingServices
	if len(missingServices) > 0 {
		out.CurrentStep = gkePolicyControlStepEnableServices
		if len(enabledServices) == 0 {
			out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_DISABLED
			out.Message = "required services are not enabled"
		} else {
			out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
			out.Message = "waiting for required services to become enabled"
		}
		return out, nil
	}

	membership, err := cli.GetFleetMembership(ctx, target.ProjectID, target.Location, target.ClusterName)
	if err != nil {
		return nil, err
	}
	out.Membership = membership
	if membership == "" {
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
		out.CurrentStep = gkePolicyControlStepEnableFleet
		out.Message = "waiting for gke cluster fleet membership"
		return out, nil
	}
	if err := validateGKEPolicyControlMembership(ctx, cli, membership, target); err != nil {
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_DEGRADED
		out.CurrentStep = gkePolicyControlStepEnableFleet
		out.Message = err.Error()
		return out, nil
	}

	policyController, err := cli.GetPolicyController(ctx, target.FleetProjectID, membership)
	if err != nil {
		return nil, err
	}
	out.Feature = policyController.FeaturePath
	out.MutationEnabled = policyController.MutationEnabled
	if !policyController.DesiredEnabled || !policyController.MutationEnabled {
		out.State = apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING
		out.CurrentStep = gkePolicyControlStepEnablePolicyController
		out.Message = "waiting for policy controller feature to become enabled"
		return out, nil
	}

	out.State, out.Message = gkePolicyControllerReadiness(policyController)
	if out.State == apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLED {
		out.CurrentStep = gkePolicyControlStepCompleted
	} else {
		out.CurrentStep = gkePolicyControlStepEnablePolicyController
	}
	return out, nil
}

func gkePolicyControllerReadiness(status GKEPolicyControllerStatus) (apiv1.GKEPolicyControlState, string) {
	lifecycleState := strings.TrimSpace(status.LifecycleState)
	mutationState := strings.TrimSpace(status.MutationComponentState)
	enabling := func(state string) bool {
		switch strings.TrimSpace(state) {
		case "", "NOT_INSTALLED", "INSTALLING", "UPDATING":
			return true
		default:
			return false
		}
	}
	if lifecycleState == "ACTIVE" && mutationState == "ACTIVE" {
		return apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLED, "gke policy control is enabled"
	}
	if enabling(lifecycleState) || (lifecycleState == "ACTIVE" && enabling(mutationState)) {
		return apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_ENABLING,
			fmt.Sprintf("waiting for policy controller to become ACTIVE, lifecycle=%q mutation=%q", lifecycleState, mutationState)
	}
	return apiv1.GKEPolicyControlState_GKE_POLICY_CONTROL_STATE_DEGRADED,
		fmt.Sprintf("policy controller is not ready, lifecycle=%q mutation=%q", lifecycleState, mutationState)
}

const gkePolicyControlIAMPermissionMessage = "cluster-agent service account missing GKE Policy Control IAM permissions"

func gkePolicyControlIAMPreconditionError(err error) error {
	if err == nil || (googleAPIStatusCode(err) != 403 && grpcCodeOfError(err) != codes.PermissionDenied) {
		return err
	}
	return wrapGRPCCodeError(codes.FailedPrecondition, err, gkePolicyControlIAMPermissionMessage)
}

func (s *AgentService) getGKEPolicyControlClient(ctx context.Context) (GKEPolicyControlClient, error) {
	if s.gkePolicyControlClient != nil {
		return s.gkePolicyControlClient, nil
	}
	googleCli, err := NewGoogleGKEPolicyControlClient(ctx)
	if err != nil {
		return nil, wrapGRPCCodeError(codes.Internal, err, "create gke policy control client")
	}
	return googleCli, nil
}

func normalizeGKEPolicyControlTarget(projectID, location, clusterName, fleetProjectID, membershipID string) (gkePolicyControlTarget, error) {
	target := gkePolicyControlTarget{
		ProjectID:      projectID,
		Location:       location,
		ClusterName:    clusterName,
		FleetProjectID: fleetProjectID,
		MembershipID:   membershipID,
	}
	target.ProjectID = strings.TrimSpace(target.ProjectID)
	target.Location = strings.TrimSpace(target.Location)
	target.ClusterName = strings.TrimSpace(target.ClusterName)
	target.FleetProjectID = strings.TrimSpace(target.FleetProjectID)
	target.MembershipID = strings.TrimSpace(target.MembershipID)
	if target.ProjectID == "" || target.Location == "" || target.ClusterName == "" || target.FleetProjectID == "" {
		return gkePolicyControlTarget{}, newGRPCCodeError(codes.InvalidArgument, "project_id, location, gke_cluster_name and fleet_project_id are required")
	}
	return target, nil
}

func (s *AgentService) enforceConfiguredGKEPolicyControlTarget(ctx context.Context, cli GKEPolicyControlClient, requestTarget gkePolicyControlTarget) (gkePolicyControlTarget, error) {
	if s == nil || s.gkePolicyControlTarget == nil {
		return gkePolicyControlTarget{}, newGRPCCodeError(codes.FailedPrecondition, "cluster-agent GKE Policy Control target is not configured")
	}
	configured, err := normalizeGKEPolicyControlTarget(
		s.gkePolicyControlTarget.ProjectID,
		s.gkePolicyControlTarget.Location,
		s.gkePolicyControlTarget.ClusterName,
		s.gkePolicyControlTarget.FleetProjectID,
		requestTarget.MembershipID,
	)
	if err != nil {
		return gkePolicyControlTarget{}, wrapGRPCCodeError(codes.FailedPrecondition, err, "cluster-agent GKE Policy Control target is incomplete")
	}
	configured.ProjectID = providergcp.CanonicalProjectIdentifier(configured.ProjectID)
	configured.FleetProjectID = providergcp.CanonicalProjectIdentifier(configured.FleetProjectID)

	// The cluster-agent runs with Google IAM for exactly one GKE cluster target.
	// Request fields are kept for the regional proxy contract, but must never
	// choose the Google resource that this agent mutates.
	if requestTarget.Location != configured.Location {
		return gkePolicyControlTarget{}, newGRPCCodeErrorf(codes.PermissionDenied, "gke policy control location %q does not match this cluster-agent target", requestTarget.Location)
	}
	if requestTarget.ClusterName != configured.ClusterName {
		return gkePolicyControlTarget{}, newGRPCCodeErrorf(codes.PermissionDenied, "gke policy control cluster %q does not match this cluster-agent target", requestTarget.ClusterName)
	}
	if matches, err := gkePolicyControlProjectsEqual(ctx, cli, requestTarget.ProjectID, configured.ProjectID); err != nil {
		return gkePolicyControlTarget{}, wrapGRPCCodeError(codes.FailedPrecondition, err, "gke policy control project cannot be verified against this cluster-agent target")
	} else if !matches {
		return gkePolicyControlTarget{}, newGRPCCodeErrorf(codes.PermissionDenied, "gke policy control project %q does not match this cluster-agent target", requestTarget.ProjectID)
	}
	if matches, err := gkePolicyControlProjectsEqual(ctx, cli, requestTarget.FleetProjectID, configured.FleetProjectID); err != nil {
		return gkePolicyControlTarget{}, wrapGRPCCodeError(codes.FailedPrecondition, err, "gke policy control fleet project cannot be verified against this cluster-agent target")
	} else if !matches {
		return gkePolicyControlTarget{}, newGRPCCodeErrorf(codes.PermissionDenied, "gke policy control fleet project %q does not match this cluster-agent target", requestTarget.FleetProjectID)
	}
	return configured, nil
}

func (t gkePolicyControlTarget) String() string {
	return fmt.Sprintf("project=%s location=%s cluster=%s fleet_project=%s membership=%s", t.ProjectID, t.Location, t.ClusterName, t.FleetProjectID, t.MembershipID)
}

func validateGKEPolicyControlMembership(ctx context.Context, cli GKEPolicyControlClient, membership string, target gkePolicyControlTarget) error {
	projectID, location, membershipID, ok := parseGKEMembershipPath(membership)
	if !ok {
		return newGRPCCodeErrorf(codes.FailedPrecondition, "gke fleet membership %q is not a valid membership path", membership)
	}
	if location != target.Location && location != "global" {
		return newGRPCCodeErrorf(codes.FailedPrecondition, "gke fleet membership %q does not match this cluster-agent target %s", membership, target.String())
	}
	matches, err := gkePolicyControlProjectsEqual(ctx, cli, projectID, target.FleetProjectID)
	if err != nil {
		return wrapGRPCCodeError(codes.FailedPrecondition, err, "gke fleet membership %q project cannot be verified against this cluster-agent target %s", membership, target.String())
	}
	if !matches {
		return newGRPCCodeErrorf(codes.FailedPrecondition, "gke fleet membership %q does not match this cluster-agent target %s", membership, target.String())
	}
	if target.MembershipID != "" && membershipID != target.MembershipID {
		return newGRPCCodeErrorf(codes.FailedPrecondition, "gke fleet membership %q does not match this cluster-agent target %s", membership, target.String())
	}
	return nil
}

func validateGKEPolicyControlTargetProject(ctx context.Context, cli GKEPolicyControlClient, target gkePolicyControlTarget) error {
	// Split cluster/fleet projects need extra IAM policy wiring. Until that is
	// managed, keep the endpoint fail-closed to same-project targets.
	matches, err := gkePolicyControlProjectsEqual(ctx, cli, target.ProjectID, target.FleetProjectID)
	if err != nil {
		return wrapGRPCCodeError(codes.FailedPrecondition, err, "gke policy control project cannot be verified")
	}
	if !matches {
		return newGRPCCodeErrorf(codes.FailedPrecondition, "gke policy control split fleet project %q is not supported until project policy grants fleet-project permissions", target.FleetProjectID)
	}
	return nil
}

func gkePolicyControlProjectsEqual(ctx context.Context, cli GKEPolicyControlClient, leftProject, rightProject string) (bool, error) {
	leftProject = providergcp.CanonicalProjectIdentifier(leftProject)
	rightProject = providergcp.CanonicalProjectIdentifier(rightProject)
	if leftProject == "" || rightProject == "" {
		return false, nil
	}
	if leftProject == rightProject {
		return true, nil
	}
	if providergcp.ProjectIdentifierKind(leftProject) == providergcp.ProjectIdentifierKind(rightProject) {
		return false, nil
	}
	leftIdentity, err := cli.GetProjectIdentity(ctx, leftProject)
	if err != nil {
		return false, errors.Wrapf(err, "get project identity for %q", leftProject)
	}
	rightIdentity, err := cli.GetProjectIdentity(ctx, rightProject)
	if err != nil {
		return false, errors.Wrapf(err, "get project identity for %q", rightProject)
	}
	if leftIdentity.ProjectNumber == "" || rightIdentity.ProjectNumber == "" {
		return false, errors.New("missing project number")
	}
	return leftIdentity.ProjectNumber == rightIdentity.ProjectNumber, nil
}

func parseGKEMembershipPath(membership string) (projectID, location, membershipID string, ok bool) {
	parts := strings.Split(strings.TrimSpace(membership), "/")
	if len(parts) != 6 || parts[0] != "projects" || parts[2] != "locations" || parts[4] != "memberships" {
		return "", "", "", false
	}
	return parts[1], parts[3], parts[5], true
}
