package services

import (
	"context"
	"fmt"
	"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"
)

// LimitError is returned when a licence cap would be exceeded. The API maps it
// to 403 with a machine-readable body.
type LimitError struct {
	Limit   string
	Current int
	Max     int
}

func (e *LimitError) Error() string {
	return fmt.Sprintf("licence limit reached: %s (%d of %d)", e.Limit, e.Current, e.Max)
}

func limitCtx() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), 5*time.Second)
}

// CheckServerLimit refuses a new server when the instance is at its cap.
//
// Counts live rows only. An instance already over its cap keeps every server it
// has — nothing is truncated — it simply cannot add another.
func CheckServerLimit(instanceID string) error {
	st := GetLicenseState(instanceID)
	ctx, cancel := limitCtx()
	defer cancel()

	n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return err
	}
	if !license.WithinLimit(int(n), st.Limits.MaxServers) {
		return &LimitError{Limit: "max_servers", Current: int(n), Max: st.Limits.MaxServers}
	}
	return nil
}

// CheckSecretGroupLimit refuses a NEW group at the cap. Writing to a group that
// already exists is always allowed, so a capped customer can still rotate the
// secrets they have.
func CheckSecretGroupLimit(instanceID, group string) error {
	st := GetLicenseState(instanceID)
	ctx, cancel := limitCtx()
	defer cancel()

	existing, err := db.Col("secrets").CountDocuments(ctx,
		bson.M{"instance_id": instanceID, "group": group})
	if err != nil {
		return err
	}
	if existing > 0 {
		return nil
	}

	var groups []string
	if err := db.Col("secrets").Distinct(ctx, "group",
		bson.M{"instance_id": instanceID}).Decode(&groups); err != nil {
		return err
	}
	if !license.WithinLimit(len(groups), st.Limits.MaxSecretGroups) {
		return &LimitError{Limit: "max_secret_groups", Current: len(groups), Max: st.Limits.MaxSecretGroups}
	}
	return nil
}

// CheckChannelLimit refuses a new notification channel at the cap.
func CheckChannelLimit(instanceID string) error {
	st := GetLicenseState(instanceID)
	ctx, cancel := limitCtx()
	defer cancel()

	n, err := db.Col("notification_channels").CountDocuments(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return err
	}
	if !license.WithinLimit(int(n), st.Limits.MaxChannels) {
		return &LimitError{Limit: "max_channels", Current: int(n), Max: st.Limits.MaxChannels}
	}
	return nil
}

// CheckMonitorLimit refuses a new monitor when the instance is at its cap.
//
// Counts live rows only, like every other check here. An instance already over
// its cap keeps every monitor it has and they keep executing — the licence
// expiry story is that monitoring never stops, so truncating here would
// contradict it.
func CheckMonitorLimit(instanceID string) error {
	st := GetLicenseState(instanceID)
	ctx, cancel := limitCtx()
	defer cancel()

	n, err := db.Col("monitors").CountDocuments(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return err
	}
	if !license.WithinLimit(int(n), st.Limits.MaxMonitors) {
		return &LimitError{Limit: "max_monitors", Current: int(n), Max: st.Limits.MaxMonitors}
	}
	return nil
}

// LicenseUsage reports current counts, so the UI can say "12 of 3 servers"
// honestly when an instance is over its cap rather than pretending.
func LicenseUsage(instanceID string) (servers, monitors, secretGroups, channels int) {
	ctx, cancel := limitCtx()
	defer cancel()

	if n, err := db.Col("servers").CountDocuments(ctx, bson.M{"instance_id": instanceID}); err == nil {
		servers = int(n)
	}
	if n, err := db.Col("monitors").CountDocuments(ctx, bson.M{"instance_id": instanceID}); err == nil {
		monitors = int(n)
	}
	var groups []string
	if err := db.Col("secrets").Distinct(ctx, "group",
		bson.M{"instance_id": instanceID}).Decode(&groups); err == nil {
		secretGroups = len(groups)
	}
	if n, err := db.Col("notification_channels").CountDocuments(ctx,
		bson.M{"instance_id": instanceID}); err == nil {
		channels = int(n)
	}
	return
}
