package services

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/crypto/bcrypt"

	"github.com/breakpilot/consent-service/internal/models"
)

var (
	ErrInvalidCredentials = errors.New("invalid email or password")
	ErrUserNotFound       = errors.New("user not found")
	ErrUserExists         = errors.New("user with this email already exists")
	ErrInvalidToken       = errors.New("invalid or expired token")
	ErrAccountLocked      = errors.New("account is temporarily locked")
	ErrAccountSuspended   = errors.New("account is suspended")
	ErrEmailNotVerified   = errors.New("email not verified")
)

// AuthService handles authentication logic
type AuthService struct {
	db               *pgxpool.Pool
	jwtSecret        string
	jwtRefreshSecret string
	accessTokenExp   time.Duration
	refreshTokenExp  time.Duration
}

// NewAuthService creates a new AuthService
func NewAuthService(db *pgxpool.Pool, jwtSecret, jwtRefreshSecret string) *AuthService {
	return &AuthService{
		db:               db,
		jwtSecret:        jwtSecret,
		jwtRefreshSecret: jwtRefreshSecret,
		accessTokenExp:   time.Hour * 1,       // 1 hour
		refreshTokenExp:  time.Hour * 24 * 30, // 30 days
	}
}

// HashPassword hashes a password using bcrypt
func (s *AuthService) HashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("failed to hash password: %w", err)
	}
	return string(bytes), nil
}

// VerifyPassword verifies a password against a hash
func (s *AuthService) VerifyPassword(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

// GenerateSecureToken generates a cryptographically secure token
func (s *AuthService) GenerateSecureToken(length int) (string, error) {
	bytes := make([]byte, length)
	if _, err := rand.Read(bytes); err != nil {
		return "", fmt.Errorf("failed to generate token: %w", err)
	}
	return base64.URLEncoding.EncodeToString(bytes), nil
}

// HashToken creates a SHA256 hash of a token for storage
func (s *AuthService) HashToken(token string) string {
	hash := sha256.Sum256([]byte(token))
	return hex.EncodeToString(hash[:])
}

// JWTClaims for access tokens
type JWTClaims struct {
	UserID        string `json:"user_id"`
	Email         string `json:"email"`
	Role          string `json:"role"`
	AccountStatus string `json:"account_status"`
	jwt.RegisteredClaims
}

// GenerateAccessToken creates a new JWT access token
func (s *AuthService) GenerateAccessToken(user *models.User) (string, error) {
	claims := JWTClaims{
		UserID:        user.ID.String(),
		Email:         user.Email,
		Role:          user.Role,
		AccountStatus: user.AccountStatus,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessTokenExp)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Subject:   user.ID.String(),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(s.jwtSecret))
}

// GenerateRefreshToken creates a new refresh token
func (s *AuthService) GenerateRefreshToken() (string, string, error) {
	token, err := s.GenerateSecureToken(32)
	if err != nil {
		return "", "", err
	}
	hash := s.HashToken(token)
	return token, hash, nil
}

// ValidateAccessToken validates a JWT access token
func (s *AuthService) ValidateAccessToken(tokenString string) (*JWTClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(s.jwtSecret), nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to parse token: %w", err)
	}

	claims, ok := token.Claims.(*JWTClaims)
	if !ok || !token.Valid {
		return nil, ErrInvalidToken
	}

	return claims, nil
}

// Register creates a new user account
func (s *AuthService) Register(ctx context.Context, req *models.RegisterRequest) (*models.User, string, error) {
	// Check if user already exists
	var exists bool
	err := s.db.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", req.Email).Scan(&exists)
	if err != nil {
		return nil, "", fmt.Errorf("failed to check existing user: %w", err)
	}
	if exists {
		return nil, "", ErrUserExists
	}

	// Hash password
	passwordHash, err := s.HashPassword(req.Password)
	if err != nil {
		return nil, "", err
	}

	// Create user
	user := &models.User{
		ID:            uuid.New(),
		Email:         req.Email,
		PasswordHash:  &passwordHash,
		Name:          req.Name,
		Role:          "user",
		EmailVerified: false,
		AccountStatus: "active",
	}

	_, err = s.db.Exec(ctx, `
		INSERT INTO users (id, email, password_hash, name, role, email_verified, account_status, created_at, updated_at)
		VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
	`, user.ID, user.Email, user.PasswordHash, user.Name, user.Role, user.EmailVerified, user.AccountStatus)

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

	// Generate email verification token
	verificationToken, err := s.GenerateSecureToken(32)
	if err != nil {
		return nil, "", err
	}

	// Store verification token
	_, err = s.db.Exec(ctx, `
		INSERT INTO email_verification_tokens (user_id, token, expires_at, created_at)
		VALUES ($1, $2, $3, NOW())
	`, user.ID, verificationToken, time.Now().Add(24*time.Hour))

	if err != nil {
		return nil, "", fmt.Errorf("failed to create verification token: %w", err)
	}

	// Create notification preferences
	_, err = s.db.Exec(ctx, `
		INSERT INTO notification_preferences (user_id, email_enabled, push_enabled, in_app_enabled, reminder_frequency, created_at, updated_at)
		VALUES ($1, true, true, true, 'weekly', NOW(), NOW())
	`, user.ID)

	if err != nil {
		// Non-critical error, just log
		fmt.Printf("Warning: failed to create notification preferences: %v\n", err)
	}

	return user, verificationToken, nil
}

