package proxy

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

const (
	gkePolicyControlEnablePathSuffix       = "/gkePolicyControl:enable"
	gkePolicyControlIAMPreconditionMessage = "GKE Policy Control IAM precondition is not ready: controller/regional identity is missing iam.roles.* permissions"
)

func (p *ClusterAgentProxy) EnableGKEPolicyControl(c *gin.Context) {
	if !strings.HasSuffix(c.Request.URL.EscapedPath(), gkePolicyControlEnablePathSuffix) {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	cluster, target, statusCode, errMsg := p.resolveGKEPolicyControlEnableTarget(c)
	if errMsg != "" {
		c.AbortWithStatusJSON(statusCode, gin.H{"error": errMsg})
		return
	}

	statusCode, errMsg = target.appendClusterAgentWorkloadIdentityMember(cluster)
	if errMsg != "" {
		c.AbortWithStatusJSON(statusCode, gin.H{"error": errMsg})
		return
	}

	// Policy Control needs persistent project-level permission; it is not tied to
	// Network lifecycle cleanup.
	for _, identity := range target.identities {
		if err := gcp.EnsureGKEPolicyControlProjectPolicy(
			c.Request.Context(),
			p.policyControlResourceManager,
			target.projectID,
			identity,
		); err != nil {
			c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"error": fmt.Sprintf("%s: %s", gkePolicyControlIAMPreconditionMessage, err)})
			return
		}
	}

	body := target.requestBody()
	c.Request.Body = io.NopCloser(bytes.NewReader(body))
	c.Request.ContentLength = int64(len(body))
	p.proxyToCluster(c, cluster, target.publicClusterID)
}

func (p *ClusterAgentProxy) GetGKEPolicyControl(c *gin.Context) {
	clusterID := strings.TrimSpace(c.Param("clusterId"))
	if clusterID == "" {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "cluster_id is required"})
		return
	}
	cluster, statusCode, errMsg := p.resolveGKEClusterForAgentRequest(c.Request.Context(), clusterID)
	if errMsg != "" {
		c.AbortWithStatusJSON(statusCode, gin.H{"error": errMsg})
		return
	}
	if statusCode, errMsg := validateGKEClusterAgentGateway(cluster, clusterID); errMsg != "" {
		c.AbortWithStatusJSON(statusCode, gin.H{"error": errMsg})
		return
	}
	target, statusCode, errMsg := gkePolicyControlTargetForCluster(clusterID, cluster)
	if errMsg != "" {
		c.AbortWithStatusJSON(statusCode, gin.H{"error": errMsg})
		return
	}
	target.applyQuery(c.Request)
	p.proxyToCluster(c, cluster, target.publicClusterID)
}

type gkePolicyControlTarget struct {
	publicClusterID string
	clusterID       string
	projectID       string
	projectNumber   string
	location        string
	clusterName     string
	fleetProject    string
	identities      []string
}

func (p *ClusterAgentProxy) resolveGKEPolicyControlGetTarget(c *gin.Context) (*cloudv1.Cluster, *gkePolicyControlTarget, int, string) {
	if p == nil || p.kubeClient == nil {
		return nil, nil, http.StatusInternalServerError, "cluster agent proxy is not configured"
	}

	clusterID := strings.TrimSpace(c.Param("clusterId"))
	if clusterID == "" {
		return nil, nil, http.StatusBadRequest, "cluster_id is required"
	}
	cluster, statusCode, errMsg := p.resolveGKEClusterForAgentGateway(c.Request.Context(), clusterID)
	if errMsg != "" {
		return nil, nil, statusCode, errMsg
	}

	target, statusCode, errMsg := gkePolicyControlTargetForCluster(clusterID, cluster)
	if errMsg != "" {
		return nil, nil, statusCode, errMsg
	}
	return cluster, target, 0, ""
}

func (p *ClusterAgentProxy) resolveGKEPolicyControlEnableTarget(c *gin.Context) (*cloudv1.Cluster, *gkePolicyControlTarget, int, string) {
	cluster, target, statusCode, errMsg := p.resolveGKEPolicyControlGetTarget(c)
	if errMsg != "" {
		return nil, nil, statusCode, errMsg
	}
	if p.policyControlResourceManager == nil {
		return nil, nil, http.StatusInternalServerError, "GKE Policy Control IAM preparer is not configured"
	}

	// Enable needs the project number to build the cluster-agent Workload
	// Identity principal. GET only reads current state and lets agent validate.
	statusCode, errMsg = target.resolveProjectIdentity(c.Request.Context(), p.policyControlResourceManager)
	if errMsg != "" {
		return nil, nil, statusCode, errMsg
	}
	return cluster, target, 0, ""
}

func gkePolicyControlTargetForCluster(clusterID string, cluster *cloudv1.Cluster) (*gkePolicyControlTarget, int, string) {
	if cluster == nil {
		return nil, http.StatusInternalServerError, "cluster is required"
	}

	projectID := gcp.CanonicalProjectIdentifier(cluster.Spec.Kubernetes.Account)
	if projectID == "" {
		return nil, http.StatusPreconditionFailed, fmt.Sprintf("cluster %s GKE project is not configured", cluster.Name)
	}
	location := strings.TrimSpace(cluster.GetRegion())
	if location == "" {
		return nil, http.StatusPreconditionFailed, fmt.Sprintf("cluster %s GKE location is not configured", cluster.Name)
	}
	clusterName := strings.TrimSpace(cluster.Spec.Kubernetes.Name)
	if clusterName == "" {
		return nil, http.StatusPreconditionFailed, fmt.Sprintf("cluster %s GKE cluster name is not configured", cluster.Name)
	}

	return &gkePolicyControlTarget{
		publicClusterID: clusterID,
		clusterID:       cluster.Name,
		projectID:       projectID,
		location:        location,
		clusterName:     clusterName,
		fleetProject:    projectID,
		identities:      gkePolicyControlClusterAgentServiceAccounts(cluster),
	}, 0, ""
}

