package services

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/base32"
	"encoding/base64"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
	"go.uber.org/zap"
	"golang.org/x/crypto/bcrypt"

	"github.com/RyanCopley/skybridge/user/internal/domain"
	"github.com/RyanCopley/skybridge/user/internal/repository/interfaces"
)

// AuthService defines the interface for authentication operations
type AuthService interface {
	// Authentication
	Login(ctx context.Context, req *domain.LoginRequest, ipAddress, userAgent string) (*domain.LoginResponse, error)
	Register(ctx context.Context, req *domain.RegisterRequest, ipAddress, userAgent string) (*domain.RegisterResponse, error)
	Logout(ctx context.Context, token string) error
	RefreshToken(ctx context.Context, token string) (*domain.LoginResponse, error)
	
	// Password management
	ForgotPassword(ctx context.Context, req *domain.ForgotPasswordRequest, ipAddress, userAgent string) error
	ResetPassword(ctx context.Context, req *domain.ResetPasswordRequest, ipAddress, userAgent string) error
	ChangePassword(ctx context.Context, userID uuid.UUID, req *domain.ChangePasswordRequest, actorID string) error
	
	// Email verification
	VerifyEmail(ctx context.Context, req *domain.VerifyEmailRequest, ipAddress, userAgent string) error
	ResendVerification(ctx context.Context, req *domain.ResendVerificationRequest, ipAddress, userAgent string) error
	
	// Two-factor authentication
	SetupTwoFactor(ctx context.Context, userID uuid.UUID) (*domain.SetupTwoFactorResponse, error)
	EnableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.EnableTwoFactorRequest, actorID string) error
	DisableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.DisableTwoFactorRequest, actorID string) error
	ValidateTwoFactor(ctx context.Context, req *domain.ValidateTwoFactorRequest, ipAddress, userAgent string) (*domain.LoginResponse, error)
	RegenerateTwoFactorBackupCodes(ctx context.Context, userID uuid.UUID, actorID string) ([]string, error)
	
	// Session management
	GetUserSessions(ctx context.Context, userID uuid.UUID) (*domain.ListSessionsResponse, error)
	RevokeSession(ctx context.Context, userID uuid.UUID, sessionID uuid.UUID, actorID string) error
	RevokeAllSessions(ctx context.Context, userID uuid.UUID, actorID string) error
	
	// Token validation
	ValidateToken(ctx context.Context, tokenString string) (*domain.AuthContext, error)
	
	// Security utilities
	IsAccountLocked(ctx context.Context, email string) (bool, *time.Time, error)
	RecordLoginAttempt(ctx context.Context, email, ipAddress, userAgent, failureReason string, success bool) error
}

type authService struct {
	userRepo         interfaces.UserRepository
	sessionRepo      interfaces.UserSessionRepository
	auditRepo        interfaces.AuditRepository
	logger           *zap.Logger
	jwtSecret        string
	jwtIssuer        string
	tokenExpiry      time.Duration
	bcryptCost       int
	maxLoginAttempts int
	lockoutDuration  time.Duration
}

// JWT Claims structure
type JWTClaims struct {
	UserID    string            `json:"user_id"`
	Email     string            `json:"email"`
	Role      string            `json:"role"`
	SessionID string            `json:"session_id"`
	TwoFactor bool              `json:"two_factor,omitempty"`
	TempToken bool              `json:"temp_token,omitempty"`
	jwt.RegisteredClaims
}

// NewAuthService creates a new authentication service
func NewAuthService(
	userRepo interfaces.UserRepository,
	sessionRepo interfaces.UserSessionRepository,
	auditRepo interfaces.AuditRepository,
	logger *zap.Logger,
	jwtSecret string,
	jwtIssuer string,
) AuthService {
	return &authService{
		userRepo:         userRepo,
		sessionRepo:      sessionRepo,
		auditRepo:        auditRepo,
		logger:           logger,
		jwtSecret:        jwtSecret,
		jwtIssuer:        jwtIssuer,
		tokenExpiry:      24 * time.Hour,
		bcryptCost:       12,
		maxLoginAttempts: 5,
		lockoutDuration:  15 * time.Minute,
	}
}

