package user

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"sync"
	"time"

	"connectrpc.com/connect"
	"connectrpc.com/validate"
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/spotdemo4/trevstack/server/internal/auth"
	userv1 "github.com/spotdemo4/trevstack/server/internal/connect/user/v1"
	"github.com/spotdemo4/trevstack/server/internal/connect/user/v1/userv1connect"
	"github.com/spotdemo4/trevstack/server/internal/interceptors"
	"github.com/spotdemo4/trevstack/server/internal/sqlc"
	"golang.org/x/crypto/bcrypt"
)

type AuthHandler struct {
	db       *sqlc.Queries
	webAuthn *webauthn.WebAuthn
	key      []byte
	name     string

	sessions *map[string]*webauthn.SessionData
	mu       sync.Mutex
}

func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) {
	// Get user
	user, err := h.db.GetUserbyUsername(ctx, req.Msg.Username)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, connect.NewError(connect.CodePermissionDenied, err)
		}

		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Check password
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.Password)); err != nil {
		return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
	}

	// Create JWT
	token, cookie, err := h.createJWT(user.ID)
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Create response
	res := connect.NewResponse(&userv1.LoginResponse{
		Token: token,
	})
	res.Header().Set("Set-Cookie", cookie.String())

	return res, nil
}

func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) {
	// Get user
	_, err := h.db.GetUserbyUsername(ctx, req.Msg.Username)
	if err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, connect.NewError(connect.CodeInternal, err)
		}
	} else {
		return nil, connect.NewError(connect.CodeAlreadyExists, errors.New("user already exists"))
	}

	// Check if confirmation passwords match
	if req.Msg.Password != req.Msg.ConfirmPassword {
		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
	}

	// Hash password
	hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Create user
	_, err = h.db.InsertUser(ctx, sqlc.InsertUserParams{
		Username:   req.Msg.Username,
		Password:   string(hash),
		WebauthnID: uuid.New().String(),
	})
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	res := connect.NewResponse(&userv1.SignUpResponse{})
	return res, nil
}

func (h *AuthHandler) Logout(_ context.Context, _ *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) {
	// Clear cookie
	cookie := http.Cookie{
		Name:     "token",
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	}

	res := connect.NewResponse(&userv1.LogoutResponse{})
	res.Header().Set("Set-Cookie", cookie.String())

	return res, nil
}

func (h *AuthHandler) BeginPasskeyLogin(ctx context.Context, req *connect.Request[userv1.BeginPasskeyLoginRequest]) (*connect.Response[userv1.BeginPasskeyLoginResponse], error) {
	// Get user
	pUser, _, err := h.getPasskeyUser(ctx, req.Msg.Username)
	if err != nil {
		return nil, connect.NewError(connect.CodeUnauthenticated, err)
	}

	// Get options for user
	options, session, err := h.webAuthn.BeginLogin(pUser)
	if err != nil {
		return nil, connect.NewError(connect.CodeInvalidArgument, err)
	}

	// Turn the options into json
	optionsJSON, err := json.Marshal(options)
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Set session for validation later
	h.setSession(req.Msg.Username, session)

	return connect.NewResponse(&userv1.BeginPasskeyLoginResponse{
		OptionsJson: string(optionsJSON),
	}), nil
}

func (h *AuthHandler) FinishPasskeyLogin(ctx context.Context, req *connect.Request[userv1.FinishPasskeyLoginRequest]) (*connect.Response[userv1.FinishPasskeyLoginResponse], error) {
	// Get user
	pUser, userID, err := h.getPasskeyUser(ctx, req.Msg.Username)
	if err != nil {
		return nil, connect.NewError(connect.CodeInvalidArgument, err)
	}

	// Get the session data previously set
	session, err := h.getSession(req.Msg.Username)
	if err != nil {
		return nil, connect.NewError(connect.CodeInvalidArgument, err)
	}

	// Parse the attestation response
	parsedResponse, err := protocol.ParseCredentialRequestResponseBytes([]byte(req.Msg.Attestation))
	if err != nil {
		return nil, connect.NewError(connect.CodeInvalidArgument, err)
	}

	// Validate the login
	credential, err := h.webAuthn.ValidateLogin(pUser, *session, parsedResponse)
	if err != nil {
		return nil, connect.NewError(connect.CodeUnauthenticated, err)
	}

	// Update the credential in the database
	lastUsed := time.Now()
	signCount := int64(credential.Authenticator.SignCount)
	err = h.db.UpdateCredential(ctx, sqlc.UpdateCredentialParams{
		// set
		LastUsed:  &lastUsed,
		SignCount: &signCount,

		// where
		ID:     string(credential.ID),
		UserID: userID,
	})
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Create JWT
	token, cookie, err := h.createJWT(userID)
	if err != nil {
		return nil, connect.NewError(connect.CodeInternal, err)
	}

	// Create response
	resp := connect.NewResponse(&userv1.FinishPasskeyLoginResponse{
		Token: token,
	})
	resp.Header().Set("Set-Cookie", cookie.String())

	return resp, nil
}

func (h *AuthHandler) getPasskeyUser(ctx context.Context, username string) (*auth.User, int64, error) {
	user, err := h.db.GetUserbyUsername(ctx, username)
	if err != nil {
		return nil, 0, err
	}

	creds, err := h.db.GetCredentials(ctx, user.ID)
	if err != nil {
		return nil, 0, err
	}

	webCreds := auth.NewCreds(creds)
	webUser := auth.NewUser(user.WebauthnID, user.Username, webCreds)

	return &webUser, user.ID, nil
}

func (h *AuthHandler) getSession(username string) (*webauthn.SessionData, error) {
	h.mu.Lock()
	defer h.mu.Unlock()

	session, ok := (*h.sessions)[username]
	if !ok {
		return nil, errors.New("session does not exist")
	}

	delete(*h.sessions, username)
	return session, nil
}

func (h *AuthHandler) setSession(username string, data *webauthn.SessionData) {
	h.mu.Lock()
	defer h.mu.Unlock()

	(*h.sessions)[username] = data
}

func (h *AuthHandler) createJWT(userid int64) (string, *http.Cookie, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
		Issuer:  h.name,
		Subject: strconv.Itoa(int(userid)),
		IssuedAt: &jwt.NumericDate{
			Time: time.Now(),
		},
		ExpiresAt: &jwt.NumericDate{
			Time: time.Now().Add(time.Hour * 24),
		},
	})

	tokenString, err := token.SignedString(h.key)
	if err != nil {
		return "", nil, err
	}

	cookie := http.Cookie{
		Name:     "token",
		Value:    tokenString,
		Path:     "/",
		MaxAge:   86400,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	}

	return tokenString, &cookie, nil
}

func NewAuthHandler(vi *validate.Interceptor, db *sqlc.Queries, webauth *webauthn.WebAuthn, name string, key string) (string, http.Handler) {
	interceptors := connect.WithInterceptors(vi, interceptors.NewRateLimitInterceptor(key))

	sd := map[string]*webauthn.SessionData{}
	return userv1connect.NewAuthServiceHandler(
		&AuthHandler{
			db:       db,
			webAuthn: webauth,
			name:     name,
			key:      []byte(key),

			sessions: &sd,
			mu:       sync.Mutex{},
		},
		interceptors,
	)
}
