package controllers

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"strings"

	"zrok-orchestration/services"

	"github.com/gin-gonic/gin"
)

// shareNamePattern is the contract the v2 naming helpers assume: a value already safe as-is for a
// CloudWatch log group, an ECS service name and a task definition family. The helpers only lowercase
// what they are given, so a value that misses this pattern reaches AWS unchanged and is rejected
// there, after the stack has already created resources.
var shareNamePattern = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)

type GetTcpShareQuery struct {
	ID string `form:"id" binding:"required"`
}

type DeleteTcpShareQuery struct {
	ID string `form:"id" binding:"required"`
}

// CreateTcpShareReq carries one of two share shapes, never both. A zrok share names share_token;
// a ziti share names service_name and ziti_identity_arn. A stack keeps the shape it was created
// with for its whole life, because the two use different container images.
type CreateTcpShareReq struct {
	Id          string `json:"id" binding:"required"`
	IngressPort int    `json:"ingress_port" binding:"required"`
	// zrok shape
	ShareToken   string `json:"share_token"`
	AccountToken string `json:"account_token"`
	// ziti shape. service_name is the ziti service the container dials. ziti_identity_arn names a
	// Secrets Manager secret holding the container's enrolled identity document. The certificate
	// fields below name SSM parameters instead, because an enrolled identity document is around
	// 10 KB and an SSM parameter value stops at 8 KB.
	ServiceName                string `json:"service_name"`
	ZitiIdentityArn            string `json:"ziti_identity_arn"`
	ShareName                  string `json:"share_name" binding:"omitempty,max=63"`
	ServerCertArn              string `json:"server_cert_arn"`
	ServerKeyArn               string `json:"server_key_arn"`
	ClientCertArn              string `json:"client_cert_arn"`
	ClientCaArn                string `json:"client_ca_arn"`
	ClientCertRevision         string `json:"client_cert_revision"`
	RequiredOrganizationalUnit string `json:"required_organizational_unit"`
	AllowedSubjects            string `json:"allowed_subjects"`
	AllowedCommonNamePrefixes  string `json:"allowed_common_name_prefixes"`
	IpsecFrontendId            string `json:"ipsec_frontend_id"`
	CustomerLabel              string `json:"customer_label"`
	NamingScheme               string `json:"naming_scheme"` // "" (legacy) or "v2"
}

// UpdateTcpShareReq repoints a live share. Every field but the id is optional, and the pointers tell
// an omitted field from a cleared one: a field the body leaves out keeps the value the stack holds,
// while a field the body sends as "" is cleared. Clearing is how a compromised client certificate or
// a stale allow-list is revoked without destroying the share.
type UpdateTcpShareReq struct {
	Id                         string  `json:"id" binding:"required"`
	ServiceName                *string `json:"service_name"`
	ZitiIdentityArn            *string `json:"ziti_identity_arn"`
	IngressPort                int     `json:"ingress_port"`
	ShareName                  *string `json:"share_name" binding:"omitempty,max=63"`
	ServerCertArn              *string `json:"server_cert_arn"`
	ServerKeyArn               *string `json:"server_key_arn"`
	ClientCertArn              *string `json:"client_cert_arn"`
	ClientCaArn                *string `json:"client_ca_arn"`
	ClientCertRevision         *string `json:"client_cert_revision"`
	RequiredOrganizationalUnit *string `json:"required_organizational_unit"`
	AllowedSubjects            *string `json:"allowed_subjects"`
	AllowedCommonNamePrefixes  *string `json:"allowed_common_name_prefixes"`
}

func (controller *Controller) GetFrontdoorTcpShare(c *gin.Context) {
	logFromCtx(c.Request.Context()).Info().Str("method", c.Request.Method).Str("uri", c.Request.RequestURI).Msg("incoming request")
	if len(c.Request.URL.Query()) == 0 {
		controller.listTcpShareStacks(c)
		return
	}
	var query GetTcpShareQuery
	if err := c.ShouldBindQuery(&query); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx := c.Request.Context()
	tcpShareStackInfo, err := controller.TcpShares.GetTcpShareStack(ctx, query.ID)
	if err != nil {
		c.JSON(mapTcpShareError(ctx, err, query.ID), tcpShareStackInfo)
		return
	}
	c.JSON(http.StatusOK, tcpShareStackInfo)
}

