package license

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"os"
	"runtime"
	"strings"
	"sync"
	"time"
	"unicode"
)

var publicKeyRetryDelays = []time.Duration{time.Second, 3 * time.Second, 5 * time.Second}

const (
	legacyTokenRefreshInterval = time.Hour
)

var (
	ErrUnavailable          = errors.New("authorization is unavailable")
	ErrReactivationRequired = errors.New("explicit license reactivation is required")
)

type Remote interface {
	Detail(context.Context, string) (Detail, error)
	Register(context.Context, RegisterRequest) (Registration, error)
	QueryRaw(context.Context, string, string, string, string) (Query, []byte, string, error)
	Heartbeat(context.Context, string, HeartbeatRequest) (*HeartbeatResult, error)
	GetPublicKey(context.Context) ([]byte, error)
}

type PersistenceError struct {
	Err error
}

func (e *PersistenceError) Error() string {
	return "persist authorization state: " + e.Err.Error()
}

func (e *PersistenceError) Unwrap() error {
	return e.Err
}

type Manager struct {
	remote            Remote
	store             StateRepository
	eval              *Evaluator
	appVersion        string
	defaultBackendURL string
	now               func() time.Time
	hostname          func() (string, error)
	mu                sync.Mutex
	hwc               *HighWaterClock
	prober            ConnectivityProber
	graceDur          time.Duration
}

func NewManager(remote Remote, store StateRepository, appVersion string, defaultBackendURL ...string) *Manager {
	var backend string
	if len(defaultBackendURL) > 0 {
		backend = strings.TrimSpace(defaultBackendURL[0])
	}
	return &Manager{
		remote:            remote,
		store:             store,
		appVersion:        strings.TrimSpace(appVersion),
		defaultBackendURL: backend,
		now:               time.Now,
		hostname:          os.Hostname,
	}
}

func (m *Manager) WithEvaluator(eval *Evaluator) *Manager {
	m.eval = eval
	return m
}

// WithHighWaterClock attaches a clock used to detect system clock rollbacks
// and to compute elapsed-since-last-heartbeat during outages.
func (m *Manager) WithHighWaterClock(hwc *HighWaterClock) *Manager {
	m.hwc = hwc
	return m
}

// WithConnectivityProber attaches a prober used to distinguish "genuine
// network outage" from "authorization servers selectively blocked" when all
// configured remotes are unreachable.
func (m *Manager) WithConnectivityProber(p ConnectivityProber) *Manager {
	m.prober = p
	return m
}

// WithGraceDuration overrides the default 3-hour offline grace window.
func (m *Manager) WithGraceDuration(d time.Duration) *Manager {
	m.graceDur = d
	return m
}