func (s *authService) Register(ctx context.Context, req *domain.RegisterRequest, ipAddress, userAgent string) (*domain.RegisterResponse, error) {
	// Check if user already exists
	exists, err := s.userRepo.ExistsByEmail(ctx, req.Email)
	if err != nil {
		return nil, fmt.Errorf("failed to check user existence: %w", err)
	}
	if exists {
		return nil, fmt.Errorf("user with email %s already exists", req.Email)
	}

	// Hash password
	passwordHash, err := s.hashPassword(req.Password)
	if err != nil {
		return nil, fmt.Errorf("failed to hash password: %w", err)
	}

	// Generate email verification token
	verificationToken, err := s.generateSecureToken()
	if err != nil {
		return nil, fmt.Errorf("failed to generate verification token: %w", err)
	}

	// Create user
	user := &domain.User{
		ID:                         uuid.New(),
		Email:                      req.Email,
		FirstName:                  req.FirstName,
		LastName:                   req.LastName,
		DisplayName:                req.DisplayName,
		Role:                       domain.UserRoleUser,
		Status:                     domain.UserStatusPending,
		PasswordHash:               passwordHash,
		EmailVerified:              false,
		EmailVerificationToken:     &verificationToken,
		EmailVerificationExpiresAt: timePtr(time.Now().Add(24 * time.Hour)),
		TwoFactorEnabled:           false,
		CreatedBy:                  req.Email,
		UpdatedBy:                  req.Email,
	}

	err = s.userRepo.Create(ctx, user)
	if err != nil {
		return nil, fmt.Errorf("failed to create user: %w", err)
	}

	// Log registration
	if s.auditRepo != nil {
		auditEvent := &interfaces.AuditEvent{
			ID:           uuid.New(),
			Type:         "auth.register",
			Severity:     "info",
			Status:       "success",
			Timestamp:    time.Now().Format(time.RFC3339),
			ActorID:      user.Email,
			ActorType:    "user",
			ActorIP:      ipAddress,
			UserAgent:    userAgent,
			ResourceID:   user.ID.String(),
			ResourceType: "user",
			Action:       "register",
			Description:  fmt.Sprintf("User %s registered", user.Email),
			Details: map[string]interface{}{
				"user_id": user.ID.String(),
				"email":   user.Email,
			},
		}
		_ = s.auditRepo.LogEvent(ctx, auditEvent)
	}

	// TODO: Send welcome email with verification link
	
	return &domain.RegisterResponse{
		User:    user,
		Message: "Registration successful. Please check your email for verification instructions.",
	}, nil
}

