package auth

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

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"golang.org/x/crypto/bcrypt"

	"logjensticks/internal/db"
)

var ErrInvalidCredentials = errors.New("invalid username or password")
var ErrSessionNotFound = errors.New("session not found or expired")

type User struct {
	Username     string `bson:"username"`
	PasswordHash string `bson:"password_hash"`
	Role         string `bson:"role"`
}

type Session struct {
	Token     string    `bson:"token"`
	Username  string    `bson:"username"`
	Role      string    `bson:"role"`
	ExpiresAt time.Time `bson:"expires_at"`
}

// Login verifies credentials, creates a session, and returns the session token.
func Login(ctx context.Context, dbc *db.Client, username, password string) (string, *Session, error) {
	var user User
	err := dbc.Users().FindOne(ctx, bson.D{{Key: "username", Value: username}}).Decode(&user)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return "", nil, ErrInvalidCredentials
	}
	if err != nil {
		return "", nil, err
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
		return "", nil, ErrInvalidCredentials
	}

	token, err := generateToken()
	if err != nil {
		return "", nil, err
	}

	session := Session{
		Token:     token,
		Username:  user.Username,
		Role:      user.Role,
		ExpiresAt: time.Now().UTC().Add(db.SessionTTL),
	}

	if _, err := dbc.Sessions().InsertOne(ctx, session); err != nil {
		return "", nil, err
	}

	return token, &session, nil
}

// ValidateSession looks up a session by token and confirms it has not expired.
func ValidateSession(ctx context.Context, dbc *db.Client, token string) (*Session, error) {
	var session Session
	err := dbc.Sessions().FindOne(ctx, bson.D{{Key: "token", Value: token}}).Decode(&session)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return nil, ErrSessionNotFound
	}
	if err != nil {
		return nil, err
	}
	if time.Now().After(session.ExpiresAt) {
		return nil, ErrSessionNotFound
	}
	return &session, nil
}

// DeleteSession removes a session document (used on logout).
func DeleteSession(ctx context.Context, dbc *db.Client, token string) error {
	_, err := dbc.Sessions().DeleteOne(ctx, bson.D{{Key: "token", Value: token}})
	return err
}

func generateToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}
