package license

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"runtime"
	"strings"
	"sync"
	"time"
)

const maxResponseBytes = 64 * 1024

// 生产构建通过 -ldflags -X 注入，禁止在源码中放置正式私钥。
var (
	EmbeddedPublicKeyBase64 string
	EmbeddedPublicKeyID     string
)

type Client struct {
	baseURL          string
	httpClient       *http.Client
	keyMu            sync.RWMutex
	publicKey        ed25519.PublicKey
	publicKeyID      string
	requirePinnedKey bool
	initErr          error
	logger           *slog.Logger
	now              func() time.Time
}

type Detail struct {
	Valid   bool
	Type    string
	Version string
}

type Query struct {
	Authorized      bool
	Status          string
	ValidUntil      time.Time
	ExpiresAt       time.Time
	Version         string
	UpdateURL       string
	DBUpdate        bool
	UpdateTime      string
	Changelog       string
	ForceUpdate     bool
	ServicePurchase string
	ServiceExpire   string
	Purchase        string
	Expire          string
	LicenseNo       string
	KeyID           string
	InstanceID      string
	Sequence        uint64
	Features        []string
	IssuedAt        time.Time
}

type RegisterRequest struct {
	LicenseCode  string
	InstanceID   string
	InstanceName string
	NodeRole     string
	SystemName   string
	SystemArch   string
	BackendURL   string
}

type Registration struct {
	Type          string
	Version       string
	InstanceToken string
	ExpiresAt     time.Time
}

type HeartbeatRequest struct {
	InstanceID      string
	ReportedVersion string
	IP              string
	BackendURL      string
}

type HTTPError struct {
	StatusCode int
	Code       string // LICENSE_SUSPENDED, LICENSE_REVOKED, 等
}

func (e *HTTPError) Error() string {
	if e.Code != "" {
		return fmt.Sprintf("license service returned HTTP %d: %s", e.StatusCode, e.Code)
	}
	return fmt.Sprintf("license service returned HTTP %d", e.StatusCode)
}

var (
	ErrLicenseSuspended = errors.New("license is suspended")
	ErrLicenseRevoked   = errors.New("license is revoked")
)

type queryRequest struct {
	LicenseCode string `json:"licenseCode"`
	InstanceID  string `json:"instanceId,omitempty"`
	OS          string `json:"os"`
	CPU         string `json:"cpu"`
	BackendURL  string `json:"backendUrl"`
	Nonce       string `json:"nonce"`
}

type querySignedPayload struct {
	Auth            string   `json:"auth"`
	Status          string   `json:"status"`
	Version         string   `json:"version"`
	UpdateURL       *string  `json:"updateUrl,omitempty"`
	DBUpdate        bool     `json:"dbUpdate"`
	UpdateTime      string   `json:"updateTime"`
	Changelog       string   `json:"changelog"`
	ForceUpdate     bool     `json:"forceUpdate"`
	ServicePurchase string   `json:"servicePurchase"`
	ServiceExpire   string   `json:"serviceExpire"`
	Purchase        string   `json:"purchase"`
	Expire          string   `json:"expire"`
	License         string   `json:"license"`
	LicenseNo       string   `json:"licenseNo"`
	ValidUntil      string   `json:"validUntil"`
	ExpiresAt       string   `json:"expiresAt"`
	Nonce           string   `json:"nonce"`
	IssuedAt        int64    `json:"issuedAt"`
	KeyID           string   `json:"keyId"`
	InstanceID      string   `json:"instanceId"`
	Sequence        uint64   `json:"sequence"`
	Features        []string `json:"features"`
}

type queryResponse struct {
	querySignedPayload
	Signature string `json:"signature"`
}

type publicKeyResponse struct {
	Algorithm string `json:"algorithm"`
	PublicKey string `json:"publicKey"`
	KeyID     string `json:"keyId"`
}

type healthResponse struct {
	Status string `json:"status"`
}

type registerRequest struct {
	LicenseCode  string `json:"licenseCode"`
	InstanceID   string `json:"instanceId"`
	InstanceName string `json:"instanceName"`
	NodeRole     string `json:"nodeRole"`
	SystemName   string `json:"systemName"`
	SystemArch   string `json:"systemArch"`
	BackendURL   string `json:"backendUrl"`
}

