package auth

import (
	"errors"
	"fmt"
	"strings"
	"system-altrak/internal/domain"
	"system-altrak/pkg/utils"
	"time"

	"gorm.io/gorm"
)

var (
	ErrInvalidAccessToken     = errors.New("invalid access token")
	ErrInvalidRefreshToken    = errors.New("invalid refresh token")
	ErrUnableToDetermineUser  = errors.New("unable to determine user session")
	ErrInvalidSession         = errors.New("invalid session")
	ErrFailedRevokeAccess     = errors.New("failed to revoke access token")
	ErrFailedRevokeUserTokens = errors.New("failed to revoke user tokens")
	ErrInvalidCredentials     = errors.New("invalid username or password")
)

type AuthService interface {
	Login(username, password string, rememberMe bool) (*domain.User, string, string, error)
	Refresh(refreshToken string) (string, string, bool, error)
	Logout(accessToken string, refreshToken string) error
	GetSession(userID uint) (*domain.User, error)
}

type serviceImpl struct {
	repo      AuthRepository
	jwtSecret string
}

func NewService(repo AuthRepository, jwtSecret string) AuthService {
	return &serviceImpl{
		repo:      repo,
		jwtSecret: jwtSecret,
	}
}

func (s *serviceImpl) Login(username, password string, rememberMe bool) (*domain.User, string, string, error) {
	normalizedUsername := strings.TrimSpace(username)
	if normalizedUsername == "" || strings.TrimSpace(password) == "" {
		return nil, "", "", ErrInvalidCredentials
	}

	user, err := s.repo.GetUserByUsername(normalizedUsername)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, "", "", ErrInvalidCredentials
		}
		return nil, "", "", fmt.Errorf("failed to load user: %w", err)
	}
	if !user.IsActive {
		return nil, "", "", ErrInvalidCredentials
	}
	if !utils.CheckPasswordHash(password, user.PasswordHash) {
		return nil, "", "", ErrInvalidCredentials
	}
	at, rt, err := utils.GenerateToken(user.ID, user.Username, user.Role, user.BranchID, s.jwtSecret)
	if err != nil {
		return nil, "", "", err
	}

	if err := s.repo.WithTransaction(func(txRepo AuthRepository) error {
		if err := txRepo.RevokeRefreshToken(user.ID); err != nil {
			return errors.New("failed to rotate refresh token")
		}

		newRefreshToken := &domain.RefreshToken{
			UserID:     user.ID,
			TokenHash:  utils.HashToken(rt),
			ExpiresAt:  time.Now().Add(7 * 24 * time.Hour),
			Persistent: rememberMe,
			Revoked:    false,
		}

		if err := txRepo.SaveRefreshToken(newRefreshToken); err != nil {
			return errors.New("failed to save refresh token")
		}

		return nil
	}); err != nil {
		return nil, "", "", err
	}

	user.PasswordHash = ""
	return user, at, rt, nil
}

func (s *serviceImpl) Refresh(refreshToken string) (string, string, bool, error) {
	_, err := utils.ValidateToken(refreshToken, s.jwtSecret)
	if err != nil {
		return "", "", false, errors.New("invalid refresh token")
	}

	rtHash := utils.HashToken(refreshToken)
	var (
		accessToken string
		newToken    string
		persistent  bool
	)

	if err := s.repo.WithTransaction(func(txRepo AuthRepository) error {
		storedToken, err := txRepo.GetRefreshToken(rtHash)
		if err != nil || storedToken.Revoked || storedToken.ExpiresAt.Before(time.Now()) {
			return errors.New("refresh token revoked or expired")
		}

		user, err := txRepo.GetUserByID(storedToken.UserID)
		if err != nil {
			return err
		}

		at, rt, err := utils.GenerateToken(user.ID, user.Username, user.Role, user.BranchID, s.jwtSecret)
		if err != nil {
			return err
		}

		newRefreshToken := &domain.RefreshToken{
			UserID:     user.ID,
			TokenHash:  utils.HashToken(rt),
			ExpiresAt:  time.Now().Add(7 * 24 * time.Hour),
			Persistent: storedToken.Persistent,
			Revoked:    false,
		}

		if err := txRepo.RevokeRefreshToken(storedToken.UserID); err != nil {
			return err
		}

		if err := txRepo.SaveRefreshToken(newRefreshToken); err != nil {
			return err
		}

		accessToken = at
		newToken = rt
		persistent = storedToken.Persistent
		return nil
	}); err != nil {
		return "", "", false, err
	}

	return accessToken, newToken, persistent, nil
}

func (s *serviceImpl) Logout(accessToken string, refreshToken string) error {
	var userID uint

	if accessToken != "" {
		claims, err := utils.ValidateToken(accessToken, s.jwtSecret)
		if err != nil {
			return ErrInvalidAccessToken
		}

		userID = claims.UserID
		if err := utils.RevokeToken(accessToken, s.jwtSecret); err != nil {
			return ErrFailedRevokeAccess
		}
	}

	if refreshToken != "" {
		claims, err := utils.ValidateToken(refreshToken, s.jwtSecret)
		if err != nil {
			return ErrInvalidRefreshToken
		}
		userID = claims.UserID
	}

	if userID == 0 {
		return ErrUnableToDetermineUser
	}

	if err := utils.RevokeAllUserTokens(userID); err != nil {
		return ErrFailedRevokeUserTokens
	}

	return s.repo.RevokeRefreshToken(userID)
}

func (s *serviceImpl) GetSession(userID uint) (*domain.User, error) {
	if userID == 0 {
		return nil, ErrInvalidSession
	}

	user, err := s.repo.GetUserByID(userID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrInvalidSession
		}
		return nil, fmt.Errorf("failed to load session user: %w", err)
	}

	if user == nil || !user.IsActive {
		return nil, ErrInvalidSession
	}

	userCopy := *user
	userCopy.PasswordHash = ""

	return &userCopy, nil
}