// Login authenticates a user and returns tokens
func (s *AuthService) Login(ctx context.Context, req *models.LoginRequest, ipAddress, userAgent string) (*models.LoginResponse, error) {
	var user models.User
	var passwordHash *string

	err := s.db.QueryRow(ctx, `
		SELECT id, email, password_hash, name, role, email_verified, account_status,
		       failed_login_attempts, locked_until, created_at, updated_at
		FROM users WHERE email = $1
	`, req.Email).Scan(
		&user.ID, &user.Email, &passwordHash, &user.Name, &user.Role, &user.EmailVerified,
		&user.AccountStatus, &user.FailedLoginAttempts, &user.LockedUntil, &user.CreatedAt, &user.UpdatedAt,
	)

	if err != nil {
		return nil, ErrInvalidCredentials
	}

	// Check if account is locked
	if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
		return nil, ErrAccountLocked
	}

	// Check if account is suspended
	if user.AccountStatus == "suspended" {
		return nil, ErrAccountSuspended
	}

	// Verify password
	if passwordHash == nil || !s.VerifyPassword(req.Password, *passwordHash) {
		// Increment failed login attempts
		_, _ = s.db.Exec(ctx, `
			UPDATE users SET
				failed_login_attempts = failed_login_attempts + 1,
				locked_until = CASE WHEN failed_login_attempts >= 4 THEN NOW() + INTERVAL '30 minutes' ELSE locked_until END,
				updated_at = NOW()
			WHERE id = $1
		`, user.ID)
		return nil, ErrInvalidCredentials
	}

	// Reset failed login attempts and update last login
	_, _ = s.db.Exec(ctx, `
		UPDATE users SET
			failed_login_attempts = 0,
			locked_until = NULL,
			last_login_at = NOW(),
			updated_at = NOW()
		WHERE id = $1
	`, user.ID)

	// Generate tokens
	accessToken, err := s.GenerateAccessToken(&user)
	if err != nil {
		return nil, fmt.Errorf("failed to generate access token: %w", err)
	}

	refreshToken, refreshTokenHash, err := s.GenerateRefreshToken()
	if err != nil {
		return nil, fmt.Errorf("failed to generate refresh token: %w", err)
	}

	// Store session
	_, err = s.db.Exec(ctx, `
		INSERT INTO user_sessions (user_id, token_hash, ip_address, user_agent, expires_at, created_at, last_activity_at)
		VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
	`, user.ID, refreshTokenHash, ipAddress, userAgent, time.Now().Add(s.refreshTokenExp))

	if err != nil {
		return nil, fmt.Errorf("failed to create session: %w", err)
	}

	return &models.LoginResponse{
		User:         user,
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(s.accessTokenExp.Seconds()),
	}, nil
}

// RefreshToken refreshes the access token using a refresh token
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*models.LoginResponse, error) {
	tokenHash := s.HashToken(refreshToken)

	var session models.UserSession
	var userID uuid.UUID

	err := s.db.QueryRow(ctx, `
		SELECT id, user_id, expires_at, revoked_at FROM user_sessions
		WHERE token_hash = $1
	`, tokenHash).Scan(&session.ID, &userID, &session.ExpiresAt, &session.RevokedAt)

	if err != nil {
		return nil, ErrInvalidToken
	}

	// Check if session is expired or revoked
	if session.RevokedAt != nil || session.ExpiresAt.Before(time.Now()) {
		return nil, ErrInvalidToken
	}

	// Get user
	var user models.User
	err = s.db.QueryRow(ctx, `
		SELECT id, email, name, role, email_verified, account_status, created_at, updated_at
		FROM users WHERE id = $1
	`, userID).Scan(
		&user.ID, &user.Email, &user.Name, &user.Role, &user.EmailVerified,
		&user.AccountStatus, &user.CreatedAt, &user.UpdatedAt,
	)

	if err != nil {
		return nil, ErrUserNotFound
	}

	// Check account status
	if user.AccountStatus == "suspended" {
		return nil, ErrAccountSuspended
	}

	// Generate new access token
	accessToken, err := s.GenerateAccessToken(&user)
	if err != nil {
		return nil, fmt.Errorf("failed to generate access token: %w", err)
	}

	// Update session last activity
	_, _ = s.db.Exec(ctx, `
		UPDATE user_sessions SET last_activity_at = NOW() WHERE id = $1
	`, session.ID)

	return &models.LoginResponse{
		User:         user,
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(s.accessTokenExp.Seconds()),
	}, nil
}

// VerifyEmail verifies a user's email address
func (s *AuthService) VerifyEmail(ctx context.Context, token string) error {
	var tokenID uuid.UUID
	var userID uuid.UUID
	var expiresAt time.Time
	var usedAt *time.Time

	err := s.db.QueryRow(ctx, `
		SELECT id, user_id, expires_at, used_at FROM email_verification_tokens
		WHERE token = $1
	`, token).Scan(&tokenID, &userID, &expiresAt, &usedAt)

	if err != nil {
		return ErrInvalidToken
	}

	if usedAt != nil || expiresAt.Before(time.Now()) {
		return ErrInvalidToken
	}

	// Mark token as used
	_, err = s.db.Exec(ctx, `UPDATE email_verification_tokens SET used_at = NOW() WHERE id = $1`, tokenID)
	if err != nil {
		return fmt.Errorf("failed to update token: %w", err)
	}

	// Verify user email
	_, err = s.db.Exec(ctx, `
		UPDATE users SET email_verified = true, email_verified_at = NOW(), updated_at = NOW()
		WHERE id = $1
	`, userID)

	if err != nil {
		return fmt.Errorf("failed to verify email: %w", err)
	}

	return nil
}