func (m *Manager) Activate(ctx context.Context, code string, backendURL string) error {
	if !m.configured() {
		return fmt.Errorf("authorization manager is not configured")
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	code = strings.TrimSpace(code)
	detail, err := m.remote.Detail(ctx, code)
	if err != nil {
		var httpError *HTTPError
		if !errors.As(err, &httpError) || httpError.StatusCode != 404 {
			return fmt.Errorf("check authorization: %w", err)
		}
	}
	if err == nil && !detail.Valid {
		return ErrUnavailable
	}

	identity, err := m.ensureRuntimeIdentity(ctx, backendURL)
	if err != nil {
		return err
	}
	return m.registerQueryAndPersist(ctx, code, identity, detail)
}

func (m *Manager) Report(ctx context.Context) error {
	if !m.configured() {
		return nil
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	state, err := m.store.Load(ctx)
	if err != nil {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return &PersistenceError{Err: err}
	}
	if strings.TrimSpace(state.Code) == "" {
		return nil
	}

	identity, err := m.ensureRuntimeIdentity(ctx, state.Identity.BackendURL)
	if err != nil {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return err
	}
	state.Identity = identity
	storedStatus := persistedLeaseStatus(state)
	if strings.TrimSpace(state.InstanceToken) == "" {
		m.blockForStoredStatus(storedStatus)
		return ErrReactivationRequired
	}

	result, err := m.remote.Heartbeat(ctx, state.InstanceToken, HeartbeatRequest{
		InstanceID:      identity.InstanceID,
		ReportedVersion: m.reportedVersion(),
		BackendURL:      identity.BackendURL,
	})
	if err != nil {
		var quorumError *QuorumRejectionError
		if errors.As(err, &quorumError) {
			m.blockForQuorumRejection(quorumError)
			if quorumError.Last != nil && quorumError.Last.StatusCode == 401 {
				return fmt.Errorf("%w: authorization nodes rejected the instance token", ErrReactivationRequired)
			}
			return fmt.Errorf("report authorization heartbeat: %w", err)
		}
		var httpError *HTTPError
		if errors.As(err, &httpError) && httpError.StatusCode == 401 {
			m.blockForStoredStatus(storedStatus)
			return fmt.Errorf("%w: instance token rejected", ErrReactivationRequired)
		}
		return m.handleUnreachableHeartbeat(ctx, state, storedStatus, err)
	}
	if result == nil {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return fmt.Errorf("report authorization heartbeat: %w", ErrLicenseProtocol)
	}
	nextLease := Lease{LicenseNo: result.LicenseNo, InstanceID: result.InstanceID, Sequence: result.Sequence}
	if state.LeaseScope != "" && state.LeaseScope != nextLease.Scope() {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return fmt.Errorf("%w: heartbeat lease scope changed", ErrLicenseProtocol)
	}
	if err := compareLeaseSequence(state.LeaseScope, state.LeaseSequence, nextLease); err != nil {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return err
	}

	// 状态或权限收缩必须立即反映到 L1，不能等待本地持久化完成。
	l1Applied := false
	if m.eval != nil && heartbeatRestrictsState(state, result) {
		if len(result.RawBody) == 0 || m.eval.LoadFromQuery(result.RawBody, result.Signature) != nil {
			if result.Status == "suspended" || result.Status == "revoked" {
				m.eval.SetBlocked(result.Status)
			} else {
				m.eval.SetUnavailable()
			}
		} else {
			l1Applied = true
		}
	}

	now := m.now().UTC()
	state.VerifiedAt = now
	state.LastHeartbeatAt = now
	if len(result.RawBody) > 0 {
		state.LeaseRaw = append([]byte(nil), result.RawBody...)
		state.LeaseSignature = strings.TrimSpace(result.Signature)
		state.LeaseScope = nextLease.Scope()
		state.LeaseSequence = result.Sequence
		state.Features = append([]string(nil), result.Features...)
	}
	if m.hwc != nil {
		if issuedAt := leaseIssuedAt(state.LeaseRaw, state.LeaseSignature); !issuedAt.IsZero() {
			m.hwc.Observe(issuedAt)
		}
	}
	state.HighWaterMark = m.hwcNow()
	state.SelectiveBlockDetected = false

	if err := m.store.SaveHeartbeat(ctx, state); err != nil {
		if m.eval != nil && result.Status == "active" {
			m.eval.SetUnavailable()
		}
		return &PersistenceError{Err: err}
	}
	// active 状态先成功落盘，再替换 L1，避免进程内状态领先于持久化状态。
	if m.eval != nil && result.Status == "active" && !l1Applied && len(result.RawBody) > 0 {
		if loadErr := m.eval.LoadFromQuery(result.RawBody, result.Signature); loadErr == nil {
			m.eval.ClearForceBlock()
		}
	}
	return nil
}

func (m *Manager) blockForQuorumRejection(rejection *QuorumRejectionError) {
	if m.eval == nil {
		return
	}
	status := "authorization_rejected"
	if rejection != nil && rejection.Last != nil && strings.TrimSpace(rejection.Last.Code) != "" {
		status = strings.ToLower(strings.TrimSpace(rejection.Last.Code))
	}
	m.eval.SetBlocked(status)
}

func persistedLeaseStatus(state State) string {
	if len(state.LeaseRaw) == 0 || strings.TrimSpace(state.LeaseSignature) == "" {
		return ""
	}
	payload, _, err := extractSignedPayloadBytes(state.LeaseRaw, state.LeaseSignature)
	if err != nil {
		return ""
	}
	lease, err := decodeLeasePayload(payload)
	if err != nil {
		return ""
	}
	return lease.Status
}

// persistedLeaseExpiresAt extracts the hard-expiry time from the last
// persisted signed lease. Returns the zero time if no valid lease is stored.
func persistedLeaseExpiresAt(state State) time.Time {
	if len(state.LeaseRaw) == 0 || strings.TrimSpace(state.LeaseSignature) == "" {
		return time.Time{}
	}
	payload, _, err := extractSignedPayloadBytes(state.LeaseRaw, state.LeaseSignature)
	if err != nil {
		return time.Time{}
	}
	lease, err := decodeLeasePayload(payload)
	if err != nil {
		return time.Time{}
	}
	return lease.ExpiresAt
}

// leaseIssuedAt extracts the server-signed issue time from a signed lease
// blob. Returns the zero time if the blob is empty or invalid.
func leaseIssuedAt(rawBody []byte, signature string) time.Time {
	if len(rawBody) == 0 || strings.TrimSpace(signature) == "" {
		return time.Time{}
	}
	payload, _, err := extractSignedPayloadBytes(rawBody, signature)
	if err != nil {
		return time.Time{}
	}
	lease, err := decodeLeasePayload(payload)
	if err != nil {
		return time.Time{}
	}
	return lease.IssuedAt
}

func (m *Manager) blockForStoredStatus(status string) {
	if m.eval == nil {
		return
	}
	if status == "suspended" || status == "revoked" {
		m.eval.SetBlocked(status)
		return
	}
	m.eval.SetUnavailable()
}

// handleUnreachableHeartbeat decides how to react when the heartbeat request
// itself failed (network error/timeout/5xx — NOT a 401, which is handled
// separately). If the license was already known to be suspended/revoked, the
// block is reapplied unconditionally. Otherwise a connectivity probe
// distinguishes a genuine outage (grace period) from a selective block on
// just the authorization servers (immediate unrecoverable block).
func (m *Manager) handleUnreachableHeartbeat(ctx context.Context, state State, storedStatus string, heartbeatErr error) error {
	if storedStatus == "suspended" || storedStatus == "revoked" {
		if m.eval != nil {
			m.eval.SetBlocked(storedStatus)
		}
		return fmt.Errorf("report authorization heartbeat: %w", heartbeatErr)
	}

	probe := ProbeResultUnknown
	if m.prober != nil {
		probe = m.prober.Probe(ctx)
	}
	if probe == ProbeResultSelectiveBlock {
		state.SelectiveBlockDetected = true
		state.HighWaterMark = m.hwcNow()
		_ = m.store.SaveHeartbeat(ctx, state)
		if m.eval != nil {
			m.eval.SetBlocked("selective_block")
		}
		return fmt.Errorf("selective block detected: authorization servers unreachable while public internet is reachable")
	}

	if state.LastHeartbeatAt.IsZero() {
		if m.eval != nil {
			m.eval.SetUnavailable()
		}
		return fmt.Errorf("report authorization heartbeat: %w", heartbeatErr)
	}
	elapsed := m.hwcNow().Sub(state.LastHeartbeatAt)
	if elapsed <= m.graceWindow() {
		expiresAt := persistedLeaseExpiresAt(state)
		if m.eval != nil {
			m.eval.SetGrace(expiresAt, state.LastHeartbeatAt.Add(m.graceWindow()))
		}
	} else if m.eval != nil {
		m.eval.SetUnavailable()
	}
	return fmt.Errorf("report authorization heartbeat: %w", heartbeatErr)
}

func heartbeatRestrictsState(state State, result *HeartbeatResult) bool {
	if result == nil || result.Status != "active" {
		return true
	}
	if len(state.LeaseRaw) == 0 || strings.TrimSpace(state.LeaseSignature) == "" {
		return false
	}
	payload, _, err := extractSignedPayloadBytes(state.LeaseRaw, state.LeaseSignature)
	if err != nil {
		return true
	}
	previous, err := decodeLeasePayload(payload)
	if err != nil {
		return true
	}
	if result.ValidUntil.Before(previous.ValidUntil) || result.ExpiresAt.Before(previous.ExpiresAt) {
		return true
	}
	nextFeatures := make(map[string]struct{}, len(result.Features))
	for _, feature := range result.Features {
		nextFeatures[strings.TrimSpace(feature)] = struct{}{}
	}
	for _, feature := range previous.Features {
		if _, ok := nextFeatures[feature]; !ok {
			return true
		}
	}
	return false
}

func (m *Manager) State(ctx context.Context) (State, error) {
	if m == nil || m.store == nil {
		return State{}, fmt.Errorf("authorization state is not configured")
	}
	return m.store.Load(ctx)
}

// InitEvaluator 在进程启动时调用：
//  1. 检测系统时钟回拨、恢复高水位时钟、检查是否存在此前检测到的选择性封锁
//  2. 从授权中心拉取公钥并注入 Evaluator，失败时按 1s/3s/5s 重试；若全部失败但本地
//     存在最近一次心跳凭据，则进入离线宽限期，否则返回 error
//  3. 本地凭据只用于恢复授权码、实例和 Token；必须在线 Query 成功后才加载 L1
func (m *Manager) InitEvaluator(ctx context.Context) error {
	if m == nil || m.eval == nil || m.remote == nil {
		return nil
	}
	if m.store == nil {
		return m.initEvaluatorFetchKeyOnly(ctx)
	}

	state, loadErr := m.store.Load(ctx)
	hasLocalCredentials := loadErr == nil && state.Configured()

	if hasLocalCredentials && !state.HighWaterMark.IsZero() {
		wallNow := m.now()
		if wallNow.Before(state.HighWaterMark.Add(-clockSkewTolerance)) {
			return fmt.Errorf("%w: wall=%s mark=%s", ErrLicenseClockRolledBack,
				wallNow.UTC().Format(time.RFC3339), state.HighWaterMark.UTC().Format(time.RFC3339))
		}
		if m.hwc != nil {
			m.hwc.Bootstrap(state.HighWaterMark)
		}
	}

	if hasLocalCredentials && state.SelectiveBlockDetected {
		if m.eval != nil {
			m.eval.SetBlocked("selective_block")
		}
		return fmt.Errorf("selective block was previously detected; clear by completing a successful heartbeat")
	}

	keyErr := m.fetchAndSetPublicKey(ctx)
	if keyErr != nil {
		if !hasLocalCredentials {
			return fmt.Errorf("fetch license public key after retries: %w", keyErr)
		}
		if state.LastHeartbeatAt.IsZero() {
			return fmt.Errorf("online license validation required: %w", keyErr)
		}
		elapsed := m.hwcNow().Sub(state.LastHeartbeatAt)
		if elapsed > m.graceWindow() {
			return fmt.Errorf("online license validation required: last heartbeat %s ago exceeds grace window: %w", elapsed.Round(time.Minute), keyErr)
		}
		expiresAt := persistedLeaseExpiresAt(state)
		if m.eval != nil {
			m.eval.SetGrace(expiresAt, state.LastHeartbeatAt.Add(m.graceWindow()))
		}
		return nil
	}

	if !hasLocalCredentials {
		return nil
	}

	query, rawBody, sigField, err := m.remote.QueryRaw(ctx, state.Code, state.Identity.InstanceID, state.InstanceToken, state.Identity.BackendURL)
	if err != nil || len(rawBody) == 0 {
		m.eval.SetUnavailable()
		var httpError *HTTPError
		if errors.As(err, &httpError) && httpError.StatusCode == 401 {
			return fmt.Errorf("%w: stored instance token rejected", ErrReactivationRequired)
		}
		if err == nil {
			err = ErrLicenseProtocol
		}
		return fmt.Errorf("online license validation required: %w", err)
	}
	nextLease := Lease{LicenseNo: query.LicenseNo, InstanceID: query.InstanceID, Sequence: query.Sequence}
	if state.LeaseScope != "" && state.LeaseScope != nextLease.Scope() {
		m.eval.SetUnavailable()
		return fmt.Errorf("%w: query lease scope changed", ErrLicenseProtocol)
	}
	if err := compareLeaseSequence(state.LeaseScope, state.LeaseSequence, nextLease); err != nil {
		m.eval.SetUnavailable()
		return err
	}
	if err := m.eval.LoadFromQuery(rawBody, sigField); err != nil {
		return err
	}
	state.VerifiedAt = m.now().UTC()
	state.LeaseRaw = append([]byte(nil), rawBody...)
	state.LeaseSignature = strings.TrimSpace(sigField)
	state.LeaseScope = nextLease.Scope()
	state.LeaseSequence = query.Sequence
	state.Features = append([]string(nil), query.Features...)
	if m.hwc != nil {
		if issuedAt := leaseIssuedAt(state.LeaseRaw, state.LeaseSignature); !issuedAt.IsZero() {
			m.hwc.Observe(issuedAt)
		}
	}
	state.HighWaterMark = m.hwcNow()
	state.SelectiveBlockDetected = false
	if err := m.store.SaveHeartbeat(ctx, state); err != nil {
		return &PersistenceError{Err: err}
	}
	return nil
}

// initEvaluatorFetchKeyOnly handles the case where no StateRepository is
// configured — there is no local credential cache to fall back on, so the
// public key fetch must succeed unconditionally.
func (m *Manager) initEvaluatorFetchKeyOnly(ctx context.Context) error {
	if err := m.fetchAndSetPublicKey(ctx); err != nil {
		return fmt.Errorf("fetch license public key after retries: %w", err)
	}
	return nil
}

// fetchAndSetPublicKey retries fetching the authorization center's Ed25519
// public key with 1s/3s/5s backoff and installs it into the evaluator on
// success. Returns the last error if all attempts fail.
func (m *Manager) fetchAndSetPublicKey(ctx context.Context) error {
	var keyErr error
	for _, delay := range append([]time.Duration{0}, publicKeyRetryDelays...) {
		if delay > 0 {
			select {
			case <-ctx.Done():
				return fmt.Errorf("fetch license public key: %w", ctx.Err())
			case <-time.After(delay):
			}
		}
		key, err := m.remote.GetPublicKey(ctx)
		if err == nil && len(key) == 32 {
			m.eval.SetPublicKey(key)
			return nil
		}
		keyErr = err
	}
	return keyErr
}

func (m *Manager) registerQueryAndPersist(ctx context.Context, code string, identity Identity, fallback Detail) error {
	registration, err := m.remote.Register(ctx, RegisterRequest{
		LicenseCode:  code,
		InstanceID:   identity.InstanceID,
		InstanceName: identity.InstanceName,
		NodeRole:     identity.NodeRole,
		SystemName:   identity.SystemName,
		SystemArch:   identity.SystemArch,
		BackendURL:   identity.BackendURL,
	})
	if err != nil {
		return fmt.Errorf("register authorization instance: %w", err)
	}
	if registration.ExpiresAt.IsZero() {
		registration.ExpiresAt = m.now().Add(legacyTokenRefreshInterval)
	}
	if !registration.ExpiresAt.After(m.now()) {
		return ErrUnavailable
	}

	query, _, _, err := m.remote.QueryRaw(ctx, code, identity.InstanceID, registration.InstanceToken, identity.BackendURL)
	if err != nil {
		return fmt.Errorf("query authorization: %w", err)
	}
	if !query.Authorized || query.Status != "active" || !query.ExpiresAt.After(m.now()) || strings.TrimSpace(query.LicenseNo) == "" || strings.TrimSpace(query.Version) == "" {
		return ErrUnavailable
	}
	registration.Type = strings.TrimSpace(registration.Type)
	registration.Version = strings.TrimSpace(registration.Version)
	if registration.Type == "" {
		registration.Type = strings.TrimSpace(fallback.Type)
	}
	if registration.Version == "" {
		registration.Version = strings.TrimSpace(fallback.Version)
	}
	if registration.Type == "" || registration.Version == "" {
		detail, detailErr := m.remote.Detail(ctx, code)
		if detailErr == nil && detail.Valid {
			if registration.Type == "" {
				registration.Type = strings.TrimSpace(detail.Type)
			}
			if registration.Version == "" {
				registration.Version = strings.TrimSpace(detail.Version)
			}
		}
	}
	if registration.Type == "" || registration.Version == "" {
		return fmt.Errorf("license service returned incomplete registration metadata")
	}

	verifiedAt := m.now().UTC()
	initResult, err := m.remote.Heartbeat(ctx, registration.InstanceToken, HeartbeatRequest{
		InstanceID:      identity.InstanceID,
		ReportedVersion: m.reportedVersion(),
		BackendURL:      identity.BackendURL,
	})
	if err != nil {
		return fmt.Errorf("report initial authorization heartbeat: %w", err)
	}
	if initResult != nil && (initResult.Status == "suspended" || initResult.Status == "revoked") {
		return ErrUnavailable
	}
	if initResult == nil || initResult.Status != "active" || len(initResult.RawBody) == 0 {
		return fmt.Errorf("initial authorization heartbeat returned invalid lease")
	}
	queryLease := Lease{LicenseNo: query.LicenseNo, InstanceID: query.InstanceID, Sequence: query.Sequence}
	heartbeatLease := Lease{LicenseNo: initResult.LicenseNo, InstanceID: initResult.InstanceID, Sequence: initResult.Sequence}
	if queryLease.Scope() != heartbeatLease.Scope() || initResult.Sequence <= query.Sequence {
		return fmt.Errorf("%w: initial heartbeat sequence did not advance", ErrLicenseSequenceStale)
	}
	state := State{
		Code:            strings.TrimSpace(code),
		Type:            registration.Type,
		Version:         registration.Version,
		VerifiedAt:      verifiedAt,
		Identity:        identity,
		InstanceToken:   registration.InstanceToken,
		TokenExpiresAt:  registration.ExpiresAt.UTC(),
		LastHeartbeatAt: verifiedAt,
		LeaseRaw:        append([]byte(nil), initResult.RawBody...),
		LeaseSignature:  strings.TrimSpace(initResult.Signature),
		LeaseScope:      heartbeatLease.Scope(),
		LeaseSequence:   initResult.Sequence,
		Features:        append([]string(nil), initResult.Features...),
	}
	if err := m.store.SaveActivation(ctx, state); err != nil {
		return &PersistenceError{Err: err}
	}
	if m.eval != nil {
		if loadErr := m.eval.LoadFromQuery(initResult.RawBody, initResult.Signature); loadErr != nil {
			m.eval.SetUnavailable()
			return fmt.Errorf("load activated license into memory: %w", loadErr)
		}
		m.eval.ClearForceBlock()
	}
	return nil
}

func (m *Manager) ensureRuntimeIdentity(ctx context.Context, backendURL string) (Identity, error) {
	identity, err := m.runtimeIdentity(backendURL)
	if err != nil {
		return Identity{}, err
	}
	identity, err = m.store.EnsureIdentity(ctx, identity)
	if err != nil {
		return Identity{}, &PersistenceError{Err: err}
	}
	return identity, nil
}

func (m *Manager) runtimeIdentity(backendURL string) (Identity, error) {
	if runtime.GOOS != "linux" && runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
		return Identity{}, fmt.Errorf("unsupported operating system")
	}
	if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
		return Identity{}, fmt.Errorf("unsupported system architecture")
	}
	hostname, err := m.hostname()
	if err != nil {
		hostname = ""
	}
	hostname = normalizeDisplayName(hostname, 128)
	if hostname == "" {
		hostname = "qycw"
	}
	normalized := NormalizeBackendURL(backendURL)
	if normalized == "" {
		normalized = NormalizeBackendURL(m.defaultBackendURL)
	}
	return Identity{
		InstanceName: hostname,
		NodeRole:     "master",
		SystemName:   runtime.GOOS,
		SystemArch:   runtime.GOARCH,
		BackendURL:   normalized,
	}, nil
}

