package license

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"aijianzhan/platform/internal/config"
)

const (
	DefaultMaxExtensions    = 1 // 临时延期总共只能用 1 次
	DefaultExtensionMaxDays = 30
	signVersion             = "v1"
	leaseFilePrefix         = "lease-"
	consumedFileName        = "_consumed.json" // 本机已消费 id 账本（签名），保证离线一文件一用
)

// Lease 单次签发的授权记录。每次续费/延期/写入都会生成独立文件，不覆盖历史。
type Lease struct {
	ID               string    `json:"id"` // 本条租约唯一 ID
	Customer         string    `json:"customer"`
	NotAfter         time.Time `json:"not_after"`
	ExtensionsUsed   int       `json:"extensions_used"`
	ExtensionsMax    int       `json:"extensions_max"`
	ExtensionMaxDays int       `json:"extension_max_days"`
	UpdatedAt        time.Time `json:"updated_at"`
	UpdatedBy        string    `json:"updated_by,omitempty"`
	Note             string    `json:"note,omitempty"`
	Signature        string    `json:"signature,omitempty"`
	FileName         string    `json:"file_name,omitempty"` // 落盘文件名（不参与签名）
}

// Status 对外状态（取目录中「签名有效且最新」的那一份）。
type Status struct {
	Enabled             bool      `json:"enabled"`
	Customer            string    `json:"customer,omitempty"`
	NotAfter            time.Time `json:"not_after,omitempty"`
	GraceDays           int       `json:"grace_days"`
	Expired             bool      `json:"expired"`
	Message             string    `json:"message,omitempty"`
	ExtensionsUsed      int       `json:"extensions_used"`
	ExtensionsRemaining int       `json:"extensions_remaining"`
	ExtensionMaxDays    int       `json:"extension_max_days"`
	LeaseDir            string    `json:"lease_dir,omitempty"`
	ActiveFile          string    `json:"active_file,omitempty"`
	LeaseID             string    `json:"lease_id,omitempty"`
	LeaseCount          int       `json:"lease_count"` // 目录内签名有效且仍可作为候选的文件数
	ConsumedCount       int       `json:"consumed_count"`
	UpdatedAt           time.Time `json:"updated_at,omitempty"`
	UpdatedBy           string    `json:"updated_by,omitempty"`
	SignatureOK         bool      `json:"signature_ok"`
}

type Manager struct {
	mu     sync.RWMutex
	cfg    config.LicenseConf
	dir    string
	state  string
	cur    *Lease
	mirror ConsumedMirror
}

func resolveLeaseDir(cfg config.LicenseConf) string {
	if d := strings.TrimSpace(cfg.LeaseDir); d != "" {
		return d
	}
	p := strings.TrimSpace(cfg.LeasePath)
	if p == "" {
		return "./data/license/leases"
	}
	if strings.HasSuffix(strings.ToLower(p), ".json") {
		return filepath.Join(filepath.Dir(p), "leases")
	}
	return p
}

func resolveStateDir(cfg config.LicenseConf, leaseDir string) string {
	if d := strings.TrimSpace(cfg.StateDir); d != "" {
		return d
	}
	// 与 leases 同级的 state，删租约目录不会清掉消费记录
	return filepath.Join(filepath.Dir(leaseDir), "state")
}

func NewManager(cfg config.LicenseConf) (*Manager, error) {
	dir := resolveLeaseDir(cfg)
	state := resolveStateDir(cfg, dir)
	m := &Manager{cfg: cfg, dir: dir, state: state}
	if !cfg.Enabled {
		return m, nil
	}
	if _, err := m.signKey(); err != nil {
		return nil, err
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, err
	}
	if err := os.MkdirAll(state, 0o755); err != nil {
		return nil, err
	}
	m.migrateLegacyConsumed()
	if err := m.reload(); err != nil {
		if !os.IsNotExist(err) {
			if isSigErr(err) {
				m.cur = nil
				return m, nil
			}
			return nil, err
		}
		seed, _ := ParseNotAfter(cfg.SeedNotAfter)
		if seed.IsZero() {
			seed, _ = ParseNotAfter(cfg.NotAfter)
		}
		if seed.IsZero() {
			seed = time.Now().UTC().AddDate(0, 0, -1)
		}
		lease := &Lease{
			Customer:         strings.TrimSpace(cfg.Customer),
			NotAfter:         seed,
			ExtensionsUsed:   0,
			ExtensionsMax:    DefaultMaxExtensions,
			ExtensionMaxDays: DefaultExtensionMaxDays,
			UpdatedAt:        time.Now().UTC(),
			UpdatedBy:        "bootstrap",
			Note:             "signed lease file; each renew/extend writes a new file",
		}
		if err := m.persistLocked(lease); err != nil {
			return nil, err
		}
		if err := m.activateLocked(lease); err != nil {
			return nil, err
		}
		m.cur = lease
	}
	return m, nil
}

