package services

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log"
	"strings"
	"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/server/internal/notify"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func generateToken(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func HashToken(token string) string {
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

func CreateServer(instanceID string) (*models.Server, string, error) {
	if err := CheckServerLimit(instanceID); err != nil {
		return nil, "", err
	}
	token, err := generateToken(32)
	if err != nil {
		return nil, "", err
	}
	expires := time.Now().Add(time.Hour)
	s := &models.Server{
		InstanceID:    instanceID,
		ServerID:      uuid.NewString(),
		PreRegToken:   token,
		PreRegExpires: &expires,
		Status:        "pending",
		CreatedAt:     time.Now(),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err = db.Col("servers").InsertOne(ctx, s)
	if err != nil {
		return nil, "", err
	}
	return s, token, nil
}

func GetServer(instanceID, serverID string) (*models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var s models.Server
	err := db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID, "instance_id": instanceID}).Decode(&s)
	if err != nil {
		return nil, err
	}
	return &s, nil
}

func getServerByID(serverID string) (*models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var s models.Server
	err := db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID}).Decode(&s)
	if err != nil {
		return nil, err
	}
	return &s, nil
}

func GetServerByPreRegToken(token string) (*models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var s models.Server
	err := db.Col("servers").FindOne(ctx, bson.M{
		"pre_reg_token":   token,
		"pre_reg_expires": bson.M{"$gt": time.Now()},
	}).Decode(&s)
	if err != nil {
		return nil, err
	}
	return &s, nil
}

func OSTypeFromInfo(osInfo string) string {
	if strings.HasPrefix(strings.ToLower(osInfo), "windows") {
		return "windows"
	}
	return "linux"
}

func defaultConsoleFields(osType string) (protocols []string, sshPort, rdpPort int) {
	if osType == "windows" {
		return []string{"rdp"}, 22, 3389
	}
	return []string{"ssh"}, 22, 3389
}

func RegisterServer(serverID, preRegToken, hostname, ipAddress, osInfo string) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var s models.Server
	err := db.Col("servers").FindOne(ctx, bson.M{
		"server_id":       serverID,
		"pre_reg_token":   preRegToken,
		"pre_reg_expires": bson.M{"$gt": time.Now()},
	}).Decode(&s)
	if err != nil {
		return "", fmt.Errorf("invalid or expired pre-registration token")
	}

	agentToken, err := generateToken(32)
	if err != nil {
		return "", err
	}
	tokenHash := HashToken(agentToken)
	now := time.Now()

	osType := OSTypeFromInfo(osInfo)
	protocols, sshPort, rdpPort := defaultConsoleFields(osType)

	setFields := bson.M{
		"hostname":         hostname,
		"ip_address":       ipAddress,
		"os_info":          osInfo,
		"os_type":          osType,
		"agent_token_hash": tokenHash,
		"status":           "active",
		"last_seen":        now,
		"pre_reg_token":    "",
		"pre_reg_expires":  nil,
	}
	if len(s.ConsoleProtocols) == 0 {
		setFields["console_protocols"] = protocols
		setFields["ssh_port"] = sshPort
		setFields["rdp_port"] = rdpPort
	}

	_, err = db.Col("servers").UpdateOne(ctx,
		bson.M{"server_id": serverID},
		bson.M{
			"$set": setFields,
		},
	)
	if err != nil {
		return "", err
	}
	return agentToken, nil
}

func ValidateAgentToken(serverID, agentToken string) (*models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	tokenHash := HashToken(agentToken)
	var s models.Server
	err := db.Col("servers").FindOne(ctx, bson.M{
		"server_id":        serverID,
		"agent_token_hash": tokenHash,
	}).Decode(&s)
	if err != nil {
		return nil, fmt.Errorf("invalid agent token")
	}

	if s.InstanceID == "" {
		return nil, fmt.Errorf("server %s has no org", serverID)
	}
	return &s, nil
}

func BackfillConsoleConfig(srv *models.Server) error {
	if srv == nil || len(srv.ConsoleProtocols) > 0 {
		return nil
	}

	osType := srv.OSType
	if osType == "" {
		osType = OSTypeFromInfo(srv.OSInfo)
	}
	protocols, sshPort, rdpPort := defaultConsoleFields(osType)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err := db.Col("servers").UpdateOne(ctx,
		bson.M{"server_id": srv.ServerID, "console_protocols": bson.M{"$in": []interface{}{nil, bson.A{}}}},
		bson.M{"$set": bson.M{
			"os_type":           osType,
			"console_protocols": protocols,
			"ssh_port":          sshPort,
			"rdp_port":          rdpPort,
		}},
	)
	return err
}

func UpdateServerLastSeen(serverID, agentVersion string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	now := time.Now()
	fields := bson.M{"last_seen": now, "status": "active"}
	if agentVersion != "" {
		fields["agent_version"] = strings.TrimPrefix(agentVersion, "v")
	}
	_, err := db.Col("servers").UpdateOne(ctx,
		bson.M{"server_id": serverID},
		bson.M{"$set": fields},
	)
	return err
}