func (controller *Controller) listTcpShareStacks(c *gin.Context) {
	ctx := c.Request.Context()
	resp, err := controller.TcpShares.ListTcpShareStacks(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, resp)
		return
	}
	c.JSON(http.StatusOK, resp)
}

func (controller *Controller) CreateFrontdoorTcpShare(c *gin.Context) {
	logFromCtx(c.Request.Context()).Info().Str("method", c.Request.Method).Str("uri", c.Request.RequestURI).Msg("incoming request")
	var req CreateTcpShareReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := validateShareShape(req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := validateCreateNaming(req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	params := services.CreateTcpShareParams{
		Id:                         req.Id,
		IngressPort:                req.IngressPort,
		ShareToken:                 req.ShareToken,
		ServiceName:                req.ServiceName,
		ZitiIdentityArn:            req.ZitiIdentityArn,
		ShareName:                  req.ShareName,
		AccountToken:               req.AccountToken,
		ServerCertArn:              req.ServerCertArn,
		ServerKeyArn:               req.ServerKeyArn,
		ClientCertArn:              req.ClientCertArn,
		ClientCaArn:                req.ClientCaArn,
		ClientCertRevision:         req.ClientCertRevision,
		RequiredOrganizationalUnit: req.RequiredOrganizationalUnit,
		AllowedSubjects:            req.AllowedSubjects,
		AllowedCommonNamePrefixes:  req.AllowedCommonNamePrefixes,
		IpsecFrontendId:            req.IpsecFrontendId,
		CustomerLabel:              req.CustomerLabel,
		NamingScheme:               req.NamingScheme,
	}
	ctx := c.Request.Context()
	tcpShareStackInfo, err := controller.TcpShares.CreateTcpShareStack(ctx, params)
	if err != nil {
		c.JSON(mapTcpShareError(ctx, err, req.Id), tcpShareStackInfo)
		return
	}
	c.JSON(http.StatusOK, tcpShareStackInfo)
}

func (controller *Controller) UpdateFrontdoorTcpShare(c *gin.Context) {
	logFromCtx(c.Request.Context()).Info().Str("method", c.Request.Method).Str("uri", c.Request.RequestURI).Msg("incoming request")
	var req UpdateTcpShareReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := validateUpdateShape(req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if err := validateUpdateNaming(req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	params := services.UpdateTcpShareParams{
		Id:                         req.Id,
		ServiceName:                req.ServiceName,
		ZitiIdentityArn:            req.ZitiIdentityArn,
		IngressPort:                req.IngressPort,
		ShareName:                  req.ShareName,
		ServerCertArn:              req.ServerCertArn,
		ServerKeyArn:               req.ServerKeyArn,
		ClientCertArn:              req.ClientCertArn,
		ClientCaArn:                req.ClientCaArn,
		ClientCertRevision:         req.ClientCertRevision,
		RequiredOrganizationalUnit: req.RequiredOrganizationalUnit,
		AllowedSubjects:            req.AllowedSubjects,
		AllowedCommonNamePrefixes:  req.AllowedCommonNamePrefixes,
	}
	ctx := c.Request.Context()
	tcpShareStackInfo, err := controller.TcpShares.UpdateTcpShareStack(ctx, params)
	if err != nil {
		c.JSON(mapTcpShareError(ctx, err, req.Id), tcpShareStackInfo)
		return
	}
	c.JSON(http.StatusOK, tcpShareStackInfo)
}

// validateShareShape rejects a create that names neither share shape, or both. An IPSec share is a
// zrok share attached to an IPSec frontend, so it is covered by the share_token arm and it cannot
// also be a ziti share: IPSec carries the traffic with TLS off, while a ziti share terminates TLS at
// the ingress and requires a server certificate.
func validateShareShape(req CreateTcpShareReq) error {
	hasZrok := req.ShareToken != ""
	hasZiti := req.ServiceName != ""
	switch {
	case !hasZrok && !hasZiti:
		return errors.New("one of share_token or service_name is required")
	case hasZrok && hasZiti:
		return errors.New("share_token and service_name are mutually exclusive")
	case hasZiti && req.ZitiIdentityArn == "":
		return errors.New("service_name requires ziti_identity_arn")
	case hasZiti && req.IpsecFrontendId != "":
		return errors.New("service_name and ipsec_frontend_id are mutually exclusive")
	}
	return nil
}

// validateUpdateShape rejects clearing a field the share cannot run without. A ziti share dials
// service_name with the identity at ziti_identity_arn, and a ziti share terminates TLS with the
// server certificate and key, so an empty value for one of those leaves a share that cannot start.
// The client-side fields carry no such rule: clearing one is how it is revoked.
func validateUpdateShape(req UpdateTcpShareReq) error {
	required := map[string]*string{
		"service_name":      req.ServiceName,
		"ziti_identity_arn": req.ZitiIdentityArn,
		"server_cert_arn":   req.ServerCertArn,
		"server_key_arn":    req.ServerKeyArn,
	}
	for name, value := range required {
		if value != nil && *value == "" {
			return errors.New(name + " cannot be cleared")
		}
	}
	return nil
}

// validateCreateNaming rejects names the v2 scheme cannot build resource names from. The legacy
// scheme derives its names from the stack id and uses share_name only as a log stream prefix, so it
// needs no check. An unknown scheme is rejected rather than treated as legacy, because the caller
// asked for naming it would not get.
func validateCreateNaming(req CreateTcpShareReq) error {
	switch req.NamingScheme {
	case "", "v2":
	default:
		return fmt.Errorf("naming_scheme %q is not supported, use \"\" or \"v2\"", req.NamingScheme)
	}
	if req.NamingScheme != "v2" {
		return nil
	}
	if err := validateName("share_name", req.ShareName); err != nil {
		return err
	}
	return validateName("customer_label", req.CustomerLabel)
}

// validateUpdateNaming holds a renamed share to the same rule. The update body carries no naming
// scheme, so the name is checked whenever it is set: a legacy stack accepts a looser name today, but
// keeping one rule stops a rename from breaking the share the next time the stack comes up.
func validateUpdateNaming(req UpdateTcpShareReq) error {
	if req.ShareName == nil {
		return nil
	}
	return validateName("share_name", *req.ShareName)
}

// validateName reports whether value is usable as-is in a v2 resource name.
func validateName(field, value string) error {
	if !shareNamePattern.MatchString(value) {
		return fmt.Errorf("%s %q must match %s", field, value, shareNamePattern)
	}
	return nil
}

func (controller *Controller) DeleteFrontdoorTcpShare(c *gin.Context) {
	logFromCtx(c.Request.Context()).Info().Str("method", c.Request.Method).Str("uri", c.Request.RequestURI).Msg("incoming request")
	var query DeleteTcpShareQuery
	if err := c.ShouldBindQuery(&query); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx := c.Request.Context()
	tcpShareStackInfo, err := controller.TcpShares.DeleteTcpShareStack(ctx, query.ID)
	if err != nil {
		c.JSON(mapTcpShareError(ctx, err, query.ID), tcpShareStackInfo)
		return
	}
	c.JSON(http.StatusOK, tcpShareStackInfo)
}

type GetTcpShareProgressQuery struct {
	ID string `form:"id" binding:"required"`
}

func (controller *Controller) GetTcpShareProgress(c *gin.Context) {
	var query GetTcpShareProgressQuery
	if err := c.ShouldBindQuery(&query); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	stackId := strings.ToLower(query.ID)
	ctx := c.Request.Context()
	snap, found, err := controller.TcpShares.Progress.Get(ctx, stackId)
	if err != nil {
		logFromCtx(ctx).Info().Err(err).Msgf("failed to get progress for tcp share %s", stackId)
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if !found {
		c.JSON(http.StatusNotFound, gin.H{"stackId": stackId, "message": "no progress record"})
		return
	}
	c.JSON(http.StatusOK, snap)
}

func mapTcpShareError(ctx context.Context, err error, id string) int {
	switch {
	case errors.Is(err, services.ErrStackNotFound):
		return http.StatusNotFound
	case errors.Is(err, services.ErrStackDuplicate):
		return http.StatusConflict
	case errors.Is(err, services.ErrStackLocked):
		return http.StatusAccepted
	default:
		stackId := strings.ToLower(id)
		logFromCtx(ctx).Info().Msgf("Internal error for tcp share stack %s: %s", stackId, err)
		return http.StatusInternalServerError
	}
}
