package cluster

import (
	"context"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"

	"github.com/google/uuid"

	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

// zeroIdLen is the exact length (in characters) of a ZeroID.
//
// A ZeroID encodes 24 bytes as base64url without padding:
//   - bytes  0–7:  cluster ID (big-endian uint64)
//   - bytes 8–23: 16-byte random secret
//
// 24 bytes × 8 bits / 6 bits-per-char = 32 characters exactly.
const zeroIdLen = 32

// buildZeroId generates a 32-character ZeroID for the given cluster.
//
// The ZeroID is a base64url encoding of:
//
//	[8-byte cluster ID (big-endian)] || [16-byte random secret]
//
// The secret is encrypted before being persisted, so the database never holds
// the plaintext secret.
func (svc *service) buildZeroId(ctx context.Context, clusterId uint64, sourceIP, tag string) (string, bizerr.BizError) {
	// Generate a 16-byte random secret using the UUID package.
	secret := uuid.New()

	// Encrypt the secret (as lowercase hex) before writing it to the database.
	encryptedSecret, encErr := svc.encryptor.EncryptBase64(ctx, hex.EncodeToString(secret[:]))
	if encErr != nil {
		return "", bizerr.WrapError(encErr, "failed to encrypt zero id secret")
	}

	// Persist the clusterId → encryptedSecret mapping (upsert so that re-renting the
	// same cluster rotates the secret rather than causing a duplicate-key error).
	if err := svc.zeroRentRecordRepo.Upsert(ctx, clusterId, encryptedSecret, sourceIP, tag); err != nil {
		return "", bizerr.WrapGormError(err, "failed to store zero rent record")
	}

	// Build the 24-byte payload and encode it as a 32-character base64url string.
	var payload [24]byte
	binary.BigEndian.PutUint64(payload[:8], clusterId)
	copy(payload[8:], secret[:])

	return base64.RawURLEncoding.EncodeToString(payload[:]), nil
}

// parseZeroId validates a ZeroID and returns the cluster ID it represents.
//
// Validation steps:
//  1. Base64url-decode the 32-character ZeroID to recover the 24-byte payload.
//  2. Extract the cluster ID (first 8 bytes) and the secret (last 16 bytes).
//  3. Look up the encrypted secret stored for that cluster.
//  4. Decrypt the stored secret and compare it to the secret embedded in the ZeroID.
func (svc *service) parseZeroId(ctx context.Context, zeroId string) (uint64, bizerr.BizError) {
	if len(zeroId) != zeroIdLen {
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid zero id: unexpected length")
	}

	payload, err := base64.RawURLEncoding.DecodeString(zeroId)
	if err != nil || len(payload) != 24 {
		return 0, bizerr.New(bizerr.ErrCodeParams, "invalid zero id: cannot decode")
	}

	clusterId := binary.BigEndian.Uint64(payload[:8])
	secretFromUser := payload[8:]

	// Retrieve the encrypted secret that was stored when buildZeroId was called.
	encryptedSecret, dbErr := svc.zeroRentRecordRepo.GetByClusterID(ctx, clusterId)
	if dbErr != nil {
		return 0, bizerr.WrapGormError(dbErr, "failed to retrieve zero rent record")
	}

	// Decrypt and compare.
	storedSecretHex, decErr := svc.encryptor.DecryptBase64(ctx, encryptedSecret)
	if decErr != nil {
		return 0, bizerr.WrapError(decErr, "failed to decrypt zero rent record secret")
	}

	if storedSecretHex != hex.EncodeToString(secretFromUser) {
		return 0, bizerr.New(bizerr.ErrCodePermission, "invalid zero id: authentication failed")
	}

	return clusterId, nil
}