type registerResponse struct {
	Valid         bool      `json:"valid"`
	Type          string    `json:"type"`
	Version       string    `json:"version"`
	InstanceToken string    `json:"instanceToken"`
	ExpiresAt     time.Time `json:"expiresAt"`
}

type heartbeatRequest struct {
	InstanceID      string `json:"instanceId"`
	ReportedVersion string `json:"reportedVersion"`
	IP              string `json:"ip"`
	BackendURL      string `json:"backendUrl"`
	Nonce           string `json:"nonce"`
}

type heartbeatSignedPayload struct {
	Status     string   `json:"status"`
	ValidUntil string   `json:"validUntil"`
	ExpiresAt  *string  `json:"expiresAt"`
	LicenseNo  string   `json:"licenseNo"`
	InstanceID string   `json:"instanceId"`
	Sequence   uint64   `json:"sequence"`
	KeyID      string   `json:"keyId"`
	Features   []string `json:"features"`
	Nonce      string   `json:"nonce"`
	IssuedAt   int64    `json:"issuedAt"`
}

type heartbeatResponse struct {
	heartbeatSignedPayload
	Signature string `json:"signature"`
}

// HeartbeatResult 是心跳成功后返回给 manager 的解析结果。
type HeartbeatResult struct {
	Status     string
	ValidUntil time.Time
	ExpiresAt  time.Time
	LicenseNo  string
	KeyID      string
	InstanceID string
	Sequence   uint64
	Features   []string
	IssuedAt   time.Time
	RawBody    []byte
	Signature  string
}

type detailRequest struct {
	LicenseCode string `json:"licenseCode"`
}

type detailResponse struct {
	Valid   bool   `json:"valid"`
	Type    string `json:"type"`
	Version string `json:"version"`
}

func NewClient(baseURL string, timeout time.Duration, logger ...*slog.Logger) *Client {
	var log *slog.Logger
	if len(logger) > 0 {
		log = logger[0]
	}
	baseURL, initErr := validateLicenseBaseURL(baseURL)
	client := &Client{
		baseURL: baseURL,
		httpClient: &http.Client{
			Timeout:   timeout,
			Transport: newLicenseHTTPTransport(),
			CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
				return http.ErrUseLastResponse
			},
		},
		logger:  log,
		now:     time.Now,
		initErr: initErr,
	}
	if key, err := decodePublicKey(EmbeddedPublicKeyBase64); err == nil && len(key) == ed25519.PublicKeySize {
		client.publicKey = key
		client.publicKeyID = strings.TrimSpace(EmbeddedPublicKeyID)
	}
	return client
}

func (c *Client) RequirePinnedKey(required bool) *Client {
	if c != nil {
		c.requirePinnedKey = required
		if required {
			c.keyMu.RLock()
			ready := len(c.publicKey) == ed25519.PublicKeySize && strings.TrimSpace(c.publicKeyID) != ""
			c.keyMu.RUnlock()
			if !ready {
				c.initErr = fmt.Errorf("pinned license public key and key id are required")
			}
		}
	}
	return c
}

func (c *Client) Detail(ctx context.Context, licenseCode string) (Detail, error) {
	if !c.configured() {
		return Detail{}, fmt.Errorf("license service is not configured")
	}
	var decoded detailResponse
	if err := c.postJSON(ctx, "/api/v1/licenses/detail", detailRequest{LicenseCode: strings.TrimSpace(licenseCode)}, &decoded); err != nil {
		return Detail{}, err
	}

	detail := Detail{
		Valid:   decoded.Valid,
		Type:    strings.TrimSpace(decoded.Type),
		Version: strings.TrimSpace(decoded.Version),
	}
	if detail.Valid && (detail.Type == "" || detail.Version == "") {
		return Detail{}, fmt.Errorf("license service returned inconsistent detail")
	}
	return detail, nil
}

func (c *Client) Query(ctx context.Context, licenseCode, instanceID, token, backendURL string) (Query, error) {
	q, _, _, err := c.QueryRaw(ctx, licenseCode, instanceID, token, backendURL)
	return q, err
}

