package services

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

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

// ErrLockout is returned when a change would leave an instance with neither
// local password login nor an enabled provider — nobody could sign in, and no
// endpoint exists to undo it without database access.
var ErrLockout = errors.New("that would leave nobody able to sign in")

// ErrLastProvider is returned when a change would remove or disable the last
// enabled provider while local login is also off.
var ErrLastProvider = errors.New("cannot remove or disable the last sign-in method")

// ErrLocalLoginRequired is returned when disabling local login would leave no
// enabled provider to sign in with.
var ErrLocalLoginRequired = errors.New("local login is required until a provider is enabled")

const authProviderCol = "auth_providers"

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

// CheckLockout is pure so the two endpoints that can reach this condition —
// saving settings and changing a provider — share one answer.
func CheckLockout(localEnabled bool, enabledProviders int) error {
	if localEnabled || enabledProviders > 0 {
		return nil
	}
	return ErrLockout
}

func ListAuthProviders(instanceID string) ([]models.AuthProvider, error) {
	ctx, cancel := authProviderCtx()
	defer cancel()
	cur, err := db.Col(authProviderCol).Find(ctx,
		bson.M{"instance_id": instanceID},
		options.Find().SetSort(bson.D{{Key: "order", Value: 1}, {Key: "created_at", Value: 1}}))
	if err != nil {
		return nil, err
	}
	out := []models.AuthProvider{}
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func ListEnabledAuthProviders(instanceID string) ([]models.AuthProvider, error) {
	all, err := ListAuthProviders(instanceID)
	if err != nil {
		return nil, err
	}
	out := []models.AuthProvider{}
	for _, p := range all {
		if p.Enabled {
			out = append(out, p)
		}
	}
	return out, nil
}

func CountEnabledAuthProviders(instanceID string) (int, error) {
	enabled, err := ListEnabledAuthProviders(instanceID)
	if err != nil {
		return 0, err
	}
	return len(enabled), nil
}

// GetAuthProvider is scoped by instance. There is deliberately no lookup by
// provider_id alone: a provider ID travels in a URL, and an unscoped lookup
// would let one instance's callback resolve another instance's provider.
func GetAuthProvider(instanceID, providerID string) (*models.AuthProvider, error) {
	ctx, cancel := authProviderCtx()
	defer cancel()
	var p models.AuthProvider
	err := db.Col(authProviderCol).FindOne(ctx,
		bson.M{"instance_id": instanceID, "provider_id": providerID}).Decode(&p)
	if err != nil {
		return nil, err
	}
	return &p, nil
}

func GetAuthProviderSecret(instanceID, providerID string) (string, error) {
	p, err := GetAuthProvider(instanceID, providerID)
	if err != nil {
		return "", err
	}
	if p.ClientSecretEnc == "" {
		return "", fmt.Errorf("provider %q has no client secret configured", p.Name)
	}
	return decryptString(p.ClientSecretEnc)
}

func CreateAuthProvider(p *models.AuthProvider, clientSecret string) (*models.AuthProvider, error) {
	if strings.TrimSpace(p.Name) == "" {
		return nil, errors.New("name is required")
	}
	if strings.TrimSpace(p.ClientID) == "" {
		return nil, errors.New("client ID is required")
	}
	if clientSecret == "" {
		return nil, errors.New("client secret is required")
	}
	enc, err := encryptString(clientSecret)
	if err != nil {
		return nil, err
	}
	id, err := randomProviderID()
	if err != nil {
		return nil, err
	}

	existing, err := ListAuthProviders(p.InstanceID)
	if err != nil {
		return nil, err
	}

	now := time.Now()
	p.ProviderID = id
	p.ClientSecretEnc = enc
	p.CallbackNotice = false
	p.Order = len(existing)
	p.CreatedAt = now
	p.UpdatedAt = now

	ctx, cancel := authProviderCtx()
	defer cancel()
	if _, err := db.Col(authProviderCol).InsertOne(ctx, p); err != nil {
		return nil, err
	}
	return p, nil
}

// AuthProviderUpdate carries only what an edit may change. Pointer fields are
// "leave alone when nil", which is what lets an empty client secret mean "keep
// the stored one" rather than "erase it".
type AuthProviderUpdate struct {
	Name         *string
	Issuer       *string
	ClientID     *string
	ClientSecret *string
	Scopes       *[]string
	Enabled      *bool
	Order        *int
}

func UpdateAuthProvider(instanceID, providerID string, in AuthProviderUpdate) error {
	set := bson.M{"updated_at": time.Now()}
	if in.Name != nil {
		if strings.TrimSpace(*in.Name) == "" {
			return errors.New("name is required")
		}
		set["name"] = *in.Name
	}
	if in.Issuer != nil {
		set["issuer"] = *in.Issuer
	}
	if in.ClientID != nil {
		set["client_id"] = *in.ClientID
	}
	if in.Scopes != nil {
		set["scopes"] = *in.Scopes
	}
	if in.Order != nil {
		set["order"] = *in.Order
	}
	if in.Enabled != nil {
		set["enabled"] = *in.Enabled
	}
	// An empty secret means "keep what is stored". Only a non-empty one writes.
	if in.ClientSecret != nil && *in.ClientSecret != "" {
		enc, err := encryptString(*in.ClientSecret)
		if err != nil {
			return err
		}
		set["client_secret_enc"] = enc
	}

	ctx, cancel := authProviderCtx()
	defer cancel()
	res, err := db.Col(authProviderCol).UpdateOne(ctx,
		bson.M{"instance_id": instanceID, "provider_id": providerID},
		bson.M{"$set": set})
	if err != nil {
		return err
	}
	if res.MatchedCount == 0 {
		return mongo.ErrNoDocuments
	}
	return nil
}

func DeleteAuthProvider(instanceID, providerID string) error {
	ctx, cancel := authProviderCtx()
	defer cancel()
	res, err := db.Col(authProviderCol).DeleteOne(ctx,
		bson.M{"instance_id": instanceID, "provider_id": providerID})
	if err != nil {
		return err
	}
	if res.DeletedCount == 0 {
		return mongo.ErrNoDocuments
	}
	return nil
}

func AckAuthProviderNotice(instanceID, providerID string) error {
	ctx, cancel := authProviderCtx()
	defer cancel()
	_, err := db.Col(authProviderCol).UpdateOne(ctx,
		bson.M{"instance_id": instanceID, "provider_id": providerID},
		bson.M{"$set": bson.M{"callback_notice": false}})
	return err
}

// IsLocalLoginEnabled fails open. A settings read error must not lock an
// instance out of its own login page, and the safe direction here is the one
// that still asks for a password.
func IsLocalLoginEnabled(instanceID string) bool {
	s, err := GetSettings(instanceID)
	if err != nil {
		return true
	}
	return shared.LocalLoginEnabled(s)
}

// LocalLoginPermitted answers whether password sign-in must be accepted for
// this instance, which is not the same question as whether an administrator
// turned it on. An instance whose only providers have become unusable — a
// lapsed licence, or every provider disabled — has to keep its password form,
// or nobody can sign in and there is no endpoint left to fix it with.
func LocalLoginPermitted(instanceID string) bool {
	if IsLocalLoginEnabled(instanceID) {
		return true
	}
	return CountUsableAuthProviders(instanceID) == 0
}

// CountUsableAuthProviders counts providers that could actually complete a
// sign-in right now: enabled, and permitted by the licence.
func CountUsableAuthProviders(instanceID string) int {
	if !GetLicenseState(instanceID).Feature("oidc") {
		return 0
	}
	n, err := CountEnabledAuthProviders(instanceID)
	if err != nil {
		return 0
	}
	return n
}

// randomProviderID is 8 bytes hex: short enough to read in a URL, wide enough
// that guessing one is not a way to enumerate an instance's providers.
func randomProviderID() (string, error) {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}
