package services

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"time"

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

var defaultSettings = models.Settings{
	Alerts: models.AlertSettings{
		OfflineThresholdMinutes: 5,
	},
}

func EnsureSettingsIndexes() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := db.Col("settings").Indexes().DropOne(ctx, "secrets.read_token_hash_1"); err != nil && !isIndexNotFound(err) {
		return err
	}

	if _, err := db.Col("settings").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys:    bson.D{{Key: "instance_id", Value: 1}},
		Options: options.Index().SetUnique(true),
	}); err != nil {
		return err
	}

	_, err := db.Col("settings").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "secrets.read_token_hash", Value: 1}},
		Options: options.Index().SetUnique(true).SetName("settings_read_token_hash_unique").
			SetPartialFilterExpression(bson.M{
				"secrets.read_token_hash": bson.M{"$type": "string", "$gt": ""},
			}),
	})
	return err
}

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

	var s models.Settings
	err := db.Col("settings").FindOne(ctx, bson.M{"instance_id": instanceID}).Decode(&s)
	if err == mongo.ErrNoDocuments {
		cp := defaultSettings
		cp.InstanceID = instanceID
		return &cp, nil
	}
	if err != nil {
		return nil, err
	}
	s.Secrets.ReadTokenSet = s.Secrets.ReadTokenHash != ""
	return &s, nil
}

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

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

	raw := make([]byte, 32)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	token := hex.EncodeToString(raw)

	_, err := db.Col("settings").UpdateOne(ctx,
		bson.M{"instance_id": instanceID},
		bson.M{
			"$set": bson.M{
				"secrets.read_token_hash": hashToken(token),
				"secrets.rotated_at":      time.Now(),
			},
			"$setOnInsert": bson.M{"instance_id": instanceID},
		},
		options.UpdateOne().SetUpsert(true),
	)
	if err != nil {
		return "", err
	}
	return token, nil
}

func ResolveSecretsReadToken(token string) (string, bool) {
	if token == "" {
		return "", false
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	var s models.Settings
	err := db.Col("settings").FindOne(ctx, bson.M{"secrets.read_token_hash": hashToken(token)}).Decode(&s)
	if err != nil || s.Secrets.ReadTokenHash == "" || s.InstanceID == "" {
		return "", false
	}
	expected, err := hex.DecodeString(s.Secrets.ReadTokenHash)
	if err != nil {
		return "", false
	}
	got := sha256.Sum256([]byte(token))
	if subtle.ConstantTimeCompare(expected, got[:]) != 1 {
		return "", false
	}
	return s.InstanceID, true
}

func SaveSettings(instanceID string, alerts models.AlertSettings, retentionDays *int, localLoginEnabled *bool) error {
	if alerts.OfflineThresholdMinutes <= 0 {
		alerts.OfflineThresholdMinutes = 5
	}
	if alerts.OfflineChannelIDs == nil {
		alerts.OfflineChannelIDs = []string{}
	}

	// The guard lives here rather than in the handler so the settings path and
	// the provider path cannot disagree about what a lockout is.
	if localLoginEnabled != nil && !*localLoginEnabled {
		n, err := CountEnabledAuthProviders(instanceID)
		if err != nil {
			return err
		}
		if err := CheckLockout(false, n); err != nil {
			return err
		}
	}

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

	set := bson.M{"alerts": alerts}
	if retentionDays != nil {
		set["workflow_log_retention_days"] = *retentionDays
	}
	if localLoginEnabled != nil {
		set["local_login_enabled"] = *localLoginEnabled
	}
	_, err := db.Col("settings").UpdateOne(ctx,
		bson.M{"instance_id": instanceID},
		bson.M{"$set": set, "$setOnInsert": bson.M{"instance_id": instanceID}},
		options.UpdateOne().SetUpsert(true),
	)
	return err
}

func GetWorkflowLogRetentionDays(instanceID string) (int, error) {
	s, err := GetSettings(instanceID)
	if err != nil {
		return 30, err
	}
	if s.WorkflowLogRetentionDays == nil {
		return 30, nil
	}
	return *s.WorkflowLogRetentionDays, nil
}