func (c *Client) QueryRaw(ctx context.Context, licenseCode, instanceID, token, backendURL string) (Query, []byte, string, error) {
	if !c.configured() {
		return Query{}, nil, "", fmt.Errorf("license service is not configured")
	}
	code := strings.TrimSpace(licenseCode)
	if code == "" {
		return Query{}, nil, "", fmt.Errorf("license code is empty")
	}
	token = strings.TrimSpace(token)
	if token == "" {
		return Query{}, nil, "", fmt.Errorf("instance token is empty")
	}
	nonce, err := newNonce()
	if err != nil {
		return Query{}, nil, "", fmt.Errorf("create license query nonce: %w", err)
	}
	request := queryRequest{
		LicenseCode: code,
		InstanceID:  strings.TrimSpace(instanceID),
		OS:          runtime.GOOS,
		CPU:         runtime.GOARCH,
		BackendURL:  strings.TrimSpace(backendURL),
		Nonce:       nonce,
	}
	rawBody, response, err := c.postJSONRawWithToken(ctx, "/api/v1/licenses/query", token, request)
	if err != nil {
		return Query{}, nil, "", err
	}
	var parsed queryResponse
	if err := json.Unmarshal(rawBody, &parsed); err != nil {
		return Query{}, nil, "", fmt.Errorf("decode license query response: %w", err)
	}
	if parsed.Nonce != nonce || !strings.EqualFold(parsed.License, code) {
		return Query{}, nil, "", fmt.Errorf("license query response does not match request")
	}
	if err := c.verifyQueryResponseBytes(ctx, rawBody, parsed.Signature); err != nil {
		return Query{}, nil, "", err
	}
	payload, _, err := extractSignedPayloadBytes(rawBody, parsed.Signature)
	if err != nil {
		return Query{}, nil, "", err
	}
	lease, err := decodeLeasePayload(payload)
	if err != nil {
		return Query{}, nil, "", err
	}
	if err := validateLease(lease, c.now(), request.InstanceID, true); err != nil {
		return Query{}, nil, "", err
	}
	if err := c.validateLeaseKeyID(lease.KeyID); err != nil {
		return Query{}, nil, "", err
	}
	if !strings.EqualFold(lease.LicenseCode, code) {
		return Query{}, nil, "", fmt.Errorf("%w: signed license code mismatch", ErrLicenseProtocol)
	}
	authorized := strings.EqualFold(strings.TrimSpace(parsed.Auth), "Yes And Must")
	if lease.Status == "active" && !authorized {
		return Query{}, nil, "", fmt.Errorf("%w: active query is not authorized", ErrLicenseProtocol)
	}
	_ = response

	updateURL := ""
	if parsed.UpdateURL != nil {
		updateURL = strings.TrimSpace(*parsed.UpdateURL)
	}
	q := Query{
		Authorized:      authorized,
		Status:          lease.Status,
		ValidUntil:      lease.ValidUntil,
		ExpiresAt:       lease.ExpiresAt,
		Version:         strings.TrimSpace(parsed.Version),
		UpdateURL:       updateURL,
		DBUpdate:        parsed.DBUpdate,
		UpdateTime:      normalizeOptionalValue(parsed.UpdateTime),
		Changelog:       normalizeOptionalValue(parsed.Changelog),
		ForceUpdate:     parsed.ForceUpdate,
		ServicePurchase: normalizeOptionalValue(parsed.ServicePurchase),
		ServiceExpire:   normalizeOptionalValue(parsed.ServiceExpire),
		Purchase:        normalizeOptionalValue(parsed.Purchase),
		Expire:          normalizeOptionalValue(parsed.Expire),
		LicenseNo:       lease.LicenseNo,
		KeyID:           lease.KeyID,
		InstanceID:      lease.InstanceID,
		Sequence:        lease.Sequence,
		Features:        append([]string(nil), lease.Features...),
		IssuedAt:        lease.IssuedAt,
	}
	return q, rawBody, parsed.Signature, nil
}

