package services

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// LicenseState is the resolved licence for one instance.
type LicenseState struct {
	Status       license.State   `json:"state"`
	Reason       string          `json:"reason,omitempty"`
	Tier         string          `json:"tier,omitempty"`
	SupportLevel string          `json:"support_level,omitempty"`
	ExpiresAt    *time.Time      `json:"expires_at,omitempty"`
	Limits       license.Limits  `json:"limits"`
	Features     map[string]bool `json:"features"`
	// Source is "stored", "env" or "none" — useful when a self-hosted operator
	// asks why the licence they pasted is not the one in effect.
	Source string `json:"source"`
}

// Active reports whether mutations are allowed.
func (s LicenseState) Active() bool { return s.Status == license.StateValid }

// Feature reports whether a named feature is granted.
func (s LicenseState) Feature(name string) bool { return s.Features[name] }

// DeploymentMode is how this install describes itself to the verifier.
//
// It defaults to self_hosted, the stricter mode. An operator who removes the
// variable gets the tighter behaviour, not the looser one.
func DeploymentMode() string {
	if strings.ToLower(os.Getenv("VANTAGE_DEPLOYMENT")) == license.DeploymentCloud {
		return license.DeploymentCloud
	}
	return license.DeploymentSelfHosted
}

type cachedLicense struct {
	state LicenseState
	at    time.Time
}

var (
	licenseCacheMu sync.Mutex
	licenseCache   = map[string]cachedLicense{}
)

const licenseCacheTTL = 60 * time.Second

// InvalidateLicenseCache drops the cached state for one instance, so a pasted
// licence takes effect immediately rather than within the TTL.
func InvalidateLicenseCache(instanceID string) {
	licenseCacheMu.Lock()
	delete(licenseCache, instanceID)
	licenseCacheMu.Unlock()
}

// GetLicenseState resolves the licence for an instance, cached for 60 seconds.
//
// Resolution order:
//
//  1. the blob stored on the instance document
//  2. VANTAGE_LICENSE, used ONLY when the instance has no stored blob, so an
//     automated self-hosted deployment can ship a licence without a human
//     pasting one
//  3. neither -> invalid / no_license
//
// A blob stored through the UI always wins afterwards, so an operator is never
// locked out by a stale environment value.
func GetLicenseState(instanceID string) LicenseState {
	licenseCacheMu.Lock()
	if e, ok := licenseCache[instanceID]; ok && time.Since(e.at) < licenseCacheTTL {
		licenseCacheMu.Unlock()
		return e.state
	}
	licenseCacheMu.Unlock()

	state := resolveLicenseState(instanceID)

	licenseCacheMu.Lock()
	licenseCache[instanceID] = cachedLicense{state: state, at: time.Now()}
	licenseCacheMu.Unlock()
	return state
}

func resolveLicenseState(instanceID string) LicenseState {
	inst, err := GetInstance(instanceID)
	if err != nil {
		return LicenseState{
			Status:   license.StateInvalid,
			Reason:   license.ReasonNoLicense,
			Features: map[string]bool{},
			Source:   "none",
		}
	}

	blob, source := inst.LicenseBlob, "stored"
	if blob == "" {
		blob, source = os.Getenv("VANTAGE_LICENSE"), "env"
	}
	if blob == "" {
		return LicenseState{
			Status:   license.StateInvalid,
			Reason:   license.ReasonNoLicense,
			Features: map[string]bool{},
			Source:   "none",
		}
	}

	res := license.Verify(blob, license.VerifyOpts{
		InstanceID: instanceID,
		Deployment: DeploymentMode(),
	})
	return stateFromResult(res, source)
}

func stateFromResult(res license.Result, source string) LicenseState {
	feats := map[string]bool{}
	for _, f := range res.License.Features {
		feats[f] = true
	}

	// A licence signed before max_monitors and audit_retention_days existed
	// decodes them as 0, which would read as "no monitors" and "trim the audit
	// log to nothing". Fill from the seed plan for the tier the licence names.
	//
	// This is the only decode site, which is why the fill belongs here rather
	// than at each of the places that reads a limit.
	limits := res.License.Limits
	deployment, tier := license.NormaliseTier(res.License.Deployment, res.License.Tier)
	if base, ok := license.PlanFor(deployment, tier); ok {
		limits = limits.FillUnset(base.Limits)
	}

	s := LicenseState{
		Status:       res.State,
		Reason:       res.Reason,
		Tier:         res.License.Tier,
		SupportLevel: res.License.SupportLevel,
		Limits:       limits,
		Features:     feats,
		Source:       source,
	}
	if !res.License.ExpiresAt.IsZero() {
		exp := res.License.ExpiresAt
		s.ExpiresAt = &exp
	}
	return s
}

// StoreLicense verifies a blob against this instance and stores it.
//
// An expired-but-otherwise-valid blob IS stored, so the UI can show what expired
// and when. An invalid blob is rejected and the previous one kept.
func StoreLicense(instanceID, blob string) (LicenseState, error) {
	blob = strings.TrimSpace(blob)

	res := license.Verify(blob, license.VerifyOpts{
		InstanceID: instanceID,
		Deployment: DeploymentMode(),
	})
	if res.State == license.StateInvalid {
		return LicenseState{}, fmt.Errorf("%s", res.Reason)
	}

	set := bson.M{
		"license_blob":   blob,
		"license_tier":   res.License.Tier,
		"license_expiry": res.License.ExpiresAt,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if _, err := db.Col("instances").UpdateOne(ctx,
		bson.M{"instance_id": instanceID}, bson.M{"$set": set}); err != nil {
		return LicenseState{}, err
	}

	InvalidateLicenseCache(instanceID)
	return stateFromResult(res, "stored"), nil
}