func gkePolicyControlClusterAgentServiceAccounts(cluster *cloudv1.Cluster) []string {
	if cluster == nil {
		return nil
	}

	// Some older dataplane clusters expose a node service account through the
	// metadata server. Keep these bindings as a fallback for that runtime mode.
	candidates := []string{}
	if cluster.Status.NodeIdentity != nil && cluster.Status.NodeIdentity.GCP != nil {
		candidates = append(candidates, cluster.Status.NodeIdentity.GCP.ServiceAccount)
	}
	if cluster.Spec.Kubernetes.Cloud.GCP != nil {
		candidates = append(candidates, cluster.Spec.Kubernetes.Cloud.GCP.AdminNodeSa)
	}

	out := make([]string, 0, len(candidates))
	seen := map[string]struct{}{}
	for _, candidate := range candidates {
		candidate = strings.TrimSpace(candidate)
		if candidate == "" {
			continue
		}
		if _, ok := seen[candidate]; ok {
			continue
		}
		seen[candidate] = struct{}{}
		out = append(out, candidate)
	}
	return out
}

func (t *gkePolicyControlTarget) resolveProjectIdentity(ctx context.Context, resourceManager gcp.ResourceManagerClient) (int, string) {
	if t == nil {
		return http.StatusInternalServerError, "GKE Policy Control target is required"
	}
	projectIdentity, err := resourceManager.GetProjectIdentity(ctx, gcp.CanonicalProjectIdentifier(t.projectID))
	if err != nil {
		return http.StatusPreconditionFailed, fmt.Sprintf("cluster GKE project_id %q cannot be verified: %s", t.projectID, err)
	}
	if projectIdentity.ProjectID == "" {
		return http.StatusPreconditionFailed, fmt.Sprintf("cluster GKE project_id %q cannot be verified: missing project id", t.projectID)
	}
	if projectIdentity.ProjectNumber == "" {
		return http.StatusPreconditionFailed, fmt.Sprintf("cluster GKE project_id %q cannot be verified: missing project number", t.projectID)
	}
	t.projectID = projectIdentity.ProjectID
	t.projectNumber = projectIdentity.ProjectNumber
	t.fleetProject = projectIdentity.ProjectID
	return 0, ""
}

func (t *gkePolicyControlTarget) appendClusterAgentWorkloadIdentityMember(cluster *cloudv1.Cluster) (int, string) {
	if t == nil {
		return http.StatusInternalServerError, "GKE Policy Control target is required"
	}
	if t.projectID == "" || t.projectNumber == "" {
		return http.StatusPreconditionFailed, "cluster GKE project identity is not resolved"
	}
	t.identities = appendUniqueGKEPolicyControlIdentity(t.identities, gkePolicyControlClusterAgentWorkloadIdentityMember(t.projectID, t.projectNumber, cluster))
	return 0, ""
}

type gkePolicyControlEnableRequestBody struct {
	ClusterID      string `json:"cluster_id"`
	ProjectID      string `json:"project_id"`
	Location       string `json:"location"`
	GKEClusterName string `json:"gke_cluster_name"`
	FleetProjectID string `json:"fleet_project_id"`
}

func (t *gkePolicyControlTarget) requestBody() []byte {
	body, _ := json.Marshal(gkePolicyControlEnableRequestBody{
		ClusterID:      t.clusterID,
		ProjectID:      t.projectID,
		Location:       t.location,
		GKEClusterName: t.clusterName,
		FleetProjectID: t.fleetProject,
	})
	return body
}

func (t *gkePolicyControlTarget) applyQuery(req *http.Request) {
	query := req.URL.Query()
	for _, key := range []string{
		"project_id", "projectId",
		"location",
		"gke_cluster_name", "gkeClusterName",
		"fleet_project_id", "fleetProjectId",
		"membership_id", "membershipId",
	} {
		query.Del(key)
	}
	query.Set("project_id", t.projectID)
	query.Set("location", t.location)
	query.Set("gke_cluster_name", t.clusterName)
	query.Set("fleet_project_id", t.fleetProject)
	req.URL.RawQuery = query.Encode()
}

func gkePolicyControlClusterAgentWorkloadIdentityMember(projectID, projectNumber string, cluster *cloudv1.Cluster) string {
	namespace := ""
	name := ""
	if cluster != nil {
		namespace = cluster.PrefixedId()
		name = namer.ClusterAgentServiceAccountName(cluster.Name)
	}
	return fmt.Sprintf(
		"principal://iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s.svc.id.goog/subject/ns/%s/sa/%s",
		strings.TrimSpace(projectNumber),
		strings.TrimSpace(projectID),
		namespace,
		name,
	)
}

func appendUniqueGKEPolicyControlIdentity(identities []string, identity string) []string {
	identity = strings.TrimSpace(identity)
	if identity == "" {
		return identities
	}
	for _, existing := range identities {
		if existing == identity {
			return identities
		}
	}
	return append(identities, identity)
}
