// Internal NexusCRM admin route for /api/named-credentials.
// This file is intentionally outside the Salesforce v63.0
// product dialect data API contract documented in the API boundary design.
package namedcredentials

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-connectors/features/namedcredential"
	"github.com/pingcap-inc/nexuscrm-connectors/interfaces/rest/internal/hostctx"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/models"
)

const (
	namedCredentialErrorInvalidBody            = "INVALID_BODY"
	namedCredentialErrorInvalidDefinition      = "INVALID_DEFINITION"
	namedCredentialErrorInvalidSecret          = "INVALID_SECRET"
	namedCredentialErrorForbidden              = "FORBIDDEN"
	namedCredentialErrorNotFound               = "NOT_FOUND"
	namedCredentialErrorEncryptionUnconfigured = "ENCRYPTION_UNCONFIGURED"
	namedCredentialErrorInternal               = "INTERNAL_ERROR"
)

// NamedCredentialAdminHandler exposes the thin setup-facing REST surface for
// outbound NamedCredential provisioning on the internal /api namespace.
// Storage, validation, permission checks, encryption, and plaintext resolution
// remain owned by namedcredential.Service.
type NamedCredentialAdminHandler struct {
	svc         *namedcredential.Service
	currentUser func(*gin.Context) *models.UserSession
}

func NewNamedCredentialAdminHandler(svc *namedcredential.Service) *NamedCredentialAdminHandler {
	return NewNamedCredentialAdminHandlerWithCurrentUserProvider(svc, hostctx.CurrentUser)
}

func NewNamedCredentialAdminHandlerWithCurrentUserProvider(svc *namedcredential.Service, currentUser func(*gin.Context) *models.UserSession) *NamedCredentialAdminHandler {
	if currentUser == nil {
		currentUser = hostctx.CurrentUser
	}
	return &NamedCredentialAdminHandler{svc: svc, currentUser: currentUser}
}

type namedCredentialDefinitionRequest struct {
	Label         string `json:"label"`
	Endpoint      string `json:"endpoint"`
	PrincipalType string `json:"principalType"`
	AuthType      string `json:"authType"`
	Description   string `json:"description"`
}

type namedCredentialDefinitionResponse struct {
	DeveloperName    string     `json:"developerName"`
	Label            string     `json:"label"`
	Endpoint         string     `json:"endpoint"`
	PrincipalType    string     `json:"principalType"`
	AuthType         string     `json:"authType"`
	Description      string     `json:"description"`
	CreatedDate      *time.Time `json:"createdDate,omitempty"`
	LastModifiedDate *time.Time `json:"lastModifiedDate,omitempty"`
}

type namedCredentialSecretRequest struct {
	SecretValue string `json:"secretValue"`
}

type namedCredentialSecretResponse struct {
	SecretKind string     `json:"secretKind"`
	MaskedHint string     `json:"maskedHint"`
	RotatedAt  *time.Time `json:"rotatedAt,omitempty"`
}

// ListDefinitions handles GET /named-credentials.
func (h *NamedCredentialAdminHandler) ListDefinitions(c *gin.Context) {
	defs, err := h.svc.ListDefinitions(c.Request.Context(), h.mustGetUser(c))
	if err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}

	out := make([]namedCredentialDefinitionResponse, 0, len(defs))
	for _, def := range defs {
		out = append(out, namedCredentialDefinitionToResponse(def))
	}
	c.JSON(http.StatusOK, gin.H{"namedCredentials": out})
}

// GetDefinition handles GET /named-credentials/:devName.
func (h *NamedCredentialAdminHandler) GetDefinition(c *gin.Context) {
	defName := strings.TrimSpace(c.Param("devName"))
	def, err := h.svc.GetDefinition(c.Request.Context(), h.mustGetUser(c), defName)
	if err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}
	c.JSON(http.StatusOK, namedCredentialDefinitionToResponse(*def))
}

// UpsertDefinition handles PUT /named-credentials/:devName.
func (h *NamedCredentialAdminHandler) UpsertDefinition(c *gin.Context) {
	var req namedCredentialDefinitionRequest
	if !decodeNamedCredentialJSON(c, &req, allowedNamedCredentialDefinitionFields) {
		return
	}

	def := namedcredential.Definition{
		DeveloperName: strings.TrimSpace(c.Param("devName")),
		Label:         req.Label,
		Endpoint:      req.Endpoint,
		PrincipalType: req.PrincipalType,
		AuthType:      req.AuthType,
		Description:   req.Description,
	}
	out, err := h.svc.UpsertDefinition(c.Request.Context(), h.mustGetUser(c), def)
	if err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}
	c.JSON(http.StatusOK, namedCredentialDefinitionToResponse(out))
}

// DeleteDefinition handles DELETE /named-credentials/:devName.
func (h *NamedCredentialAdminHandler) DeleteDefinition(c *gin.Context) {
	defName := strings.TrimSpace(c.Param("devName"))
	if err := h.svc.DeleteDefinition(c.Request.Context(), h.mustGetUser(c), defName); err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}
	c.Status(http.StatusNoContent)
}

