// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later

package licensing

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"hash/crc32"
	"strings"
	"time"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)

func init() {
	db.RegisterModel(new(License))
}

// License represents a consumer-facing license with a DLID (Download ID).
type License struct {
	ID         int64              `xorm:"pk autoincr"`
	UserID     int64              `xorm:"INDEX NOT NULL"`
	DLID       string             `xorm:"VARCHAR(36) UNIQUE NOT NULL"`
	Tier       string             `xorm:"VARCHAR(30) NOT NULL DEFAULT 'base'"`
	MaxDomains int                `xorm:"NOT NULL DEFAULT 1"`
	Status     string             `xorm:"VARCHAR(20) NOT NULL DEFAULT 'active'"` // active, expired, revoked, suspended
	ExpiresAt  timeutil.TimeStamp `xorm:"INDEX"`
	Notes      string             `xorm:"TEXT"`
	CreatedAt  timeutil.TimeStamp `xorm:"INDEX CREATED"`
	UpdatedAt  timeutil.TimeStamp `xorm:"UPDATED"`
}

func (License) TableName() string {
	return "license"
}

// IsExpired returns true if the license has a set expiry that has passed.
func (l *License) IsExpired() bool {
	if l.ExpiresAt == 0 {
		return false
	}
	return time.Unix(int64(l.ExpiresAt), 0).Before(time.Now())
}

// IsActive returns true if the license status is "active" and not expired.
func (l *License) IsActive() bool {
	return l.Status == "active" && !l.IsExpired()
}

// GenerateDLID creates a new DLID: 28 random hex chars + 4 CRC32 checksum chars,
// formatted as 8-8-8-8 groups.
func GenerateDLID() (string, error) {
	b := make([]byte, 14) // 14 bytes = 28 hex chars
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	prefix := hex.EncodeToString(b)
	checksum := crc32.ChecksumIEEE([]byte(prefix))
	full := fmt.Sprintf("%s%04x", prefix, checksum&0xFFFF)
	// Format as 8-8-8-8
	return fmt.Sprintf("%s-%s-%s-%s", full[0:8], full[8:16], full[16:24], full[24:32]), nil
}

// ValidateDLIDFormat checks if a DLID has valid format and CRC32 checksum.
// This is a client-side check that catches typos without a database hit.
func ValidateDLIDFormat(dlid string) bool {
	clean := strings.ReplaceAll(dlid, "-", "")
	if len(clean) != 32 {
		return false
	}
	// Validate hex
	if _, err := hex.DecodeString(clean); err != nil {
		return false
	}
	// CRC32 check: last 4 chars should match CRC32 of first 28
	prefix := clean[:28]
	expected := fmt.Sprintf("%04x", crc32.ChecksumIEEE([]byte(prefix))&0xFFFF)
	return clean[28:32] == expected
}

// CreateLicense creates a new license with an auto-generated DLID.
func CreateLicense(ctx context.Context, userID int64, tier string, maxDomains int, expiresAt timeutil.TimeStamp) (*License, error) {
	dlid, err := GenerateDLID()
	if err != nil {
		return nil, err
	}

	license := &License{
		UserID:     userID,
		DLID:       dlid,
		Tier:       tier,
		MaxDomains: maxDomains,
		Status:     "active",
		ExpiresAt:  expiresAt,
	}

	_, err = db.GetEngine(ctx).Insert(license)
	if err != nil {
		return nil, err
	}
	return license, nil
}

// GetLicenseByDLID looks up a license by its DLID string.
func GetLicenseByDLID(ctx context.Context, dlid string) (*License, error) {
	license := new(License)
	has, err := db.GetEngine(ctx).Where("dlid = ?", dlid).Get(license)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, nil
	}
	return license, nil
}

// GetLicenseByID returns a license by primary key.
func GetLicenseByID(ctx context.Context, id int64) (*License, error) {
	license := new(License)
	has, err := db.GetEngine(ctx).ID(id).Get(license)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, nil
	}
	return license, nil
}

// GetLicensesByUser returns all licenses for a user.
func GetLicensesByUser(ctx context.Context, userID int64) ([]*License, error) {
	var licenses []*License
	return licenses, db.GetEngine(ctx).Where("user_id = ?", userID).Find(&licenses)
}

// UpdateLicenseTier changes a license's tier, rebuilds entitlements, and logs the change.
func UpdateLicenseTier(ctx context.Context, licenseID int64, newTier string) error {
	license, err := GetLicenseByID(ctx, licenseID)
	if err != nil || license == nil {
		return err
	}
	oldTier := license.Tier
	_, err = db.GetEngine(ctx).ID(licenseID).Cols("tier", "updated_at").Update(&License{Tier: newTier})
	if err != nil {
		return err
	}
	if err := LogLicenseAudit(ctx, licenseID, "tier_change", oldTier, newTier); err != nil {
		return err
	}
	return RebuildEntitlements(ctx, licenseID, newTier)
}

// SetLicenseStatus updates the status field and logs the transition.
func SetLicenseStatus(ctx context.Context, licenseID int64, status string) error {
	license, err := GetLicenseByID(ctx, licenseID)
	if err != nil || license == nil {
		return err
	}
	oldStatus := license.Status
	_, err = db.GetEngine(ctx).ID(licenseID).Cols("status", "updated_at").Update(&License{Status: status})
	if err != nil {
		return err
	}
	return LogLicenseAudit(ctx, licenseID, "status_change", oldStatus, status)
}

// RevokeLicense permanently revokes a license.
func RevokeLicense(ctx context.Context, licenseID int64) error {
	return SetLicenseStatus(ctx, licenseID, "revoked")
}

// SuspendLicense temporarily suspends a license.
func SuspendLicense(ctx context.Context, licenseID int64) error {
	return SetLicenseStatus(ctx, licenseID, "suspended")
}

// ReactivateLicense restores a suspended or expired license to active.
func ReactivateLicense(ctx context.Context, licenseID int64) error {
	return SetLicenseStatus(ctx, licenseID, "active")
}