func (m *Manager) AttachMirror(store ConsumedMirror) {
	if m == nil || store == nil {
		return
	}
	m.mu.Lock()
	m.mirror = store
	m.mu.Unlock()
	// 启动时用 DB 补全本地账本
	key, err := m.signKey()
	if err != nil {
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	led, _ := m.loadConsumed(key)
	if led == nil {
		led = &consumedLedger{IDs: map[string]struct{}{}}
	}
	_ = m.mergeMirrorLocked(led)
	_ = m.saveConsumed(led, key)
}

func (m *Manager) migrateLegacyConsumed() {
	legacy := filepath.Join(m.dir, consumedFileName)
	dest := m.consumedPath()
	if _, err := os.Stat(dest); err == nil {
		return
	}
	b, err := os.ReadFile(legacy)
	if err != nil {
		return
	}
	_ = os.WriteFile(dest, b, 0o644)
}

func (m *Manager) Path() string { return m.dir }

func (m *Manager) StatePath() string { return m.state }

func (m *Manager) Enabled() bool {
	return m != nil && m.cfg.Enabled
}

func (m *Manager) signKey() ([]byte, error) {
	s := strings.TrimSpace(m.cfg.SignSecret)
	if s == "" {
		s = strings.TrimSpace(m.cfg.ControlSecret)
	}
	if s == "" {
		return nil, fmt.Errorf("License.ControlSecret（或 SignSecret）未配置，无法签发/校验租约")
	}
	return []byte(s), nil
}

func (m *Manager) reload() error {
	key, err := m.signKey()
	if err != nil {
		return err
	}
	ledger, ledErr := m.loadConsumed(key)
	if ledErr != nil && !os.IsNotExist(ledErr) {
		return ledErr // 账本被篡改：拒绝生效（防清账复用）
	}
	if ledger == nil {
		ledger = &consumedLedger{IDs: map[string]struct{}{}}
	}

	entries, err := os.ReadDir(m.dir)
	if err != nil {
		return err
	}
	var valid []*Lease
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !strings.HasPrefix(name, leaseFilePrefix) || !strings.HasSuffix(strings.ToLower(name), ".json") {
			continue
		}
		lease, lerr := m.readFile(filepath.Join(m.dir, name), key)
		if lerr != nil {
			continue
		}
		lease.FileName = name
		valid = append(valid, lease)
	}
	if len(valid) == 0 {
		return os.ErrNotExist
	}

	// 首次无账本：把已有全部 id 记为已消费，仅最新一份可作 active（防把旧文件再当「新延期」用）
	if ledErr != nil && os.IsNotExist(ledErr) {
		var newest *Lease
		for _, l := range valid {
			ledger.IDs[l.ID] = struct{}{}
			if newest == nil || leaseNewer(l, newest) {
				newest = l
			}
		}
		ledger.ActiveID = newest.ID
		if err := m.saveConsumed(ledger, key); err != nil {
			return err
		}
		m.cur = newest
		return nil
	}

	var best *Lease
	for _, l := range valid {
		if !ledger.usable(l.ID) {
			continue // 已消费且非当前 active → 同一份文件不能再次生效
		}
		if best == nil || leaseNewer(l, best) {
			best = l
		}
	}
	if best == nil {
		return sigErr("无可用租约：有效文件均已消费，请使用新的延期包")
	}
	if best.ID != ledger.ActiveID {
		ledger.IDs[best.ID] = struct{}{}
		ledger.ActiveID = best.ID
		if err := m.saveConsumed(ledger, key); err != nil {
			return err
		}
	}
	m.cur = best
	return nil
}

