package auth

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"

	"switchboard-core/models"
	"switchboard-core/store"
)

const bcryptCost = 12

type BuiltinProvider struct{}

func NewBuiltinProvider() *BuiltinProvider { return &BuiltinProvider{} }

func (p *BuiltinProvider) Mode() Mode                { return ModeBuiltin }
func (p *BuiltinProvider) SupportsRegistration() bool { return true }

func (p *BuiltinProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) {
	var req struct {
		Login    string `json:"login" binding:"required"`
		Password string `json:"password" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		return nil, err
	}

	user, err := stores.Users.GetByLogin(c.Request.Context(), req.Login)
	if err != nil {
		return nil, ErrInvalidCreds
	}
	if !user.IsActive {
		return nil, ErrInactive
	}
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
		return nil, ErrInvalidCreds
	}

	return &Result{User: user, VaultHint: req.Password}, nil
}

func (p *BuiltinProvider) Register(c *gin.Context, stores store.Stores) (*Result, error) {
	var req struct {
		Username string `json:"username" binding:"required"`
		Email    string `json:"email" binding:"required"`
		Password string `json:"password" binding:"required,min=8"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		return nil, err
	}

	ctx := c.Request.Context()

	allowed, _ := stores.Policies.GetBool(ctx, "allow_registration")
	if !allowed {
		return nil, ErrRegistrationOff
	}

	if existing, _ := stores.Users.GetByUsername(ctx, req.Username); existing != nil {
		return nil, ErrDuplicate
	}
	if existing, _ := stores.Users.GetByEmail(ctx, req.Email); existing != nil {
		return nil, ErrDuplicate
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)
	if err != nil {
		return nil, err
	}

	handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(req.Username))

	defaultActive, _ := stores.Policies.GetBool(ctx, "default_user_active")

	user := &models.User{
		Username:     strings.ToLower(req.Username),
		Email:        strings.ToLower(req.Email),
		PasswordHash: string(hash),
		IsActive:     defaultActive,
		AuthSource:   string(ModeBuiltin),
		Handle:       handle,
	}

	if err := stores.Users.Create(ctx, user); err != nil {
		return nil, err
	}

	EnsureEveryoneGroup(ctx, stores, user.ID)

	return &Result{User: user, IsNewUser: true, VaultHint: req.Password}, nil
}

// UniqueHandle appends -2, -3, etc. on collision.
func UniqueHandle(ctx context.Context, users store.UserStore, base string) string {
	if base == "" {
		base = "user"
	}
	if u, _ := users.GetByHandle(ctx, base); u == nil {
		return base
	}
	for i := 2; i < 1000; i++ {
		candidate := fmt.Sprintf("%s-%d", base, i)
		if u, _ := users.GetByHandle(ctx, candidate); u == nil {
			return candidate
		}
	}
	return fmt.Sprintf("%s-%s", base, store.NewID()[:8])
}

func ErrorToHTTPStatus(err error) int {
	switch err {
	case ErrInvalidCreds:
		return http.StatusUnauthorized
	case ErrInactive:
		return http.StatusForbidden
	case ErrRegistrationOff:
		return http.StatusForbidden
	case ErrDuplicate:
		return http.StatusConflict
	case ErrNotSupported:
		return http.StatusMethodNotAllowed
	default:
		return http.StatusBadRequest
	}
}
