// Package license — формат лицензии Bridge-and-Join-s и её подпись Ed25519.
//
// Лицензия — самодостаточный подписанный токен (offline-проверяемый):
// клиент проверяет подпись зашитым публичным ключом и срок действия БЕЗ
// обращения к серверу. Это значит, что on-prem bj-server продолжает
// работать даже если license-сервер недоступен.
//
// Online-сервер (cmd/bj-license-server) нужен только для отзыва (revocation)
// и выдачи новых ключей. Базовая модель — годовой ключ: выпустили на год,
// клиент проверяет offline; перед обновлением bj-server гейтит установку
// валидной непросроченной лицензией.
//
// Издатель держит приватный ключ в секрете; публичный зашит в bj-server.
package license

import (
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"
)

const CurrentSchema = 1

// Plan — тариф лицензии.
type Plan string

const (
	PlanFree       Plan = "free"
	PlanPro        Plan = "pro"
	PlanEnterprise Plan = "enterprise"
)

// License — содержимое лицензии (подписывается целиком).
type License struct {
	Schema    int       `json:"schema"`
	ID        string    `json:"id"`         // UUID лицензии
	Tenant    string    `json:"tenant"`     // организация-клиент
	Product   string    `json:"product"`    // "bj-server"
	Plan      Plan      `json:"plan"`       // free|pro|enterprise
	IssuedAt  time.Time `json:"issued_at"`  // дата выпуска
	ExpiresAt time.Time `json:"expires_at"` // дата окончания (годовой ключ)
	Features  []string  `json:"features,omitempty"`     // "updates","web-cabinet",...
	MaxNodes  int       `json:"max_nodes,omitempty"`    // лимит инсталляций (0 = без лимита)
	Note      string    `json:"note,omitempty"`
}

// Token — лицензия + подпись. Именно это вводит клиент (одна base64-строка
// или JSON-файл). Формат: base64url(payload).base64url(sig) — компактно.
type Token struct {
	Payload   string `json:"payload"`   // base64(каноничный JSON License)
	Signature string `json:"signature"` // base64(ed25519 over каноничным JSON)
	KeyID     string `json:"key_id,omitempty"`
}

// Canonical сериализует лицензию детерминированно (для подписи/проверки).
func (l *License) Canonical() ([]byte, error) {
	if l.Schema == 0 {
		l.Schema = CurrentSchema
	}
	return json.Marshal(l)
}

// Valid проверяет срок действия на момент now.
func (l *License) Valid(now time.Time) error {
	if now.Before(l.IssuedAt.Add(-24 * time.Hour)) {
		return errors.New("license: ещё не действует (issued_at в будущем)")
	}
	if now.After(l.ExpiresAt) {
		return fmt.Errorf("license: истекла %s", l.ExpiresAt.Format("02.01.2006"))
	}
	return nil
}

// HasFeature — включена ли фича (или план enterprise — всё включено).
func (l *License) HasFeature(f string) bool {
	if l.Plan == PlanEnterprise {
		return true
	}
	for _, x := range l.Features {
		if x == f {
			return true
		}
	}
	return false
}

// AllowsUpdates — разрешены ли обновления по этой лицензии.
func (l *License) AllowsUpdates() bool { return l.HasFeature("updates") }

// DaysLeft — сколько дней до окончания (может быть отрицательным).
func (l *License) DaysLeft(now time.Time) int {
	return int(l.ExpiresAt.Sub(now).Hours() / 24)
}

// Sign подписывает лицензию и возвращает Token.
func Sign(l *License, priv ed25519.PrivateKey, keyID string) (*Token, error) {
	payload, err := l.Canonical()
	if err != nil {
		return nil, fmt.Errorf("license: canonical: %w", err)
	}
	sig := ed25519.Sign(priv, payload)
	return &Token{
		Payload:   base64.StdEncoding.EncodeToString(payload),
		Signature: base64.StdEncoding.EncodeToString(sig),
		KeyID:     keyID,
	}, nil
}

// Verify проверяет подпись и возвращает License (срок проверяется отдельно
// через License.Valid — Verify только про подлинность).
func Verify(t *Token, pub ed25519.PublicKey) (*License, error) {
	sig, err := base64.StdEncoding.DecodeString(t.Signature)
	if err != nil {
		return nil, fmt.Errorf("license: decode signature: %w", err)
	}
	payload, err := base64.StdEncoding.DecodeString(t.Payload)
	if err != nil {
		return nil, fmt.Errorf("license: decode payload: %w", err)
	}
	if !ed25519.Verify(pub, payload, sig) {
		return nil, errors.New("license: подпись недействительна")
	}
	var l License
	if err := json.Unmarshal(payload, &l); err != nil {
		return nil, fmt.Errorf("license: unmarshal: %w", err)
	}
	if l.Schema != CurrentSchema {
		return nil, fmt.Errorf("license: неподдерживаемая схема %d", l.Schema)
	}
	return &l, nil
}

// Encode сериализует Token в компактную строку payload.signature[.keyid]
// (то, что клиент вставляет в поле «лицензионный ключ»).
func (t *Token) Encode() string {
	s := t.Payload + "." + t.Signature
	if t.KeyID != "" {
		s += "." + t.KeyID
	}
	return s
}

// DecodeToken разбирает компактную строку обратно в Token.
func DecodeToken(s string) (*Token, error) {
	parts := strings.Split(strings.TrimSpace(s), ".")
	if len(parts) < 2 {
		return nil, errors.New("license: неверный формат ключа (ожидается payload.signature)")
	}
	t := &Token{Payload: parts[0], Signature: parts[1]}
	if len(parts) >= 3 {
		t.KeyID = parts[2]
	}
	return t, nil
}

// --- Ключи (как в release) ---

func LoadPrivateKey(path string) (ed25519.PrivateKey, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	seed, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(b)))
	if err != nil {
		return nil, fmt.Errorf("license: decode seed: %w", err)
	}
	if len(seed) != ed25519.SeedSize {
		return nil, fmt.Errorf("license: неверный размер seed %d", len(seed))
	}
	return ed25519.NewKeyFromSeed(seed), nil
}

func ParsePublicKey(b64 string) (ed25519.PublicKey, error) {
	pub, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64))
	if err != nil {
		return nil, err
	}
	if len(pub) != ed25519.PublicKeySize {
		return nil, fmt.Errorf("license: неверный размер pubkey %d", len(pub))
	}
	return ed25519.PublicKey(pub), nil
}