func (m *Manager) consumedPath() string {
	return filepath.Join(m.state, consumedFileName)
}

func (m *Manager) mergeMirrorLocked(led *consumedLedger) error {
	if m.mirror == nil || led == nil {
		return nil
	}
	active, ids, err := m.mirror.Load(context.Background())
	if err != nil {
		return nil // DB 暂不可用时不阻断，靠本地账本
	}
	for _, id := range ids {
		id = strings.TrimSpace(id)
		if id != "" {
			led.IDs[id] = struct{}{}
		}
	}
	if active != "" && led.ActiveID == "" {
		led.ActiveID = active
	}
	if led.ActiveID != "" {
		led.IDs[led.ActiveID] = struct{}{}
	}
	return nil
}

type consumedLedger struct {
	ActiveID string
	IDs      map[string]struct{}
}

func (c *consumedLedger) usable(id string) bool {
	if id == "" {
		return false
	}
	if c.ActiveID == id {
		return true
	}
	_, used := c.IDs[id]
	return !used
}

type consumedFile struct {
	ActiveID  string   `json:"active_id"`
	IDs       []string `json:"ids"`
	UpdatedAt string   `json:"updated_at"`
	Signature string   `json:"signature"`
}

func (m *Manager) loadConsumed(key []byte) (*consumedLedger, error) {
	b, err := os.ReadFile(m.consumedPath())
	if err != nil {
		// 本地无账本：尝试从 DB 恢复（防只删 state 文件）
		led := &consumedLedger{IDs: map[string]struct{}{}}
		if m.mirror != nil {
			_ = m.mergeMirrorLocked(led)
			if len(led.IDs) > 0 {
				return led, nil
			}
		}
		return nil, err
	}
	var raw consumedFile
	if err := json.Unmarshal(b, &raw); err != nil {
		return nil, sigErr("消费账本损坏")
	}
	payload := consumedCanonical(raw.ActiveID, raw.IDs, raw.UpdatedAt)
	want := signPayload(payload, key)
	if !hmac.Equal([]byte(strings.ToLower(raw.Signature)), []byte(strings.ToLower(want))) {
		return nil, sigErr("消费账本签名校验失败（疑似清账复用）")
	}
	out := &consumedLedger{
		ActiveID: raw.ActiveID,
		IDs:      map[string]struct{}{},
	}
	for _, id := range raw.IDs {
		id = strings.TrimSpace(id)
		if id != "" {
			out.IDs[id] = struct{}{}
		}
	}
	if out.ActiveID != "" {
		out.IDs[out.ActiveID] = struct{}{}
	}
	_ = m.mergeMirrorLocked(out)
	return out, nil
}

func (m *Manager) saveConsumed(led *consumedLedger, key []byte) error {
	ids := make([]string, 0, len(led.IDs))
	for id := range led.IDs {
		ids = append(ids, id)
	}
	sortStrings(ids)
	ua := time.Now().UTC().Format(time.RFC3339)
	raw := consumedFile{
		ActiveID:  led.ActiveID,
		IDs:       ids,
		UpdatedAt: ua,
		Signature: signPayload(consumedCanonical(led.ActiveID, ids, ua), key),
	}
	b, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(m.state, 0o755); err != nil {
		return err
	}
	path := m.consumedPath()
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, b, 0o644); err != nil {
		return err
	}
	if err := os.Rename(tmp, path); err != nil {
		return err
	}
	if m.mirror != nil {
		_ = m.mirror.Save(context.Background(), led.ActiveID, ids)
	}
	return nil
}

func consumedCanonical(active string, ids []string, updatedAt string) string {
	cp := append([]string(nil), ids...)
	sortStrings(cp)
	return strings.Join([]string{
		signVersion,
		"consumed",
		active,
		strings.Join(cp, ","),
		updatedAt,
	}, "|")
}