func (s *authService) Login(ctx context.Context, req *domain.LoginRequest, ipAddress, userAgent string) (*domain.LoginResponse, error) {
	// Check if account is locked
	locked, lockedUntil, err := s.IsAccountLocked(ctx, req.Email)
	if err != nil {
		s.logger.Error("Failed to check account lock status", zap.String("email", req.Email), zap.Error(err))
		return nil, fmt.Errorf("authentication failed")
	}
	
	if locked {
		_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "account_locked", false)
		return nil, fmt.Errorf("account is temporarily locked until %v", lockedUntil.Format(time.RFC3339))
	}

	// Get user
	user, err := s.userRepo.GetByEmail(ctx, req.Email)
	if err != nil {
		_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "user_not_found", false)
		return nil, fmt.Errorf("authentication failed")
	}

	// Verify password
	if !s.verifyPassword(req.Password, user.PasswordHash) {
		_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "invalid_password", false)
		_ = s.incrementFailedAttempts(ctx, user.ID)
		return nil, fmt.Errorf("authentication failed")
	}

	// Check user status
	if user.Status != domain.UserStatusActive {
		_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, fmt.Sprintf("user_status_%s", user.Status), false)
		return nil, fmt.Errorf("account is not active")
	}

	// Check email verification
	if !user.EmailVerified {
		_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "email_not_verified", false)
		return nil, fmt.Errorf("please verify your email address before logging in")
	}

	// Handle two-factor authentication
	if user.TwoFactorEnabled {
		if req.TwoFactorCode == nil {
			// Generate temporary token for 2FA completion
			tempToken, err := s.generateTempTwoFactorToken(user, ipAddress, userAgent)
			if err != nil {
				return nil, fmt.Errorf("failed to generate temporary token: %w", err)
			}
			
			return &domain.LoginResponse{
				RequiresTwoFactor:  true,
				TwoFactorTempToken: &tempToken,
			}, nil
		}

		// Validate 2FA code
		valid, err := s.validateTOTPCode(*req.TwoFactorCode, *user.TwoFactorSecret)
		if err != nil || !valid {
			_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "invalid_2fa_code", false)
			return nil, fmt.Errorf("invalid two-factor authentication code")
		}
	}

	// Reset failed login attempts on successful login
	_ = s.resetFailedAttempts(ctx, user.ID)

	// Create session
	session, err := s.createSession(ctx, user, ipAddress, userAgent, req.RememberMe)
	if err != nil {
		return nil, fmt.Errorf("failed to create session: %w", err)
	}

	// Generate JWT token
	token, expiresAt, err := s.generateJWTToken(user, session.ID.String(), false)
	if err != nil {
		return nil, fmt.Errorf("failed to generate token: %w", err)
	}

	// Update last login
	_ = s.userRepo.UpdateLastLogin(ctx, user.ID)

	// Record successful login
	_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "", true)

	// Log successful login
	if s.auditRepo != nil {
		auditEvent := &interfaces.AuditEvent{
			ID:           uuid.New(),
			Type:         "auth.login",
			Severity:     "info",
			Status:       "success",
			Timestamp:    time.Now().Format(time.RFC3339),
			ActorID:      user.Email,
			ActorType:    "user",
			ActorIP:      ipAddress,
			UserAgent:    userAgent,
			ResourceID:   user.ID.String(),
			ResourceType: "user",
			Action:       "login",
			Description:  fmt.Sprintf("User %s logged in", user.Email),
			SessionID:    session.ID.String(),
			Details: map[string]interface{}{
				"user_id":    user.ID.String(),
				"session_id": session.ID.String(),
				"two_factor": user.TwoFactorEnabled,
			},
		}
		_ = s.auditRepo.LogEvent(ctx, auditEvent)
	}

	return &domain.LoginResponse{
		User:      user,
		Token:     token,
		ExpiresAt: expiresAt,
	}, nil
}

// Helper functions

func (s *authService) hashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), s.bcryptCost)
	return string(bytes), err
}

func (s *authService) verifyPassword(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

func (s *authService) generateSecureToken() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(bytes), nil
}

func (s *authService) generateJWTToken(user *domain.User, sessionID string, isTempToken bool) (string, time.Time, error) {
	expiresAt := time.Now().Add(s.tokenExpiry)
	if isTempToken {
		expiresAt = time.Now().Add(5 * time.Minute) // Temp tokens expire in 5 minutes
	}

	claims := JWTClaims{
		UserID:    user.ID.String(),
		Email:     user.Email,
		Role:      string(user.Role),
		SessionID: sessionID,
		TwoFactor: user.TwoFactorEnabled,
		TempToken: isTempToken,
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   user.ID.String(),
			Issuer:    s.jwtIssuer,
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ExpiresAt: jwt.NewNumericDate(expiresAt),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(s.jwtSecret))
	if err != nil {
		return "", time.Time{}, err
	}

	return tokenString, expiresAt, nil
}

func (s *authService) generateTempTwoFactorToken(user *domain.User, ipAddress, userAgent string) (string, error) {
	token, _, err := s.generateJWTToken(user, "", true)
	return token, err
}

func (s *authService) createSession(ctx context.Context, user *domain.User, ipAddress, userAgent string, rememberMe bool) (*domain.UserSession, error) {
	expiresAt := time.Now().Add(s.tokenExpiry)
	if rememberMe {
		expiresAt = time.Now().Add(30 * 24 * time.Hour) // 30 days for remember me
	}

	sessionToken, err := s.generateSecureToken()
	if err != nil {
		return nil, err
	}

	session := &domain.UserSession{
		ID:         uuid.New(),
		UserID:     user.ID,
		Token:      sessionToken,
		IPAddress:  ipAddress,
		UserAgent:  userAgent,
		ExpiresAt:  expiresAt,
		LastUsedAt: time.Now(),
	}

	err = s.sessionRepo.Create(ctx, session)
	if err != nil {
		return nil, err
	}

	return session, nil
}

