package license

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	codeSettingKey            = "platform.license_code"
	verifiedAtSettingKey      = "platform.license_verified_at"
	typeSettingKey            = "platform.license_type"
	versionSettingKey         = "platform.license_version"
	instanceIDSettingKey      = "platform.license_instance_id"
	instanceNameSettingKey    = "platform.license_instance_name"
	nodeRoleSettingKey        = "platform.license_node_role"
	systemNameSettingKey      = "platform.license_system_name"
	systemArchSettingKey      = "platform.license_system_arch"
	domainSettingKey          = "platform.license_domain"
	instanceTokenSettingKey   = "platform.license_instance_token"
	tokenExpiresAtSettingKey  = "platform.license_token_expires_at"
	lastHeartbeatAtSettingKey = "platform.license_last_heartbeat_at"
)

var stateSettingKeys = []string{
	codeSettingKey,
	verifiedAtSettingKey,
	typeSettingKey,
	versionSettingKey,
	instanceIDSettingKey,
	instanceNameSettingKey,
	nodeRoleSettingKey,
	systemNameSettingKey,
	systemArchSettingKey,
	domainSettingKey,
	instanceTokenSettingKey,
	tokenExpiresAtSettingKey,
	lastHeartbeatAtSettingKey,
}

var legacySecretSettingKeys = []string{
	codeSettingKey,
	verifiedAtSettingKey,
	typeSettingKey,
	versionSettingKey,
	instanceTokenSettingKey,
	tokenExpiresAtSettingKey,
	lastHeartbeatAtSettingKey,
}

type Identity struct {
	InstanceID   string
	InstanceName string
	NodeRole     string
	SystemName   string
	SystemArch   string
	BackendURL   string
}

type State struct {
	Code            string
	Type            string
	Version         string
	VerifiedAt      time.Time
	Identity        Identity
	InstanceToken   string
	TokenExpiresAt  time.Time
	LastHeartbeatAt time.Time
	LeaseRaw        []byte
	LeaseSignature  string
	LeaseScope      string
	LeaseSequence   uint64
	Features        []string
	// HighWaterMark is the highest server-signed time seen across all heartbeats.
	// Persisted in the encrypted credential envelope to detect clock rollbacks.
	HighWaterMark time.Time
	// SelectiveBlockDetected is true when a connectivity probe found public internet
	// reachable but all auth centers were not. Cleared on next successful heartbeat.
	SelectiveBlockDetected bool
}

func (s State) Configured() bool {
	return strings.TrimSpace(s.Code) != "" &&
		!s.VerifiedAt.IsZero() &&
		validInstanceID(s.Identity.InstanceID) &&
		strings.TrimSpace(s.InstanceToken) != "" &&
		!s.TokenExpiresAt.IsZero()
}

type StateRepository interface {
	Load(context.Context) (State, error)
	EnsureIdentity(context.Context, Identity) (Identity, error)
	SaveActivation(context.Context, State) error
	SaveHeartbeat(context.Context, State) error
}

type Store struct {
	db          *gorm.DB
	keyProvider KeyProvider
	mu          sync.Mutex
}

func NewStore(db *gorm.DB, providers ...KeyProvider) *Store {
	var provider KeyProvider = NewFileKeyProvider("data/license/master.key")
	if len(providers) > 0 && providers[0] != nil {
		provider = providers[0]
	}
	return &Store{db: db, keyProvider: provider}
}

func (s *Store) Load(ctx context.Context) (State, error) {
	if s == nil || s.db == nil {
		return State{}, fmt.Errorf("license state store is not configured")
	}
	legacy, err := loadState(ctx, s.db)
	if err != nil {
		return State{}, err
	}

	var record credentialRecord
	query := s.db.WithContext(ctx)
	if validInstanceID(legacy.Identity.InstanceID) {
		query = query.Where("instance_id = ?", legacy.Identity.InstanceID)
	} else {
		query = query.Order("updated_at DESC")
	}
	if err := query.First(&record).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return legacy, nil
		}
		return State{}, err
	}
	key, err := s.keyProvider.LoadOrCreate(ctx)
	if err != nil {
		return State{}, err
	}
	state, err := openCredential(record, key)
	if err != nil {
		return State{}, err
	}
	if validInstanceID(legacy.Identity.InstanceID) && legacy.Identity.InstanceID != state.Identity.InstanceID {
		return State{}, ErrCredentialMismatch
	}
	return state, nil
}

