package usecase

import (
	"context"
	"errors"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/gofrs/uuid/v5"
	"golang.org/x/crypto/bcrypt"

	"github.com/saveinmed/backend-go/internal/domain"
)

// Login validates credentials via username or email and returns a signed JWT.
func (s *Service) Login(ctx context.Context, identifier, password string) (string, time.Time, error) {
	if strings.TrimSpace(identifier) == "" {
		return "", time.Time{}, errors.New("identifier is required")
	}

	user, err := s.repo.GetUserByUsername(ctx, identifier)
	if err != nil {
		user, err = s.repo.GetUserByEmail(ctx, identifier)
		if err != nil {
			return "", time.Time{}, errors.New("invalid credentials")
		}
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil {
		return "", time.Time{}, errors.New("invalid credentials")
	}

	return s.issueAccessToken(user)
}

// Authenticate is an alias for Login kept for backward compatibility.
func (s *Service) Authenticate(ctx context.Context, identifier, password string) (string, time.Time, error) {
	user, err := s.repo.GetUserByUsername(ctx, identifier)
	if err != nil {
		user, err = s.repo.GetUserByEmail(ctx, identifier)
		if err != nil {
			return "", time.Time{}, errors.New("invalid credentials")
		}
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil {
		return "", time.Time{}, errors.New("invalid credentials")
	}

	if user.CompanyID != uuid.Nil {
		if _, err := s.repo.GetCompany(ctx, user.CompanyID); err != nil {
			return "", time.Time{}, errors.New("associated company not found")
		}
	}

	return s.issueAccessToken(user)
}

// RegisterAccount creates the company (if provided) and the user bound to it.
func (s *Service) RegisterAccount(ctx context.Context, company *domain.Company, user *domain.User, password string) error {
	if company != nil {
		if company.ID == uuid.Nil {
			company.ID = uuid.Must(uuid.NewV7())
			company.IsVerified = false
			if err := s.repo.CreateCompany(ctx, company); err != nil {
				return err
			}
		} else {
			if _, err := s.repo.GetCompany(ctx, company.ID); err != nil {
				return err
			}
		}
		user.CompanyID = company.ID
	}
	return s.CreateUser(ctx, user, password)
}

// RefreshToken validates the provided JWT and issues a new access token.
func (s *Service) RefreshToken(ctx context.Context, tokenStr string) (string, time.Time, error) {
	claims, err := s.parseToken(tokenStr)
	if err != nil {
		return "", time.Time{}, err
	}

	if scope, ok := claims["scope"].(string); ok && scope != "" {
		return "", time.Time{}, errors.New("invalid token scope")
	}

	sub, ok := claims["sub"].(string)
	if !ok || sub == "" {
		return "", time.Time{}, errors.New("invalid token subject")
	}

	userID, err := uuid.FromString(sub)
	if err != nil {
		return "", time.Time{}, errors.New("invalid token subject")
	}

	user, err := s.repo.GetUser(ctx, userID)
	if err != nil {
		return "", time.Time{}, err
	}

	return s.issueAccessToken(user)
}

// CreatePasswordResetToken generates a short-lived JWT for password reset.
func (s *Service) CreatePasswordResetToken(ctx context.Context, email string) (string, time.Time, error) {
	user, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil {
		return "", time.Time{}, err
	}

	expiresAt := time.Now().Add(passwordResetTTL)
	claims := jwt.MapClaims{
		"sub":   user.ID.String(),
		"scope": "password_reset",
	}
	signed, err := s.signToken(claims, expiresAt)
	if err != nil {
		return "", time.Time{}, err
	}
	return signed, expiresAt, nil
}

// ResetPassword validates the reset token and updates the user password.
func (s *Service) ResetPassword(ctx context.Context, tokenStr, newPassword string) error {
	claims, err := s.parseToken(tokenStr)
	if err != nil {
		return err
	}

	scope, _ := claims["scope"].(string)
	if scope != "password_reset" {
		return errors.New("invalid token scope")
	}

	sub, ok := claims["sub"].(string)
	if !ok || sub == "" {
		return errors.New("invalid token subject")
	}

	userID, err := uuid.FromString(sub)
	if err != nil {
		return errors.New("invalid token subject")
	}

	user, err := s.repo.GetUser(ctx, userID)
	if err != nil {
		return err
	}
	return s.UpdateUser(ctx, user, newPassword)
}

// VerifyEmail marks the user email as verified using a scoped JWT.
func (s *Service) VerifyEmail(ctx context.Context, tokenStr string) (*domain.User, error) {
	claims, err := s.parseToken(tokenStr)
	if err != nil {
		return nil, err
	}

	if scope, ok := claims["scope"].(string); ok && scope != "" && scope != "email_verify" {
		return nil, errors.New("invalid token scope")
	}

	sub, ok := claims["sub"].(string)
	if !ok || sub == "" {
		return nil, errors.New("invalid token subject")
	}

	userID, err := uuid.FromString(sub)
	if err != nil {
		return nil, errors.New("invalid token subject")
	}

	user, err := s.repo.GetUser(ctx, userID)
	if err != nil {
		return nil, err
	}

	if !user.EmailVerified {
		user.EmailVerified = true
		if err := s.repo.UpdateUser(ctx, user); err != nil {
			return nil, err
		}
	}
	return user, nil
}

// CreateAdminIfMissing idempotently creates the platform admin account.
func (s *Service) CreateAdminIfMissing(ctx context.Context, email, username, password string) error {
	if _, err := s.repo.GetUserByEmail(ctx, email); err == nil {
		return nil
	}
	return s.CreateUser(ctx, &domain.User{
		Role:          "Admin",
		Name:          "Administrador",
		Username:      username,
		Email:         email,
		EmailVerified: true,
	}, password)
}

// CreateLojistaIfMissing idempotently creates a lojista (seller) seed account.
func (s *Service) CreateLojistaIfMissing(ctx context.Context, email, username, password, cnpj, corporateName string) error {
	if _, err := s.repo.GetUserByEmail(ctx, email); err == nil {
		return nil
	}

	company := &domain.Company{
		ID:            uuid.Must(uuid.NewV7()),
		CNPJ:          cnpj,
		CorporateName: corporateName,
		Category:      "distribuidora",
		IsVerified:    true,
	}
	user := &domain.User{
		Role:          "Dono",
		Name:          corporateName + " Admin",
		Username:      username,
		Email:         email,
		EmailVerified: true,
	}
	return s.RegisterAccount(ctx, company, user, password)
}

// issueAccessToken signs a JWT with user identity claims.
func (s *Service) issueAccessToken(user *domain.User) (string, time.Time, error) {
	expiresAt := time.Now().Add(s.tokenTTL)
	claims := jwt.MapClaims{
		"sub":        user.ID.String(),
		"role":       user.Role,
		"company_id": user.CompanyID.String(),
	}
	signed, err := s.signToken(claims, expiresAt)
	if err != nil {
		return "", time.Time{}, err
	}
	return signed, expiresAt, nil
}

// signToken appends expiry and produces a signed HS256 JWT.
func (s *Service) signToken(claims jwt.MapClaims, expiresAt time.Time) (string, error) {
	claims["exp"] = expiresAt.Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(s.jwtSecret)
}

// parseToken validates and decodes a JWT signed by this service.
func (s *Service) parseToken(tokenStr string) (jwt.MapClaims, error) {
	if strings.TrimSpace(tokenStr) == "" {
		return nil, errors.New("token is required")
	}

	claims := jwt.MapClaims{}
	parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
	token, err := parser.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) {
		return s.jwtSecret, nil
	})
	if err != nil || !token.Valid {
		return nil, errors.New("invalid token")
	}
	return claims, nil
}

// pepperPassword appends the configured pepper to a raw password before hashing.
func (s *Service) pepperPassword(password string) string {
	if s.passwordPepper == "" {
		return password
	}
	return password + s.passwordPepper
}