func sortStrings(a []string) {
	sort.Strings(a)
}

// ImportSigned 离线导入延期软件签发的租约文件：验签后落盘；同一 id 只能生效一次。
func (m *Manager) ImportSigned(rawJSON []byte, by string) (*Lease, error) {
	if !m.Enabled() {
		return nil, fmt.Errorf("license 未启用")
	}
	key, err := m.signKey()
	if err != nil {
		return nil, err
	}
	var file leaseFile
	if err := json.Unmarshal(rawJSON, &file); err != nil {
		return nil, fmt.Errorf("租约 JSON 无效: %w", err)
	}
	lease, err := file.toLease()
	if err != nil {
		return nil, err
	}
	if err := verifyLease(lease, key); err != nil {
		return nil, err
	}
	m.mu.Lock()
	defer m.mu.Unlock()

	ledger, ledErr := m.loadConsumed(key)
	if ledErr != nil && !os.IsNotExist(ledErr) {
		return nil, ledErr
	}
	if ledger == nil {
		ledger = &consumedLedger{IDs: map[string]struct{}{}}
		_ = m.mergeMirrorLocked(ledger)
	}
	if _, used := ledger.IDs[lease.ID]; used {
		return nil, fmt.Errorf("该延期文件已使用过（id=%s），不可重复导入", lease.ID)
	}
	// 配置了核销地址：必须联网把 id 核销掉；删光本地后旧包也会被服务端拒绝
	if strings.TrimSpace(m.cfg.RedeemURL) != "" {
		if err := m.redeemOnline(lease.ID, "redeem"); err != nil {
			return nil, err
		}
	}

	by = sanitizeFilePart(nonempty(by, "import"))
	name := fmt.Sprintf("%s%s-%s-%s.json",
		leaseFilePrefix,
		lease.UpdatedAt.UTC().Format("20060102T150405Z"),
		by,
		lease.ID,
	)
	full := filepath.Join(m.dir, name)
	// 原样写入（保留延期软件签名）
	pretty, err := json.MarshalIndent(file, "", "  ")
	if err != nil {
		return nil, err
	}
	tmp := full + ".tmp"
	if err := os.WriteFile(tmp, pretty, 0o644); err != nil {
		return nil, err
	}
	if err := os.Rename(tmp, full); err != nil {
		_ = os.Remove(tmp)
		return nil, err
	}
	lease.FileName = name
	ledger.IDs[lease.ID] = struct{}{}
	ledger.ActiveID = lease.ID
	if err := m.saveConsumed(ledger, key); err != nil {
		return nil, err
	}
	m.cur = lease
	return cloneLease(lease), nil
}

// MintSignedJSON 延期软件离线签发：生成带签名的 JSON（不落盘、不消费）；客户机 ImportSigned 后才会生效且仅一次。
func (m *Manager) MintSignedJSON(lease *Lease) ([]byte, error) {
	if !m.Enabled() {
		return nil, fmt.Errorf("license 未启用")
	}
	key, err := m.signKey()
	if err != nil {
		return nil, err
	}
	if lease == nil {
		return nil, fmt.Errorf("lease 为空")
	}
	if lease.ID == "" {
		lease.ID = newLeaseID()
	}
	if lease.UpdatedAt.IsZero() {
		lease.UpdatedAt = time.Now().UTC()
	}
	if lease.ExtensionsMax <= 0 {
		lease.ExtensionsMax = DefaultMaxExtensions
	}
	if lease.ExtensionMaxDays <= 0 {
		lease.ExtensionMaxDays = DefaultExtensionMaxDays
	}
	lease.Signature = signPayload(canonical(lease), key)
	raw := leaseFile{
		ID:               lease.ID,
		Customer:         lease.Customer,
		NotAfter:         lease.NotAfter.UTC().Format(time.RFC3339),
		ExtensionsUsed:   lease.ExtensionsUsed,
		ExtensionsMax:    lease.ExtensionsMax,
		ExtensionMaxDays: lease.ExtensionMaxDays,
		UpdatedAt:        lease.UpdatedAt.UTC().Format(time.RFC3339),
		UpdatedBy:        lease.UpdatedBy,
		Note:             lease.Note,
		Signature:        lease.Signature,
	}
	return json.MarshalIndent(raw, "", "  ")
}