func (c *Client) Register(ctx context.Context, input RegisterRequest) (Registration, error) {
	if !c.configured() {
		return Registration{}, fmt.Errorf("license service is not configured")
	}
	request := registerRequest{
		LicenseCode:  strings.TrimSpace(input.LicenseCode),
		InstanceID:   strings.TrimSpace(input.InstanceID),
		InstanceName: strings.TrimSpace(input.InstanceName),
		NodeRole:     strings.TrimSpace(input.NodeRole),
		SystemName:   strings.TrimSpace(input.SystemName),
		SystemArch:   strings.TrimSpace(input.SystemArch),
		BackendURL:   strings.TrimSpace(input.BackendURL),
	}
	var response registerResponse
	if err := c.postJSON(ctx, "/api/v1/instances/register", request, &response); err != nil {
		return Registration{}, err
	}
	response.Type = strings.TrimSpace(response.Type)
	response.Version = strings.TrimSpace(response.Version)
	response.InstanceToken = strings.TrimSpace(response.InstanceToken)
	if response.InstanceToken == "" {
		return Registration{}, fmt.Errorf("license service returned inconsistent registration")
	}
	return Registration{
		Type:          response.Type,
		Version:       response.Version,
		InstanceToken: response.InstanceToken,
		ExpiresAt:     response.ExpiresAt.UTC(),
	}, nil
}

func (c *Client) Heartbeat(ctx context.Context, token string, input HeartbeatRequest) (*HeartbeatResult, error) {
	if !c.configured() {
		return nil, fmt.Errorf("license service is not configured")
	}
	if strings.TrimSpace(token) == "" {
		return nil, fmt.Errorf("instance token is empty")
	}
	nonce, err := newNonce()
	if err != nil {
		return nil, fmt.Errorf("create heartbeat nonce: %w", err)
	}
	request := heartbeatRequest{
		InstanceID:      strings.TrimSpace(input.InstanceID),
		ReportedVersion: strings.TrimSpace(input.ReportedVersion),
		IP:              strings.TrimSpace(input.IP),
		BackendURL:      strings.TrimSpace(input.BackendURL),
		Nonce:           nonce,
	}
	rawBody, _, err := c.postJSONRawWithToken(ctx, "/api/v1/instances/heartbeat", strings.TrimSpace(token), request)
	if err != nil {
		return nil, err
	}
	var parsed heartbeatResponse
	if err := json.Unmarshal(rawBody, &parsed); err != nil {
		return nil, fmt.Errorf("decode heartbeat response: %w", err)
	}
	if parsed.Nonce != nonce {
		return nil, fmt.Errorf("heartbeat nonce mismatch")
	}
	if err := c.verifyQueryResponseBytes(ctx, rawBody, parsed.Signature); err != nil {
		return nil, fmt.Errorf("heartbeat signature verification failed: %w", err)
	}
	payload, _, err := extractSignedPayloadBytes(rawBody, parsed.Signature)
	if err != nil {
		return nil, err
	}
	lease, err := decodeLeasePayload(payload)
	if err != nil {
		return nil, err
	}
	if err := validateLease(lease, c.now(), request.InstanceID, true); err != nil {
		return nil, err
	}
	if err := c.validateLeaseKeyID(lease.KeyID); err != nil {
		return nil, err
	}
	return &HeartbeatResult{
		Status:     lease.Status,
		ValidUntil: lease.ValidUntil,
		ExpiresAt:  lease.ExpiresAt,
		LicenseNo:  lease.LicenseNo,
		KeyID:      lease.KeyID,
		InstanceID: lease.InstanceID,
		Sequence:   lease.Sequence,
		Features:   append([]string(nil), lease.Features...),
		IssuedAt:   lease.IssuedAt,
		RawBody:    rawBody,
		Signature:  parsed.Signature,
	}, nil
}

func (c *Client) verifyQueryResponse(ctx context.Context, response queryResponse) error {
	payload, err := json.Marshal(response.querySignedPayload)
	if err != nil {
		return fmt.Errorf("encode license query response: %w", err)
	}
	signatureText := strings.TrimSpace(response.Signature)
	if !strings.HasPrefix(signatureText, "SIGN-") {
		return fmt.Errorf("license query signature is invalid")
	}
	signature, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(signatureText, "SIGN-"))
	if err != nil || len(signature) != ed25519.SignatureSize {
		return fmt.Errorf("license query signature is invalid")
	}

	key, err := c.getPublicKey(ctx, false)
	if err != nil {
		return err
	}
	if ed25519.Verify(key, payload, signature) {
		return nil
	}
	key, err = c.getPublicKey(ctx, true)
	if err != nil {
		return err
	}
	if !ed25519.Verify(key, payload, signature) {
		return fmt.Errorf("license query signature verification failed")
	}
	return nil
}

