package auth

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"time"

	"bookra/apps/backend/internal/db"

	"github.com/golang-jwt/jwt/v5"
	"github.com/jackc/pgx/v5"
	"golang.org/x/crypto/bcrypt"
)

const (
	accessTokenTTL   = 24 * time.Hour
	refreshTokenTTL  = 30 * 24 * time.Hour
	magicLinkTTL     = 15 * time.Minute
	passwordResetTTL = 30 * time.Minute
)

var (
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrInvalidToken       = errors.New("invalid or expired token")
	ErrUserNotFound       = errors.New("user not found")
	ErrEmailAlreadyExists = errors.New("email already exists")
	ErrPasswordTooShort   = errors.New("password must be at least 8 characters")
	ErrMagicLinkExpired   = errors.New("magic link expired")
	ErrMagicLinkUsed      = errors.New("magic link already used")
	ErrInvalidResetToken  = errors.New("invalid or expired reset token")
)

type TokenPair struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken,omitempty"`
	TokenType    string `json:"tokenType"`
	ExpiresIn    int    `json:"expiresIn"`
}

type Claims struct {
	UserID string `json:"sub"`
	Email  string `json:"email"`
	Name   string `json:"name,omitempty"`
	Role   string `json:"role"`
	Type   string `json:"type"`
	jwt.RegisteredClaims
}

type Service struct {
	repo      db.Repository
	jwtSecret []byte
}

func NewService(repo db.Repository, jwtSecret string) *Service {
	return &Service{
		repo:      repo,
		jwtSecret: []byte(jwtSecret),
	}
}

// RegisterWithPassword creates a new user with email and password
func (s *Service) RegisterWithPassword(ctx context.Context, email, password, name string) (*db.UserRecord, *TokenPair, error) {
	if len(password) < 8 {
		return nil, nil, ErrPasswordTooShort
	}

	// Check if user exists
	existing, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, nil, err
	}
	if existing != nil {
		return nil, nil, ErrEmailAlreadyExists
	}

	// Hash password
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, nil, err
	}

	// Create user
	user, err := s.repo.CreateUser(ctx, email, string(hash), name, "email", "user")
	if err != nil {
		return nil, nil, err
	}

	// Generate tokens
	tokens, err := s.generateTokenPair(user.ID.String(), email, name, "user")
	if err != nil {
		return nil, nil, err
	}

	return user, tokens, nil
}

// LoginWithPassword authenticates a user with email and password
func (s *Service) LoginWithPassword(ctx context.Context, email, password string) (*db.UserRecord, *TokenPair, error) {
	user, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil, ErrInvalidCredentials
		}
		return nil, nil, err
	}

	if user.PasswordHash == nil {
		return nil, nil, ErrInvalidCredentials
	}

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

	// Update last login
	if err := s.repo.UpdateLastLogin(ctx, user.ID.String()); err != nil {
		// Log but don't fail
	}

	tokens, err := s.generateTokenPair(user.ID.String(), user.Email, derefString(user.Name), user.Role)
	if err != nil {
		return nil, nil, err
	}

	return user, tokens, nil
}

// CreateMagicLink generates a magic link for passwordless auth
func (s *Service) CreateMagicLink(ctx context.Context, email string) (string, error) {
	// Get or create user
	user, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return "", err
	}

	if user == nil {
		user, err = s.repo.CreateUser(ctx, email, "", "", "email", "user")
		if err != nil {
			return "", err
		}
	}

	// Generate token
	token := generateRandomToken(32)
	expiresAt := time.Now().Add(magicLinkTTL)

	if err := s.repo.CreateMagicLink(ctx, token, user.ID.String(), email, expiresAt); err != nil {
		return "", err
	}

	return token, nil
}

// VerifyMagicLink validates a magic link and returns tokens
func (s *Service) VerifyMagicLink(ctx context.Context, token string) (*db.UserRecord, *TokenPair, error) {
	ml, err := s.repo.GetMagicLink(ctx, token)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil, ErrInvalidToken
		}
		return nil, nil, err
	}

	if ml.Used {
		return nil, nil, ErrMagicLinkUsed
	}

	if time.Now().After(ml.ExpiresAt) {
		return nil, nil, ErrMagicLinkExpired
	}

	// Mark as used
	if err := s.repo.MarkMagicLinkUsed(ctx, token); err != nil {
		return nil, nil, err
	}

	// Get user
	user, err := s.repo.GetUserByID(ctx, ml.UserID.String())
	if err != nil {
		return nil, nil, err
	}

	// Mark email as verified
	if err := s.repo.MarkEmailVerified(ctx, user.ID.String()); err != nil {
		// Log but don't fail
	}

	// Update last login
	if err := s.repo.UpdateLastLogin(ctx, user.ID.String()); err != nil {
		// Log but don't fail
	}

	tokens, err := s.generateTokenPair(user.ID.String(), user.Email, derefString(user.Name), user.Role)
	if err != nil {
		return nil, nil, err
	}

	return user, tokens, nil
}

// RefreshToken refreshes an access token using a refresh token
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (*TokenPair, error) {
	claims, err := s.ValidateToken(refreshToken)
	if err != nil {
		return nil, ErrInvalidToken
	}

	if claims.Type != "refresh" {
		return nil, ErrInvalidToken
	}

	user, err := s.repo.GetUserByID(ctx, claims.UserID)
	if err != nil {
		return nil, ErrUserNotFound
	}

	return s.generateTokenPair(user.ID.String(), user.Email, derefString(user.Name), user.Role)
}

// ValidateToken validates a JWT token and returns claims
func (s *Service) ValidateToken(tokenString string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return s.jwtSecret, nil
	})

	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}

	return nil, ErrInvalidToken
}

// GetUser retrieves a user by ID
func (s *Service) GetUser(ctx context.Context, userID string) (*db.UserRecord, error) {
	return s.repo.GetUserByID(ctx, userID)
}

// IsAdmin checks if the user has admin role
func (s *Service) IsAdmin(ctx context.Context, userID string) (bool, error) {
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return false, err
	}
	return user.Role == "admin" || user.Role == "superadmin", nil
}

// generateTokenPair creates access and refresh tokens
func (s *Service) generateTokenPair(userID, email, name, role string) (*TokenPair, error) {
	now := time.Now()

	// Access token
	accessClaims := Claims{
		UserID: userID,
		Email:  email,
		Name:   name,
		Role:   role,
		Type:   "access",
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(now.Add(accessTokenTTL)),
			IssuedAt:  jwt.NewNumericDate(now),
			NotBefore: jwt.NewNumericDate(now),
		},
	}

	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
	accessTokenString, err := accessToken.SignedString(s.jwtSecret)
	if err != nil {
		return nil, err
	}

	// Refresh token
	refreshClaims := Claims{
		UserID: userID,
		Email:  email,
		Role:   role,
		Type:   "refresh",
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(now.Add(refreshTokenTTL)),
			IssuedAt:  jwt.NewNumericDate(now),
			NotBefore: jwt.NewNumericDate(now),
		},
	}

	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
	refreshTokenString, err := refreshToken.SignedString(s.jwtSecret)
	if err != nil {
		return nil, err
	}

	return &TokenPair{
		AccessToken:  accessTokenString,
		RefreshToken: refreshTokenString,
		TokenType:    "Bearer",
		ExpiresIn:    int(accessTokenTTL.Seconds()),
	}, nil
}

func generateRandomToken(length int) string {
	b := make([]byte, length)
	rand.Read(b)
	return base64.URLEncoding.EncodeToString(b)
}

func derefString(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}