func leaseNewer(a, b *Lease) bool {
	if a.UpdatedAt.After(b.UpdatedAt) {
		return true
	}
	if a.UpdatedAt.Equal(b.UpdatedAt) && a.NotAfter.After(b.NotAfter) {
		return true
	}
	if a.UpdatedAt.Equal(b.UpdatedAt) && a.NotAfter.Equal(b.NotAfter) && a.ID > b.ID {
		return true
	}
	return false
}

func (m *Manager) readFile(path string, key []byte) (*Lease, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var raw leaseFile
	if err := json.Unmarshal(b, &raw); err != nil {
		return nil, fmt.Errorf("parse lease: %w", err)
	}
	lease, err := raw.toLease()
	if err != nil {
		return nil, err
	}
	if err := verifyLease(lease, key); err != nil {
		return nil, err
	}
	return lease, nil
}

func (m *Manager) countValidLocked(key []byte) int {
	entries, err := os.ReadDir(m.dir)
	if err != nil {
		return 0
	}
	n := 0
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !strings.HasPrefix(name, leaseFilePrefix) || !strings.HasSuffix(strings.ToLower(name), ".json") {
			continue
		}
		if _, err := m.readFile(filepath.Join(m.dir, name), key); err == nil {
			n++
		}
	}
	return n
}

func (m *Manager) load() (*Lease, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if err := m.reload(); err != nil {
		return nil, err
	}
	return cloneLease(m.cur), nil
}

func (m *Manager) Status(now time.Time) Status {
	st := Status{
		Enabled:             m.cfg.Enabled,
		GraceDays:           m.cfg.GraceDays,
		LeaseDir:            m.dir,
		ExtensionMaxDays:    DefaultExtensionMaxDays,
		ExtensionsRemaining: DefaultMaxExtensions,
	}
	if !m.cfg.Enabled {
		return st
	}
	lease, err := m.load()
	key, _ := m.signKey()
	m.mu.Lock()
	if key != nil {
		st.LeaseCount = m.countValidLocked(key)
		if led, e := m.loadConsumed(key); e == nil && led != nil {
			st.ConsumedCount = len(led.IDs)
		}
	}
	m.mu.Unlock()
	if err != nil || lease == nil {
		st.Expired = true
		st.SignatureOK = false
		msg := "授权租约无效或签名校验失败（禁止外部篡改文件）"
		if err != nil && isSigErr(err) {
			msg = err.Error()
		} else if err != nil {
			msg = "授权租约不可用，请用延期软件签发新文件后导入"
		}
		if mcfg := strings.TrimSpace(m.cfg.Message); mcfg != "" && !isSigErr(err) {
			msg = mcfg
		}
		st.Message = msg
		return st
	}
	st.SignatureOK = true
	st.ActiveFile = lease.FileName
	st.LeaseID = lease.ID
	st.Customer = lease.Customer
	if st.Customer == "" {
		st.Customer = strings.TrimSpace(m.cfg.Customer)
	}
	st.NotAfter = lease.NotAfter
	st.UpdatedAt = lease.UpdatedAt
	st.UpdatedBy = lease.UpdatedBy
	st.ExtensionsUsed = lease.ExtensionsUsed
	maxExt := lease.ExtensionsMax
	if maxExt <= 0 || maxExt > DefaultMaxExtensions {
		maxExt = DefaultMaxExtensions
	}
	maxDays := lease.ExtensionMaxDays
	if maxDays <= 0 || maxDays > DefaultExtensionMaxDays {
		maxDays = DefaultExtensionMaxDays
	}
	st.ExtensionMaxDays = maxDays
	remain := maxExt - lease.ExtensionsUsed
	if remain < 0 {
		remain = 0
	}
	st.ExtensionsRemaining = remain

	deadline := lease.NotAfter
	if m.cfg.GraceDays > 0 {
		deadline = lease.NotAfter.AddDate(0, 0, m.cfg.GraceDays)
	}
	if now.UTC().After(deadline) {
		st.Expired = true
		msg := strings.TrimSpace(m.cfg.Message)
		if msg == "" {
			msg = "授权已过期，请联系宇信达续费或使用临时延期"
		}
		st.Message = msg
	}
	return st
}