// GetPublicKey 返回授权中心 Ed25519 公钥的原始字节，供 Evaluator 注入。
func (c *Client) GetPublicKey(ctx context.Context) ([]byte, error) {
	key, err := c.getPublicKey(ctx, false)
	if err != nil {
		return nil, err
	}
	return []byte(key), nil
}

// Probe verifies that this authorization center is reachable before a
// mutating license request is sent to exactly one selected node.
func (c *Client) Probe(ctx context.Context) error {
	var response healthResponse
	if err := c.getJSON(ctx, "/api/health", &response); err != nil {
		return err
	}
	if strings.TrimSpace(response.Status) != "ok" {
		return fmt.Errorf("license service returned unhealthy status")
	}
	return nil
}

func (c *Client) getPublicKey(ctx context.Context, refresh bool) (ed25519.PublicKey, error) {
	if !refresh {
		c.keyMu.RLock()
		if len(c.publicKey) == ed25519.PublicKeySize {
			key := append(ed25519.PublicKey(nil), c.publicKey...)
			c.keyMu.RUnlock()
			return key, nil
		}
		c.keyMu.RUnlock()
	}
	if c.requirePinnedKey {
		return nil, fmt.Errorf("pinned license public key is required")
	}

	var response publicKeyResponse
	if err := c.getJSON(ctx, "/api/v1/licenses/public-key", &response); err != nil {
		return nil, err
	}
	if response.Algorithm != "Ed25519" {
		return nil, fmt.Errorf("license service returned unsupported public key")
	}
	decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(response.PublicKey))
	if err != nil || len(decoded) != ed25519.PublicKeySize {
		return nil, fmt.Errorf("license service returned invalid public key")
	}
	key := ed25519.PublicKey(append([]byte(nil), decoded...))
	c.keyMu.Lock()
	c.publicKey = append(ed25519.PublicKey(nil), key...)
	c.publicKeyID = strings.TrimSpace(response.KeyID)
	c.keyMu.Unlock()
	return key, nil
}

func (c *Client) postJSON(ctx context.Context, path string, request any, response any) error {
	return c.postJSONWithToken(ctx, path, "", request, response)
}

func (c *Client) postJSONRaw(ctx context.Context, path string, request any) ([]byte, json.RawMessage, error) {
	return c.postJSONRawWithToken(ctx, path, "", request)
}