func (s *Store) EnsureIdentity(ctx context.Context, desired Identity) (Identity, error) {
	if s == nil || s.db == nil {
		return Identity{}, fmt.Errorf("license state store is not configured")
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	var identity Identity
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		state, err := loadStateForUpdate(ctx, tx)
		if err != nil {
			return err
		}
		identity = mergeIdentity(state.Identity, desired)
		if !validInstanceID(identity.InstanceID) {
			identity.InstanceID = uuid.NewString()
		}
		if identity == state.Identity {
			return nil
		}
		return saveIdentity(ctx, tx, identity, time.Now().UTC())
	})
	return identity, err
}

func (s *Store) SaveActivation(ctx context.Context, state State) error {
	if s == nil || s.db == nil {
		return fmt.Errorf("license state store is not configured")
	}
	if !state.Configured() {
		return fmt.Errorf("license activation state is incomplete")
	}
	key, err := s.keyProvider.LoadOrCreate(ctx)
	if err != nil {
		return err
	}
	record, err := sealCredential(state, key)
	if err != nil {
		return err
	}
	now := time.Now().UTC()
	record.CreatedAt = now
	record.UpdatedAt = now
	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := saveIdentity(ctx, tx, state.Identity, now); err != nil {
			return err
		}
		if err := saveCredential(ctx, tx, record, key, now, true); err != nil {
			return err
		}
		return deleteLegacySecrets(ctx, tx)
	})
}

func (s *Store) SaveHeartbeat(ctx context.Context, state State) error {
	if s == nil || s.db == nil {
		return fmt.Errorf("license state store is not configured")
	}
	if !state.Configured() {
		return fmt.Errorf("license heartbeat state is incomplete")
	}
	key, err := s.keyProvider.LoadOrCreate(ctx)
	if err != nil {
		return err
	}
	record, err := sealCredential(state, key)
	if err != nil {
		return err
	}
	now := time.Now().UTC()
	record.CreatedAt = now
	record.UpdatedAt = now
	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := saveIdentity(ctx, tx, state.Identity, now); err != nil {
			return err
		}
		if err := saveCredential(ctx, tx, record, key, now, false); err != nil {
			return err
		}
		return deleteLegacySecrets(ctx, tx)
	})
}

type storedSetting struct {
	Key   string          `gorm:"column:key"`
	Value json.RawMessage `gorm:"column:value"`
}

type settingValue struct {
	Key   string
	Value any
	Label string
}

func loadState(ctx context.Context, db *gorm.DB) (State, error) {
	var settings []storedSetting
	if err := db.WithContext(ctx).
		Table("system_settings").
		Select("`key`, value").
		Where("`key` IN ?", stateSettingKeys).
		Scan(&settings).Error; err != nil {
		return State{}, err
	}
	return decodeState(settings), nil
}

func loadStateForUpdate(ctx context.Context, tx *gorm.DB) (State, error) {
	var settings []storedSetting
	if err := tx.WithContext(ctx).
		Table("system_settings").
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Select("`key`, value").
		Where("`key` IN ?", stateSettingKeys).
		Find(&settings).Error; err != nil {
		return State{}, err
	}
	return decodeState(settings), nil
}

func decodeState(settings []storedSetting) State {
	values := make(map[string]json.RawMessage, len(settings))
	for _, setting := range settings {
		values[setting.Key] = setting.Value
	}
	return State{
		Code:       decodeString(values[codeSettingKey]),
		Type:       decodeString(values[typeSettingKey]),
		Version:    decodeString(values[versionSettingKey]),
		VerifiedAt: decodeTime(values[verifiedAtSettingKey]),
		Identity: Identity{
			InstanceID:   decodeString(values[instanceIDSettingKey]),
			InstanceName: decodeString(values[instanceNameSettingKey]),
			NodeRole:     decodeString(values[nodeRoleSettingKey]),
			SystemName:   decodeString(values[systemNameSettingKey]),
			SystemArch:   decodeString(values[systemArchSettingKey]),
			BackendURL:   decodeString(values[domainSettingKey]),
		},
		InstanceToken:   decodeString(values[instanceTokenSettingKey]),
		TokenExpiresAt:  decodeTime(values[tokenExpiresAtSettingKey]),
		LastHeartbeatAt: decodeTime(values[lastHeartbeatAtSettingKey]),
	}
}