// ListSecrets handles GET /named-credentials/:devName/secrets.
func (h *NamedCredentialAdminHandler) ListSecrets(c *gin.Context) {
	defName := strings.TrimSpace(c.Param("devName"))
	secrets, err := h.svc.ListSecretMetadata(c.Request.Context(), h.mustGetUser(c), defName)
	if err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}

	out := make([]namedCredentialSecretResponse, 0, len(secrets))
	for _, secret := range secrets {
		out = append(out, namedCredentialSecretToResponse(secret))
	}
	c.JSON(http.StatusOK, gin.H{"secrets": out})
}

// SetSecret handles POST /named-credentials/:devName/secrets/:kind.
func (h *NamedCredentialAdminHandler) SetSecret(c *gin.Context) {
	if !requestHasJSONContentType(c) {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return
	}

	var req namedCredentialSecretRequest
	if !decodeNamedCredentialJSON(c, &req, allowedNamedCredentialSecretFields) {
		return
	}

	meta, err := h.svc.SetSecret(
		c.Request.Context(),
		h.mustGetUser(c),
		strings.TrimSpace(c.Param("devName")),
		strings.TrimSpace(c.Param("kind")),
		req.SecretValue,
	)
	if err != nil {
		writeNamedCredentialAdminError(c, err)
		return
	}
	c.JSON(http.StatusOK, namedCredentialSecretToResponse(meta))
}

func (h *NamedCredentialAdminHandler) mustGetUser(c *gin.Context) *models.UserSession {
	if h != nil && h.currentUser != nil {
		if user := h.currentUser(c); user != nil {
			return user
		}
	}
	return hostctx.MustCurrentUser(c)
}

var allowedNamedCredentialDefinitionFields = map[string]struct{}{
	"label":         {},
	"endpoint":      {},
	"principalType": {},
	"authType":      {},
	"description":   {},
}

var allowedNamedCredentialSecretFields = map[string]struct{}{
	"secretValue": {},
}

func decodeNamedCredentialJSON(c *gin.Context, dst interface{}, allowed map[string]struct{}) bool {
	var raw map[string]json.RawMessage
	dec := json.NewDecoder(c.Request.Body)
	if err := dec.Decode(&raw); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return false
	}
	if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return false
	}
	if len(raw) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return false
	}
	for key := range raw {
		if _, ok := allowed[key]; !ok {
			c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
			return false
		}
	}
	body, err := json.Marshal(raw)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return false
	}
	if err := json.Unmarshal(body, dst); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidBody})
		return false
	}
	return true
}

func namedCredentialDefinitionToResponse(def namedcredential.Definition) namedCredentialDefinitionResponse {
	return namedCredentialDefinitionResponse{
		DeveloperName:    def.DeveloperName,
		Label:            def.Label,
		Endpoint:         def.Endpoint,
		PrincipalType:    def.PrincipalType,
		AuthType:         def.AuthType,
		Description:      def.Description,
		CreatedDate:      nonZeroTime(def.CreatedDate),
		LastModifiedDate: nonZeroTime(def.LastModifiedDate),
	}
}

func namedCredentialSecretToResponse(meta namedcredential.SecretMetadata) namedCredentialSecretResponse {
	return namedCredentialSecretResponse{
		SecretKind: meta.SecretKind,
		MaskedHint: meta.MaskedHint,
		RotatedAt:  nonZeroTime(meta.RotatedAt),
	}
}

func nonZeroTime(t time.Time) *time.Time {
	if t.IsZero() {
		return nil
	}
	utc := t.UTC()
	return &utc
}

func requestHasJSONContentType(c *gin.Context) bool {
	return strings.Contains(strings.ToLower(c.GetHeader("Content-Type")), "json")
}

func writeNamedCredentialAdminError(c *gin.Context, err error) {
	switch {
	case errors.Is(err, namedcredential.ErrInvalidDefinition):
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidDefinition})
	case errors.Is(err, namedcredential.ErrInvalidSecret):
		c.JSON(http.StatusBadRequest, gin.H{"error": namedCredentialErrorInvalidSecret})
	case errors.Is(err, namedcredential.ErrPermissionDenied):
		c.JSON(http.StatusForbidden, gin.H{"error": namedCredentialErrorForbidden})
	case errors.Is(err, namedcredential.ErrNotFound):
		c.JSON(http.StatusNotFound, gin.H{"error": namedCredentialErrorNotFound})
	case isNamedCredentialEncryptionUnconfigured(err):
		c.JSON(http.StatusInternalServerError, gin.H{"error": namedCredentialErrorEncryptionUnconfigured})
	default:
		c.JSON(http.StatusInternalServerError, gin.H{"error": namedCredentialErrorInternal})
	}
}

func isNamedCredentialEncryptionUnconfigured(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	return strings.Contains(msg, "SecretCipher") && strings.Contains(msg, "env var unset")
}