func (c *Client) postJSONRawWithToken(ctx context.Context, path string, token string, request any) ([]byte, json.RawMessage, error) {
	payload, err := json.Marshal(request)
	if err != nil {
		return nil, nil, fmt.Errorf("encode license request: %w", err)
	}
	if c.logger != nil {
		c.logger.Debug("license request", "method", "POST", "path", path, "body", json.RawMessage(payload))
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(payload))
	if err != nil {
		return nil, nil, fmt.Errorf("create license request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	body, err := c.doRaw(req)
	return body, json.RawMessage(body), err
}

func (c *Client) doRaw(req *http.Request) ([]byte, error) {
	if !c.configured() {
		return nil, fmt.Errorf("license service is not configured")
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request license service: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("read license response: %w", err)
	}
	if c.logger != nil {
		c.logger.Debug("license response", "path", req.URL.Path, "status", resp.StatusCode, "body", json.RawMessage(sanitizeLogBody(body)))
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		var errBody struct {
			Code string `json:"code"`
		}
		_ = json.Unmarshal(body, &errBody)
		return nil, &HTTPError{StatusCode: resp.StatusCode, Code: strings.TrimSpace(errBody.Code)}
	}
	return body, nil
}

func (c *Client) verifyQueryResponseBytes(ctx context.Context, rawBody []byte, signatureField string) error {
	payload, sig, err := extractSignedPayloadBytes(rawBody, signatureField)
	if err != nil {
		return err
	}
	key, err := c.getPublicKey(ctx, false)
	if err != nil {
		return err
	}
	if ed25519.Verify(key, payload, sig) {
		return nil
	}
	key, err = c.getPublicKey(ctx, true)
	if err != nil {
		return err
	}
	if !ed25519.Verify(key, payload, sig) {
		return fmt.Errorf("license query signature verification failed")
	}
	return nil
}

func extractSignedPayloadBytes(body []byte, signatureField string) ([]byte, []byte, error) {
	sigText := strings.TrimPrefix(strings.TrimSpace(signatureField), "SIGN-")
	sig, err := base64.RawURLEncoding.DecodeString(sigText)
	if err != nil || len(sig) != ed25519.SignatureSize {
		return nil, nil, fmt.Errorf("invalid signature format")
	}
	idx := bytes.LastIndex(body, []byte(`,"signature"`))
	if idx < 0 {
		return nil, nil, fmt.Errorf("signature field not found in response body")
	}
	pl := make([]byte, idx+1)
	copy(pl, body[:idx])
	pl[idx] = '}'
	return pl, sig, nil
}

func (c *Client) postJSONWithToken(ctx context.Context, path string, token string, request any, response any) error {
	payload, err := json.Marshal(request)
	if err != nil {
		return fmt.Errorf("encode license request: %w", err)
	}
	if c.logger != nil {
		c.logger.Debug("license request", "method", "POST", "path", path, "body", json.RawMessage(payload))
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(payload))
	if err != nil {
		return fmt.Errorf("create license request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	return c.doJSON(req, response)
}

func (c *Client) getJSON(ctx context.Context, path string, response any) error {
	if c.logger != nil {
		c.logger.Debug("license request", "method", "GET", "path", path)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
	if err != nil {
		return fmt.Errorf("create license request: %w", err)
	}
	return c.doJSON(req, response)
}

func (c *Client) doJSON(req *http.Request, response any) error {
	if !c.configured() {
		return fmt.Errorf("license service is not configured")
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request license service: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return fmt.Errorf("read license response: %w", err)
	}
	if c.logger != nil {
		c.logger.Debug("license response", "path", req.URL.Path, "status", resp.StatusCode, "body", json.RawMessage(sanitizeLogBody(body)))
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		var errBody struct {
			Code string `json:"code"`
		}
		_ = json.Unmarshal(body, &errBody)
		return &HTTPError{StatusCode: resp.StatusCode, Code: strings.TrimSpace(errBody.Code)}
	}
	if err := json.Unmarshal(body, response); err != nil {
		return fmt.Errorf("decode license response: %w", err)
	}
	return nil
}

func sanitizeLogBody(raw []byte) []byte {
	var m map[string]json.RawMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		return []byte(`"<non-json>"`)
	}
	blocked := map[string]bool{
		"instanceToken": true,
		"access_token":  true,
		"signature":     true,
	}
	for k := range blocked {
		if _, ok := m[k]; ok {
			m[k] = json.RawMessage(`"***"`)
		}
	}
	out, _ := json.Marshal(m)
	return out
}

func (c *Client) configured() bool {
	return c != nil && c.httpClient != nil && c.baseURL != "" && c.initErr == nil
}

func (c *Client) validateLeaseKeyID(keyID string) error {
	c.keyMu.RLock()
	trusted := strings.TrimSpace(c.publicKeyID)
	c.keyMu.RUnlock()
	if trusted != "" && strings.TrimSpace(keyID) != trusted {
		return fmt.Errorf("%w: signing key id mismatch", ErrLicenseProtocol)
	}
	return nil
}

func decodePublicKey(value string) (ed25519.PublicKey, error) {
	decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(value))
	if err != nil || len(decoded) != ed25519.PublicKeySize {
		return nil, fmt.Errorf("invalid embedded Ed25519 public key")
	}
	return ed25519.PublicKey(append([]byte(nil), decoded...)), nil
}

func validateLicenseBaseURL(raw string) (string, error) {
	raw = strings.TrimRight(strings.TrimSpace(raw), "/")
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", fmt.Errorf("invalid license service URL")
	}
	if parsed.Scheme != "https" {
		host := parsed.Hostname()
		ip := net.ParseIP(host)
		if parsed.Scheme != "http" || !(strings.EqualFold(host, "localhost") || (ip != nil && ip.IsLoopback())) {
			return "", fmt.Errorf("license service must use HTTPS outside loopback development")
		}
	}
	return raw, nil
}

func newNonce() (string, error) {
	raw := make([]byte, 24)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

func normalizeOptionalValue(value string) string {
	value = strings.TrimSpace(value)
	if strings.EqualFold(value, "None") {
		return ""
	}
	return value
}