func (m *Manager) reportedVersion() string {
	version := normalizeDisplayName(m.appVersion, 64)
	if version == "" {
		return "unknown"
	}
	return version
}

func (m *Manager) configured() bool {
	return m != nil && m.remote != nil && m.store != nil && m.now != nil && m.hostname != nil
}

// hwcNow returns the high-water clock's current time if one is attached,
// otherwise falls back to the Manager's own now() function.
func (m *Manager) hwcNow() time.Time {
	if m.hwc != nil {
		return m.hwc.Now()
	}
	return m.now()
}

// graceWindow returns the configured offline grace duration, defaulting to
// 3 hours when unset.
func (m *Manager) graceWindow() time.Duration {
	if m.graceDur > 0 {
		return m.graceDur
	}
	return 3 * time.Hour
}

func NormalizeBackendURL(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
		raw = "http://" + raw
	}
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Host == "" {
		return ""
	}
	result := parsed.Scheme + "://" + parsed.Host
	if len(result) > 512 {
		return ""
	}
	return result
}

func normalizeDisplayName(value string, maxRunes int) string {
	value = strings.TrimSpace(value)
	runes := make([]rune, 0, len(value))
	for _, character := range value {
		if unicode.IsControl(character) {
			continue
		}
		runes = append(runes, character)
		if len(runes) == maxRunes {
			break
		}
	}
	return strings.TrimSpace(string(runes))
}
