package services

import (
	"context"
	"errors"
	"fmt"
	"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/shared/provision"
	"go.mongodb.org/mongo-driver/v2/bson"
	"golang.org/x/crypto/bcrypt"
)

var ErrLastOwner = errors.New("this is the organization's last owner promote another member to owner first")

// ErrHQManaged is returned when a caller tries to change a user this instance
// does not own.
//
// An hq-sourced row is projected from a Vantage HQ account: HQ owns its role,
// its password and its existence. A role editable in two places is a role with
// two answers, and the loser is whichever writer ran first. Refusing here
// rather than merely hiding the control in web/ is the point — the API is the
// boundary, the UI is a courtesy.
var ErrHQManaged = errors.New("this member is managed in Vantage HQ; change their role or remove them from the HQ portal")

func CountUsers() (int64, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return db.Col("users").CountDocuments(ctx, bson.M{})
}

func CountInstanceUsers(instanceID string) (int64, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return db.Col("users").CountDocuments(ctx, bson.M{"instance_id": instanceID})
}

func countOtherOwners(instanceID, exceptUserID string) (int64, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return db.Col("users").CountDocuments(ctx, bson.M{
		"instance_id": instanceID,
		"role":        models.RoleOwner,
		"user_id":     bson.M{"$ne": exceptUserID},
	})
}

func GetUserInInstance(instanceID, userID string) (*models.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var u models.User
	err := db.Col("users").FindOne(ctx, bson.M{"user_id": userID, "instance_id": instanceID}).Decode(&u)
	if err != nil {
		return nil, err
	}
	return &u, nil
}

func CreateUser(instanceID, email, password, role, authSource string) (*models.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	u, err := provision.CreateUser(ctx, db.Database, instanceID, email, password, role, authSource)
	if errors.Is(err, provision.ErrEmailTaken) {
		// Preserve the exact error string the API returned before this call
		// was delegated to the shared module.
		return nil, fmt.Errorf("email already registered")
	}
	return u, err
}

// GetUserInInstanceByEmail finds a user by address WITHIN one instance.
//
// There is deliberately no unscoped lookup by email. users is unique on
// (instance_id, email), not on email alone, so an unscoped FindOne would return
// an arbitrary one of several matching users — which on the login path means
// signing someone into a tenant that is not theirs.
func GetUserInInstanceByEmail(instanceID, email string) (*models.User, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	var u models.User
	err := db.Col("users").FindOne(ctx, bson.M{
		"instance_id": instanceID,
		"email":       email,
	}).Decode(&u)
	if err != nil {
		return nil, err
	}
	return &u, nil
}

func VerifyPassword(u *models.User, password string) bool {
	if u == nil || u.PasswordHash == "" {
		return false
	}
	return bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil
}

func TouchLastLogin(userID string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	now := time.Now()
	_, err := db.Col("users").UpdateOne(ctx, bson.M{"user_id": userID},
		bson.M{"$set": bson.M{"last_login": now}})
	return err
}

func ListUsers(instanceID string) ([]models.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cursor, err := db.Col("users").Find(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)
	var users []models.User
	if err := cursor.All(ctx, &users); err != nil {
		return nil, err
	}
	return users, nil
}

func UpdateUserRole(instanceID, userID, role string) error {
	if !models.ValidRole(role) {
		return fmt.Errorf("invalid role %q", role)
	}
	target, err := GetUserInInstance(instanceID, userID)
	if err != nil {
		return fmt.Errorf("user not found")
	}
	if target.AuthSource == models.AuthHQ {
		return ErrHQManaged
	}

	if target.Role == models.RoleOwner && role != models.RoleOwner {
		others, err := countOtherOwners(instanceID, userID)
		if err != nil {
			return err
		}
		if others == 0 {
			return ErrLastOwner
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err = db.Col("users").UpdateOne(ctx,
		bson.M{"user_id": userID, "instance_id": instanceID},
		bson.M{"$set": bson.M{"role": role}})
	return err
}

func DeleteUser(instanceID, userID string) error {
	target, err := GetUserInInstance(instanceID, userID)
	if err != nil {
		return fmt.Errorf("user not found")
	}
	if target.AuthSource == models.AuthHQ {
		return ErrHQManaged
	}
	if target.Role == models.RoleOwner {
		others, err := countOtherOwners(instanceID, userID)
		if err != nil {
			return err
		}
		if others == 0 {
			return ErrLastOwner
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err = db.Col("users").DeleteOne(ctx, bson.M{"user_id": userID, "instance_id": instanceID})
	return err
}