func ListServers(instanceID string) ([]models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
	cursor, err := db.Col("servers").Find(ctx, bson.M{"instance_id": instanceID}, opts)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	var servers []models.Server
	if err := cursor.All(ctx, &servers); err != nil {
		return nil, err
	}
	return servers, nil
}

func DeleteServer(instanceID, serverID string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err := db.Col("servers").DeleteOne(ctx, bson.M{"server_id": serverID, "instance_id": instanceID})
	if err != nil {
		return err
	}

	_, err = db.Col("assignments").DeleteMany(ctx, bson.M{"server_id": serverID, "instance_id": instanceID})
	return err
}

func StoreAvailableUpdates(serverID string, pkgs []models.PackageUpdate) error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	now := time.Now()
	_, err := db.Col("servers").UpdateOne(ctx,
		bson.M{"server_id": serverID},
		bson.M{"$set": bson.M{
			"available_updates":  pkgs,
			"updates_checked_at": now,
		}},
	)
	return err
}

func MarkOfflineServers() error {
	instanceIDs, err := ListInstanceIDs()
	if err != nil {
		return err
	}

	for _, instanceID := range instanceIDs {
		if err := markOfflineForFilter(bson.M{"instance_id": instanceID}, instanceID); err != nil {
			log.Printf("offline sweep failed for org %s: %v", instanceID, err)
		}
	}

	if err := markOfflineForFilter(bson.M{"instance_id": bson.M{"$nin": instanceIDs}}, ""); err != nil {
		log.Printf("offline sweep failed for orphaned servers: %v", err)
	}
	return nil
}

func markOfflineForFilter(scope bson.M, instanceID string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	var settings *models.Settings
	thresholdMinutes := 5
	if instanceID != "" {
		settings, _ = GetSettings(instanceID)
		if settings != nil && settings.Alerts.OfflineThresholdMinutes > 0 {
			thresholdMinutes = settings.Alerts.OfflineThresholdMinutes
		}
	}
	cutoff := time.Now().Add(-time.Duration(thresholdMinutes) * time.Minute)

	filter := bson.M{
		"status":    "active",
		"last_seen": bson.M{"$lt": cutoff},
	}
	for k, v := range scope {
		filter[k] = v
	}

	cursor, err := db.Col("servers").Find(ctx, filter)
	if err != nil {
		return err
	}
	var goingOffline []models.Server
	err = cursor.All(ctx, &goingOffline)
	cursor.Close(ctx)
	if err != nil {
		return err
	}
	if len(goingOffline) == 0 {
		return nil
	}

	for _, s := range goingOffline {
		LogEvent(s.InstanceID, "server.offline", "system", s.ServerID, "", fmt.Sprintf("%s (%s) went offline", s.Hostname, s.IPAddress))
		if settings != nil {
			notifyServerOffline(s.InstanceID, settings.Alerts.OfflineChannelIDs, s)
		}
	}

	_, err = db.Col("servers").UpdateMany(ctx, filter,
		bson.M{"$set": bson.M{"status": "offline"}},
	)
	return err
}

// notifyServerOffline delivers an agent-offline alert over the instance's
// chosen notification channels — the same destinations monitors dispatch to,
// so a webhook or SMTP destination is configured and tested in exactly one
// place. No channels selected means the alert is audited but not sent.
func notifyServerOffline(instanceID string, channelIDs []string, s models.Server) {
	if len(channelIDs) == 0 {
		return
	}
	channels, err := GetChannels(instanceID, channelIDs)
	if err != nil {
		log.Printf("notify: load offline channels for %s: %v", instanceID, err)
		return
	}
	ev := notify.Event{
		MonitorName: s.Hostname,
		Type:        notify.TypeServer,
		OldStatus:   models.StatusUp,
		NewStatus:   models.StatusDown,
		Message:     fmt.Sprintf("agent has not checked in (%s)", s.IPAddress),
		Time:        time.Now(),
	}
	for _, ch := range channels {
		if !ch.Enabled {
			continue
		}
		go func(c models.NotificationChannel) {
			if err := notify.Dispatch(c, ev); err != nil {
				log.Printf("notify: offline dispatch to %s (%s): %v", c.Name, c.Type, err)
			}
		}(ch)
	}
}

// ListServersFiltered is ListServers with an optional tag selector. An empty
// selector returns the whole fleet — unlike MatchesTags, where empty means
// "nothing", because here the caller is a list view whose default is
// "everything", not a run about to touch machines.
func ListServersFiltered(instanceID string, sel map[string]string) ([]models.Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	filter := bson.M{"instance_id": instanceID}
	for k, v := range sel {
		filter["tags."+k] = v
	}

	cur, err := db.Col("servers").Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}))
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)

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

// EnsureServerIndexes declares the wildcard index over the tag subdocument.
// It is wildcard because the queried key is chosen by the user at request time
// and cannot be named in advance.
//
// Non-fatal, following EnsureSecretIndexes: a missing index degrades tag
// filtering to a collection scan over a small collection, which is slower.
// A fatal error here would refuse to boot the fleet list over it.
func EnsureServerIndexes() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	_, err := db.Col("servers").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "tags.$**", Value: 1}},
	})
	return err
}