func mergeIdentity(stored Identity, desired Identity) Identity {
	identity := desired
	identity.InstanceID = strings.TrimSpace(stored.InstanceID)
	if strings.TrimSpace(identity.InstanceName) == "" {
		identity.InstanceName = strings.TrimSpace(stored.InstanceName)
	}
	if strings.TrimSpace(identity.NodeRole) == "" {
		identity.NodeRole = strings.TrimSpace(stored.NodeRole)
	}
	if strings.TrimSpace(identity.SystemName) == "" {
		identity.SystemName = strings.TrimSpace(stored.SystemName)
	}
	if strings.TrimSpace(identity.SystemArch) == "" {
		identity.SystemArch = strings.TrimSpace(stored.SystemArch)
	}
	if strings.TrimSpace(identity.BackendURL) == "" {
		identity.BackendURL = strings.TrimSpace(stored.BackendURL)
	}
	return identity
}

func saveIdentity(ctx context.Context, tx *gorm.DB, identity Identity, now time.Time) error {
	values := []settingValue{
		{Key: instanceIDSettingKey, Value: strings.TrimSpace(identity.InstanceID), Label: "Instance ID"},
		{Key: instanceNameSettingKey, Value: strings.TrimSpace(identity.InstanceName), Label: "Instance name"},
		{Key: nodeRoleSettingKey, Value: strings.TrimSpace(identity.NodeRole), Label: "Node role"},
		{Key: systemNameSettingKey, Value: strings.TrimSpace(identity.SystemName), Label: "System name"},
		{Key: systemArchSettingKey, Value: strings.TrimSpace(identity.SystemArch), Label: "System architecture"},
		{Key: domainSettingKey, Value: strings.TrimSpace(identity.BackendURL), Label: "Instance backend URL"},
	}
	for _, item := range values {
		if err := upsertSetting(ctx, tx, item, now); err != nil {
			return err
		}
	}
	return nil
}

func upsertSetting(ctx context.Context, tx *gorm.DB, item settingValue, now time.Time) error {
	raw, err := json.Marshal(item.Value)
	if err != nil {
		return err
	}
	row := map[string]any{
		"key":         item.Key,
		"value":       json.RawMessage(raw),
		"label":       item.Label,
		"description": "Go commercial authorization state",
		"created_at":  now,
		"updated_at":  now,
	}
	return tx.WithContext(ctx).Table("system_settings").Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "key"}},
		DoUpdates: clause.AssignmentColumns([]string{"value", "label", "description", "updated_at"}),
	}).Create(row).Error
}

func saveCredential(ctx context.Context, tx *gorm.DB, record credentialRecord, key KeyMaterial, now time.Time, allowCorruptReplacement bool) error {
	var existing credentialRecord
	err := tx.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("instance_id = ?", record.InstanceID).
		First(&existing).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return tx.WithContext(ctx).Create(&record).Error
	}
	if err != nil {
		return err
	}
	_, openErr := openCredential(existing, key)
	if openErr != nil && !allowCorruptReplacement {
		return openErr
	}
	if openErr == nil && existing.LeaseScope == record.LeaseScope && existing.LeaseSequence >= record.LeaseSequence {
		return fmt.Errorf("%w: stored %d, received %d", ErrLicenseSequenceStale, existing.LeaseSequence, record.LeaseSequence)
	}
	result := tx.WithContext(ctx).Model(&credentialRecord{}).
		Where("id = ? AND row_version = ?", existing.ID, existing.RowVersion).
		Updates(map[string]any{
			"format_version":    record.FormatVersion,
			"signing_key_id":    record.SigningKeyID,
			"encryption_key_id": record.EncryptionKeyID,
			"lease_scope":       record.LeaseScope,
			"lease_sequence":    record.LeaseSequence,
			"nonce":             record.Nonce,
			"ciphertext":        record.Ciphertext,
			"row_version":       gorm.Expr("row_version + 1"),
			"updated_at":        now,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected != 1 {
		return fmt.Errorf("%w: credential row changed concurrently", ErrLicenseSequenceStale)
	}
	return nil
}

func deleteLegacySecrets(ctx context.Context, tx *gorm.DB) error {
	return tx.WithContext(ctx).
		Table("system_settings").
		Where("`key` IN ?", legacySecretSettingKeys).
		Delete(&storedSetting{}).Error
}

func decodeString(raw json.RawMessage) string {
	var value string
	if len(raw) == 0 || json.Unmarshal(raw, &value) != nil {
		return ""
	}
	return strings.TrimSpace(value)
}

func decodeTime(raw json.RawMessage) time.Time {
	value := decodeString(raw)
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		return time.Time{}
	}
	return parsed.UTC()
}

func validInstanceID(value string) bool {
	parsed, err := uuid.Parse(strings.TrimSpace(value))
	return err == nil && parsed.Version() != 0
}