func (m *Manager) Renew(notAfter time.Time, by, note string) (*Lease, error) {
	if !m.Enabled() {
		return nil, fmt.Errorf("license 未启用")
	}
	if notAfter.IsZero() {
		return nil, fmt.Errorf("not_after 无效")
	}
	if notAfter.UTC().Before(time.Now().UTC()) {
		return nil, fmt.Errorf("续费到期日不能早于当前时间")
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	_ = m.reload()
	cur := m.cur
	if cur == nil {
		cur = &Lease{
			Customer:         strings.TrimSpace(m.cfg.Customer),
			ExtensionsMax:    DefaultMaxExtensions,
			ExtensionMaxDays: DefaultExtensionMaxDays,
		}
	}
	next := &Lease{
		Customer:         cur.Customer,
		NotAfter:         notAfter.UTC(),
		ExtensionsUsed:   0,
		ExtensionsMax:    nonzero(cur.ExtensionsMax, DefaultMaxExtensions),
		ExtensionMaxDays: nonzero(cur.ExtensionMaxDays, DefaultExtensionMaxDays),
		UpdatedAt:        time.Now().UTC(),
		UpdatedBy:        nonempty(by, "renew"),
		Note:             note,
	}
	if next.Customer == "" {
		next.Customer = strings.TrimSpace(m.cfg.Customer)
	}
	if err := m.persistLocked(next); err != nil {
		return nil, err
	}
	if err := m.activateLocked(next); err != nil {
		return nil, err
	}
	m.cur = next
	return cloneLease(next), nil
}

func (m *Manager) Extend(days int, by, note string) (*Lease, error) {
	if !m.Enabled() {
		return nil, fmt.Errorf("license 未启用")
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	if err := m.reload(); err != nil {
		return nil, fmt.Errorf("当前租约无效，无法延期: %w", err)
	}
	cur := m.cur
	if cur == nil {
		return nil, fmt.Errorf("租约不存在")
	}
	maxExt := nonzero(cur.ExtensionsMax, DefaultMaxExtensions)
	if maxExt > DefaultMaxExtensions {
		maxExt = DefaultMaxExtensions
	}
	maxDays := nonzero(cur.ExtensionMaxDays, DefaultExtensionMaxDays)
	if maxDays > DefaultExtensionMaxDays {
		maxDays = DefaultExtensionMaxDays
	}
	if days <= 0 {
		return nil, fmt.Errorf("延期天数须大于 0")
	}
	if days > maxDays {
		return nil, fmt.Errorf("每次临时延期最多 %d 天", maxDays)
	}
	if cur.ExtensionsUsed >= maxExt {
		return nil, fmt.Errorf("临时延期已使用过（仅允许 1 次），请正式续费")
	}
	base := cur.NotAfter
	now := time.Now().UTC()
	if base.Before(now) {
		base = now
	}
	next := &Lease{
		Customer:         cur.Customer,
		NotAfter:         base.AddDate(0, 0, days),
		ExtensionsUsed:   cur.ExtensionsUsed + 1,
		ExtensionsMax:    maxExt,
		ExtensionMaxDays: maxDays,
		UpdatedAt:        now,
		UpdatedBy:        nonempty(by, "extend"),
		Note:             note,
	}
	if err := m.persistLocked(next); err != nil {
		return nil, err
	}
	if err := m.activateLocked(next); err != nil {
		return nil, err
	}
	m.cur = next
	return cloneLease(next), nil
}

func (m *Manager) PutLease(in *Lease, by string) (*Lease, error) {
	if !m.Enabled() {
		return nil, fmt.Errorf("license 未启用")
	}
	if in == nil || in.NotAfter.IsZero() {
		return nil, fmt.Errorf("not_after 必填")
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	_ = m.reload()
	next := &Lease{
		Customer:         strings.TrimSpace(in.Customer),
		NotAfter:         in.NotAfter.UTC(),
		ExtensionsUsed:   in.ExtensionsUsed,
		ExtensionsMax:    in.ExtensionsMax,
		ExtensionMaxDays: in.ExtensionMaxDays,
		UpdatedAt:        time.Now().UTC(),
		UpdatedBy:        nonempty(by, "watchdog"),
		Note:             in.Note,
	}
	if next.Customer == "" {
		next.Customer = strings.TrimSpace(m.cfg.Customer)
	}
	if next.ExtensionsMax <= 0 {
		next.ExtensionsMax = DefaultMaxExtensions
	}
	if next.ExtensionMaxDays <= 0 {
		next.ExtensionMaxDays = DefaultExtensionMaxDays
	}
	if next.ExtensionMaxDays > DefaultExtensionMaxDays {
		next.ExtensionMaxDays = DefaultExtensionMaxDays
	}
	if next.ExtensionsMax > DefaultMaxExtensions {
		next.ExtensionsMax = DefaultMaxExtensions
	}
	if next.ExtensionsUsed < 0 {
		next.ExtensionsUsed = 0
	}
	if next.ExtensionsUsed > next.ExtensionsMax {
		next.ExtensionsUsed = next.ExtensionsMax
	}
	if err := m.persistLocked(next); err != nil {
		return nil, err
	}
	if err := m.activateLocked(next); err != nil {
		return nil, err
	}
	m.cur = next
	return cloneLease(next), nil
}

// activateLocked 将租约 id 记入本机消费账本（离线防同一文件二次生效）。
func (m *Manager) activateLocked(lease *Lease) error {
	key, err := m.signKey()
	if err != nil {
		return err
	}
	ledger, ledErr := m.loadConsumed(key)
	if ledErr != nil && !os.IsNotExist(ledErr) {
		return ledErr
	}
	if ledger == nil {
		ledger = &consumedLedger{IDs: map[string]struct{}{}}
	}
	ledger.IDs[lease.ID] = struct{}{}
	ledger.ActiveID = lease.ID
	return m.saveConsumed(ledger, key)
}

// persistLocked 每次写入新文件，不覆盖历史租约。
func (m *Manager) persistLocked(lease *Lease) error {
	key, err := m.signKey()
	if err != nil {
		return err
	}
	if lease.ID == "" {
		lease.ID = newLeaseID()
	}
	if lease.UpdatedAt.IsZero() {
		lease.UpdatedAt = time.Now().UTC()
	}
	lease.Signature = signPayload(canonical(lease), key)
	raw := leaseFile{
		ID:               lease.ID,
		Customer:         lease.Customer,
		NotAfter:         lease.NotAfter.UTC().Format(time.RFC3339),
		ExtensionsUsed:   lease.ExtensionsUsed,
		ExtensionsMax:    lease.ExtensionsMax,
		ExtensionMaxDays: lease.ExtensionMaxDays,
		UpdatedAt:        lease.UpdatedAt.UTC().Format(time.RFC3339),
		UpdatedBy:        lease.UpdatedBy,
		Note:             lease.Note,
		Signature:        lease.Signature,
	}
	b, err := json.MarshalIndent(raw, "", "  ")
	if err != nil {
		return err
	}
	by := sanitizeFilePart(lease.UpdatedBy)
	if by == "" {
		by = "op"
	}
	name := fmt.Sprintf("%s%s-%s-%s.json",
		leaseFilePrefix,
		lease.UpdatedAt.UTC().Format("20060102T150405Z"),
		by,
		lease.ID,
	)
	full := filepath.Join(m.dir, name)
	tmp := full + ".tmp"
	if err := os.WriteFile(tmp, b, 0o644); err != nil {
		return err
	}
	if err := os.Rename(tmp, full); err != nil {
		_ = os.Remove(tmp)
		return err
	}
	lease.FileName = name
	return nil
}

func newLeaseID() string {
	var b [8]byte
	_, _ = rand.Read(b[:])
	return hex.EncodeToString(b[:])
}

func sanitizeFilePart(s string) string {
	s = strings.TrimSpace(strings.ToLower(s))
	var b strings.Builder
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
			b.WriteRune(r)
		}
	}
	out := b.String()
	if len(out) > 24 {
		out = out[:24]
	}
	return out
}

type leaseFile struct {
	ID               string `json:"id"`
	Customer         string `json:"customer"`
	NotAfter         string `json:"not_after"`
	ExtensionsUsed   int    `json:"extensions_used"`
	ExtensionsMax    int    `json:"extensions_max"`
	ExtensionMaxDays int    `json:"extension_max_days"`
	UpdatedAt        string `json:"updated_at"`
	UpdatedBy        string `json:"updated_by"`
	Note             string `json:"note"`
	Signature        string `json:"signature"`
}

func (f leaseFile) toLease() (*Lease, error) {
	na, err := ParseNotAfter(f.NotAfter)
	if err != nil || na.IsZero() {
		return nil, fmt.Errorf("lease.not_after 无效")
	}
	ua, _ := ParseNotAfter(f.UpdatedAt)
	if ua.IsZero() {
		ua = time.Now().UTC()
	}
	l := &Lease{
		ID:               strings.TrimSpace(f.ID),
		Customer:         f.Customer,
		NotAfter:         na,
		ExtensionsUsed:   f.ExtensionsUsed,
		ExtensionsMax:    f.ExtensionsMax,
		ExtensionMaxDays: f.ExtensionMaxDays,
		UpdatedAt:        ua,
		UpdatedBy:        f.UpdatedBy,
		Note:             f.Note,
		Signature:        strings.TrimSpace(f.Signature),
	}
	if l.ExtensionsMax <= 0 {
		l.ExtensionsMax = DefaultMaxExtensions
	}
	if l.ExtensionMaxDays <= 0 {
		l.ExtensionMaxDays = DefaultExtensionMaxDays
	}
	return l, nil
}

func canonical(l *Lease) string {
	return strings.Join([]string{
		signVersion,
		l.ID,
		l.Customer,
		l.NotAfter.UTC().Format(time.RFC3339),
		strconv.Itoa(l.ExtensionsUsed),
		strconv.Itoa(l.ExtensionsMax),
		strconv.Itoa(l.ExtensionMaxDays),
		l.UpdatedAt.UTC().Format(time.RFC3339),
		l.UpdatedBy,
		l.Note,
	}, "|")
}

func signPayload(payload string, key []byte) string {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write([]byte(payload))
	return hex.EncodeToString(mac.Sum(nil))
}

func verifyLease(l *Lease, key []byte) error {
	if l == nil {
		return sigErr("租约为空")
	}
	if strings.TrimSpace(l.ID) == "" {
		return sigErr("租约缺少 id")
	}
	if strings.TrimSpace(l.Signature) == "" {
		return sigErr("租约缺少签名")
	}
	want := signPayload(canonical(l), key)
	if !hmac.Equal([]byte(strings.ToLower(l.Signature)), []byte(strings.ToLower(want))) {
		return sigErr("租约签名校验失败（疑似外部篡改）")
	}
	return nil
}

type sigError string

func (e sigError) Error() string { return string(e) }

func sigErr(msg string) error { return sigError(msg) }

func isSigErr(err error) bool {
	if _, ok := err.(sigError); ok {
		return true
	}
	if err == nil {
		return false
	}
	s := err.Error()
	return strings.Contains(s, "签名") || strings.Contains(s, "篡改")
}

func ParseNotAfter(s string) (time.Time, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return time.Time{}, nil
	}
	if t, err := time.Parse(time.RFC3339, s); err == nil {
		return t.UTC(), nil
	}
	if t, err := time.Parse("2006-01-02", s); err == nil {
		return t.UTC(), nil
	}
	return time.Time{}, fmt.Errorf("时间格式应为 RFC3339 或 YYYY-MM-DD")
}

func cloneLease(l *Lease) *Lease {
	if l == nil {
		return nil
	}
	cp := *l
	return &cp
}

func nonempty(s, def string) string {
	s = strings.TrimSpace(s)
	if s == "" {
		return def
	}
	return s
}

func nonzero(v, def int) int {
	if v <= 0 {
		return def
	}
	return v
}
