package auth

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

const (
	tokenTypeAccess  = "access"
	tokenTypeRefresh = "refresh"
)

var ErrInvalidToken = errors.New("invalid token")

type JWTManager struct {
	secret        []byte
	accessTTL     time.Duration
	refreshTTL    time.Duration
	signingMethod jwt.SigningMethod
}

type Claims struct {
	UserID      uint64 `json:"uid"`
	Phone       string `json:"phone"`
	TokenType   string `json:"typ"`
	SubjectType string `json:"sub_type"`
	jwt.RegisteredClaims
}

type TokenPair struct {
	AccessToken      string `json:"access_token"`
	RefreshToken     string `json:"refresh_token"`
	TokenType        string `json:"token_type"`
	ExpiresInSeconds int64  `json:"expires_in"`
}

func NewJWTManager(secret string) *JWTManager {
	return &JWTManager{
		secret:        []byte(secret),
		accessTTL:     2 * time.Hour,
		refreshTTL:    14 * 24 * time.Hour,
		signingMethod: jwt.SigningMethodHS256,
	}
}

func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error) {
	return m.GenerateSubjectPair(userID, phone, "user")
}

func (m *JWTManager) GenerateSubjectPair(userID uint64, subject string, subjectType string) (TokenPair, error) {
	accessToken, err := m.generate(userID, subject, subjectType, tokenTypeAccess, m.accessTTL)
	if err != nil {
		return TokenPair{}, err
	}
	refreshToken, err := m.generate(userID, subject, subjectType, tokenTypeRefresh, m.refreshTTL)
	if err != nil {
		return TokenPair{}, err
	}
	return TokenPair{
		AccessToken:      accessToken,
		RefreshToken:     refreshToken,
		TokenType:        "Bearer",
		ExpiresInSeconds: int64(m.accessTTL.Seconds()),
	}, nil
}

func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) {
	return m.ParseSubject(tokenText, expectedType, "")
}

func (m *JWTManager) ParseSubject(tokenText, expectedType string, expectedSubjectType string) (*Claims, error) {
	claims := &Claims{}
	token, err := jwt.ParseWithClaims(tokenText, claims, func(token *jwt.Token) (any, error) {
		if token.Method != m.signingMethod {
			return nil, ErrInvalidToken
		}
		return m.secret, nil
	})
	if err != nil || !token.Valid {
		return nil, ErrInvalidToken
	}
	if claims.TokenType != expectedType {
		return nil, ErrInvalidToken
	}
	if expectedSubjectType != "" && claims.SubjectType != expectedSubjectType {
		return nil, ErrInvalidToken
	}
	return claims, nil
}

func (m *JWTManager) generate(userID uint64, subject string, subjectType string, tokenType string, ttl time.Duration) (string, error) {
	now := time.Now()
	claims := Claims{
		UserID:      userID,
		Phone:       subject,
		TokenType:   tokenType,
		SubjectType: subjectType,
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   subject,
			IssuedAt:  jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
		},
	}
	token := jwt.NewWithClaims(m.signingMethod, claims)
	return token.SignedString(m.secret)
}
