package services

import (
	"context"
	"time"

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

// VulnScanningEnabled reports whether this instance may collect packages.
//
// It reads the feature by name from shared/license and never switches on tier,
// so changing what a tier includes needs no server release. A lapsed licence
// collects nothing: there is no point accumulating inventory an instance cannot
// act on.
func VulnScanningEnabled(instanceID string) bool {
	st := GetLicenseState(instanceID)
	return st.Active() && st.Feature(license.FeatureVulnScanning)
}

// HasPackageHash reports whether we already hold this exact package set, which
// is what lets the agent skip sending ~150KB it has already sent.
func HasPackageHash(instanceID, serverID, hash string) (bool, error) {
	err := db.Col("server_packages").FindOne(context.Background(), bson.M{
		"instance_id": instanceID,
		"server_id":   serverID,
		"hash":        hash,
	}, options.FindOne().SetProjection(bson.M{"_id": 1})).Err()
	if err == mongo.ErrNoDocuments {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// StorePackages replaces a server's package set and marks it for scanning.
//
// It deliberately does NOT match against the vulnerability database. Matching
// happens in vulnsched, on the leader, for two reasons: every replica would
// otherwise need the ~50MB database resident, and a database refresh would have
// N replicas racing to rescan the same fleet and sending N digests.
func StorePackages(instanceID, serverID string, os models.OSRelease, hash string, pkgs []models.InstalledPackage) error {
	now := time.Now()
	_, err := db.Col("server_packages").UpdateOne(context.Background(),
		bson.M{"instance_id": instanceID, "server_id": serverID},
		bson.M{"$set": bson.M{
			"os":           os,
			"hash":         hash,
			"packages":     pkgs,
			"collected_at": now,
			"scan_pending": true,
		}},
		options.UpdateOne().SetUpsert(true),
	)
	return err
}

// ListPackages returns a server's stored package set, or nil when the agent has
// not reported yet. A missing document is not an error: an agent that has never
// reported is the normal state for the first hour after install.
func ListPackages(instanceID, serverID string) (*models.ServerPackages, error) {
	var sp models.ServerPackages
	err := db.Col("server_packages").FindOne(context.Background(), bson.M{
		"instance_id": instanceID,
		"server_id":   serverID,
	}).Decode(&sp)
	if err == mongo.ErrNoDocuments {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &sp, nil
}

// PackageHit is one server running one package.
type PackageHit struct {
	ServerID string `json:"server_id"`
	Name     string `json:"name"`
	Version  string `json:"version"`
}

// SearchPackages answers "which servers run package X" across the fleet — the
// question people actually ask during an incident.
//
// The Mongo filter narrows to documents containing the name; the second pass is
// needed because a multikey match returns the whole document, not the matching
// array element.
func SearchPackages(instanceID, name string) ([]PackageHit, error) {
	ctx := context.Background()
	cur, err := db.Col("server_packages").Find(ctx, bson.M{
		"instance_id":   instanceID,
		"packages.name": name,
	}, options.Find().SetProjection(bson.M{"server_id": 1, "packages": 1}))
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)

	var docs []models.ServerPackages
	if err := cur.All(ctx, &docs); err != nil {
		return nil, err
	}

	hits := []PackageHit{}
	for _, d := range docs {
		for _, p := range d.Packages {
			if p.Name == name {
				hits = append(hits, PackageHit{ServerID: d.ServerID, Name: p.Name, Version: p.Version})
			}
		}
	}
	return hits, nil
}