func (s *authService) incrementFailedAttempts(ctx context.Context, userID uuid.UUID) error {
	return s.userRepo.IncrementFailedAttempts(ctx, userID, s.lockoutDuration)
}

func (s *authService) resetFailedAttempts(ctx context.Context, userID uuid.UUID) error {
	return s.userRepo.ResetFailedAttempts(ctx, userID)
}

func (s *authService) validateTOTPCode(code, secret string) (bool, error) {
	return totp.Validate(code, secret, time.Now()), nil
}

func (s *authService) generateBackupCode() (string, error) {
	// Generate 8-digit backup code
	bytes := make([]byte, 4)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	
	code := ""
	for _, b := range bytes {
		code += fmt.Sprintf("%02d", int(b)%100)
	}
	
	return code, nil
}

// Utility function
func timePtr(t time.Time) *time.Time {
	return &t
}

// Placeholder implementations for other methods (to be completed)
func (s *authService) Logout(ctx context.Context, token string) error {
	// TODO: Implement logout
	return nil
}

func (s *authService) RefreshToken(ctx context.Context, token string) (*domain.LoginResponse, error) {
	// TODO: Implement refresh token
	return nil, nil
}

func (s *authService) ForgotPassword(ctx context.Context, req *domain.ForgotPasswordRequest, ipAddress, userAgent string) error {
	// TODO: Implement forgot password
	return nil
}

func (s *authService) ResetPassword(ctx context.Context, req *domain.ResetPasswordRequest, ipAddress, userAgent string) error {
	// TODO: Implement reset password
	return nil
}

func (s *authService) ChangePassword(ctx context.Context, userID uuid.UUID, req *domain.ChangePasswordRequest, actorID string) error {
	// TODO: Implement change password
	return nil
}

func (s *authService) VerifyEmail(ctx context.Context, req *domain.VerifyEmailRequest, ipAddress, userAgent string) error {
	// TODO: Implement email verification
	return nil
}

func (s *authService) ResendVerification(ctx context.Context, req *domain.ResendVerificationRequest, ipAddress, userAgent string) error {
	// TODO: Implement resend verification
	return nil
}

func (s *authService) SetupTwoFactor(ctx context.Context, userID uuid.UUID) (*domain.SetupTwoFactorResponse, error) {
	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("user not found: %w", err)
	}

	if user.TwoFactorEnabled {
		return nil, fmt.Errorf("two-factor authentication is already enabled")
	}

	// Generate TOTP secret
	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      s.jwtIssuer,
		AccountName: user.Email,
		SecretSize:  32,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to generate TOTP secret: %w", err)
	}

	// Generate backup codes
	backupCodes := make([]string, 10)
	for i := 0; i < 10; i++ {
		code, err := s.generateBackupCode()
		if err != nil {
			return nil, fmt.Errorf("failed to generate backup code: %w", err)
		}
		backupCodes[i] = code
	}

	// Create QR code URL
	qrCodeURL := key.URL()

	return &domain.SetupTwoFactorResponse{
		Secret:      key.Secret(),
		QRCodeURL:   qrCodeURL,
		BackupCodes: backupCodes,
	}, nil
}

func (s *authService) EnableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.EnableTwoFactorRequest, actorID string) error {
	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return fmt.Errorf("user not found: %w", err)
	}

	if user.TwoFactorEnabled {
		return fmt.Errorf("two-factor authentication is already enabled")
	}

	if user.TwoFactorSecret == nil {
		return fmt.Errorf("two-factor authentication setup required first")
	}

	// Validate the provided code
	valid, err := s.validateTOTPCode(req.Code, *user.TwoFactorSecret)
	if err != nil || !valid {
		return fmt.Errorf("invalid verification code")
	}

	// Generate backup codes
	backupCodes := make([]string, 10)
	for i := 0; i < 10; i++ {
		code, err := s.generateBackupCode()
		if err != nil {
			return fmt.Errorf("failed to generate backup code: %w", err)
		}
		backupCodes[i] = code
	}

	// Enable 2FA in database
	err = s.userRepo.UpdateTwoFactorSettings(ctx, userID, true, user.TwoFactorSecret, backupCodes)
	if err != nil {
		return fmt.Errorf("failed to enable two-factor authentication: %w", err)
	}

	// Log audit event
	if s.auditRepo != nil {
		auditEvent := &interfaces.AuditEvent{
			ID:           uuid.New(),
			Type:         "auth.2fa_enabled",
			Severity:     "info",
			Status:       "success",
			Timestamp:    time.Now().Format(time.RFC3339),
			ActorID:      actorID,
			ActorType:    "user",
			ResourceID:   userID.String(),
			ResourceType: "user",
			Action:       "enable_2fa",
			Description:  fmt.Sprintf("Two-factor authentication enabled for user %s", user.Email),
			Details: map[string]interface{}{
				"user_id": userID.String(),
				"email":   user.Email,
			},
		}
		_ = s.auditRepo.LogEvent(ctx, auditEvent)
	}

	s.logger.Info("Two-factor authentication enabled", 
		zap.String("user_id", userID.String()),
		zap.String("email", user.Email),
		zap.String("actor", actorID))

	return nil
}

