package security

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/niels/banking-app/backend/internal/domain"
)

type TokenManager struct {
	secret []byte
	issuer string
	ttl    time.Duration
}

type Claims struct {
	Subject     string   `json:"sub"`
	Email       string   `json:"email"`
	Role        string   `json:"role"`
	Scopes      []string `json:"scopes"`
	StepUpUntil int64    `json:"step_up_until,omitempty"`
	Issuer      string   `json:"iss"`
	ExpiresAt   int64    `json:"exp"`
	IssuedAt    int64    `json:"iat"`
}

func NewTokenManager(secret, issuer string, ttl time.Duration) *TokenManager {
	return &TokenManager{
		secret: []byte(secret),
		issuer: issuer,
		ttl:    ttl,
	}
}

func (m *TokenManager) Generate(user domain.User) (string, error) {
	return m.GenerateWithScopes(user, nil, time.Time{})
}

func (m *TokenManager) GenerateWithStepUp(user domain.User, stepUpUntil time.Time) (string, error) {
	return m.GenerateWithScopes(user, nil, stepUpUntil)
}

func (m *TokenManager) GenerateWithScopes(user domain.User, scopes []string, stepUpUntil time.Time) (string, error) {
	now := time.Now().UTC()
	effectiveScopes := scopes
	if len(effectiveScopes) == 0 {
		effectiveScopes = ScopesForRole(user.Role)
	}
	claims := Claims{
		Subject:   user.ID,
		Email:     user.Email,
		Role:      user.Role,
		Scopes:    effectiveScopes,
		Issuer:    m.issuer,
		IssuedAt:  now.Unix(),
		ExpiresAt: now.Add(m.ttl).Unix(),
	}
	if !stepUpUntil.IsZero() {
		claims.StepUpUntil = stepUpUntil.UTC().Unix()
	}

	header := map[string]string{"alg": "HS256", "typ": "JWT"}
	headerJSON, err := json.Marshal(header)
	if err != nil {
		return "", err
	}
	claimsJSON, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}

	unsigned := base64.RawURLEncoding.EncodeToString(headerJSON) + "." + base64.RawURLEncoding.EncodeToString(claimsJSON)
	return unsigned + "." + m.signature(unsigned), nil
}

func (m *TokenManager) Verify(rawToken string) (*Claims, error) {
	parts := strings.Split(rawToken, ".")
	if len(parts) != 3 {
		return nil, domain.ErrUnauthorized
	}

	unsigned := parts[0] + "." + parts[1]
	expected := m.signature(unsigned)
	if !hmac.Equal([]byte(expected), []byte(parts[2])) {
		return nil, domain.ErrUnauthorized
	}

	headerJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return nil, domain.ErrUnauthorized
	}
	var header struct {
		Alg string `json:"alg"`
		Typ string `json:"typ"`
	}
	if err := json.Unmarshal(headerJSON, &header); err != nil {
		return nil, domain.ErrUnauthorized
	}
	if header.Alg != "HS256" || header.Typ != "JWT" {
		return nil, domain.ErrUnauthorized
	}

	claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, domain.ErrUnauthorized
	}

	var claims Claims
	if err := json.Unmarshal(claimsJSON, &claims); err != nil {
		return nil, domain.ErrUnauthorized
	}

	now := time.Now().UTC().Unix()
	if claims.Issuer != m.issuer || claims.Subject == "" || claims.ExpiresAt <= now {
		return nil, domain.ErrUnauthorized
	}
	if claims.IssuedAt > time.Now().UTC().Add(2*time.Minute).Unix() {
		return nil, domain.ErrUnauthorized
	}
	if len(claims.Scopes) == 0 {
		claims.Scopes = ScopesForRole(claims.Role)
	}

	return &claims, nil
}

func (c Claims) HasScope(scope string) bool {
	return HasScope(c.Scopes, scope)
}

func (c Claims) HasFreshStepUp(now time.Time) bool {
	return c.StepUpUntil > now.UTC().Unix()
}

func (m *TokenManager) signature(unsigned string) string {
	mac := hmac.New(sha256.New, m.secret)
	_, _ = mac.Write([]byte(unsigned))
	return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}

func BearerToken(header string) (string, error) {
	const prefix = "Bearer "
	if !strings.HasPrefix(header, prefix) {
		return "", fmt.Errorf("%w: missing bearer token", domain.ErrUnauthorized)
	}

	token := strings.TrimSpace(strings.TrimPrefix(header, prefix))
	if token == "" {
		return "", domain.ErrUnauthorized
	}

	return token, nil
}