func (s *authService) DisableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.DisableTwoFactorRequest, actorID string) error {
	// TODO: Implement disable 2FA
	return nil
}

func (s *authService) ValidateTwoFactor(ctx context.Context, req *domain.ValidateTwoFactorRequest, ipAddress, userAgent string) (*domain.LoginResponse, error) {
	// TODO: Implement validate 2FA
	return nil, nil
}

func (s *authService) RegenerateTwoFactorBackupCodes(ctx context.Context, userID uuid.UUID, actorID string) ([]string, error) {
	// TODO: Implement regenerate backup codes
	return nil, nil
}

func (s *authService) GetUserSessions(ctx context.Context, userID uuid.UUID) (*domain.ListSessionsResponse, error) {
	// TODO: Implement get user sessions
	return nil, nil
}

func (s *authService) RevokeSession(ctx context.Context, userID uuid.UUID, sessionID uuid.UUID, actorID string) error {
	// TODO: Implement revoke session
	return nil
}

func (s *authService) RevokeAllSessions(ctx context.Context, userID uuid.UUID, actorID string) error {
	// TODO: Implement revoke all sessions
	return nil
}

func (s *authService) ValidateToken(ctx context.Context, tokenString string) (*domain.AuthContext, error) {
	// TODO: Implement token validation
	return nil, nil
}

func (s *authService) IsAccountLocked(ctx context.Context, email string) (bool, *time.Time, error) {
	user, err := s.userRepo.GetByEmail(ctx, email)
	if err != nil {
		return false, nil, nil // User doesn't exist, not locked
	}

	// Check if account is currently locked
	if user.LockedUntil != nil && time.Now().Before(*user.LockedUntil) {
		return true, user.LockedUntil, nil
	}

	// Clear expired lock
	if user.LockedUntil != nil && time.Now().After(*user.LockedUntil) {
		_ = s.userRepo.ResetFailedAttempts(ctx, user.ID)
	}

	return false, nil, nil
}

func (s *authService) RecordLoginAttempt(ctx context.Context, email, ipAddress, userAgent, failureReason string, success bool) error {
	attempt := &domain.LoginAttempt{
		ID:            uuid.New(),
		Email:         email,
		IPAddress:     ipAddress,
		UserAgent:     &userAgent,
		Success:       success,
		FailureReason: &failureReason,
		AttemptedAt:   time.Now(),
	}

	if failureReason == "" {
		attempt.FailureReason = nil
	}

	// In a full implementation, you would use a LoginAttemptRepository
	// For now, just log the audit event
	if s.auditRepo != nil {
		auditEvent := &interfaces.AuditEvent{
			ID:           uuid.New(),
			Type:         "auth.login_attempt",
			Severity:     "info",
			Status:       "success",
			Timestamp:    time.Now().Format(time.RFC3339),
			ActorID:      email,
			ActorType:    "user",
			ActorIP:      ipAddress,
			UserAgent:    userAgent,
			Action:       "login_attempt",
			Description:  fmt.Sprintf("Login attempt for %s: success=%v", email, success),
			Details: map[string]interface{}{
				"email":          email,
				"success":        success,
				"failure_reason": failureReason,
			},
		}
		_ = s.auditRepo.LogEvent(ctx, auditEvent)
	}

	return nil
}