package service

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"math/big"
	"net/url"
	"strings"
	"time"

	"git.threesix.ai/jordan/persona-community-5/pkg/auth"
	"git.threesix.ai/jordan/persona-community-5/pkg/logging"
	"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
	"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
)

const (
	// TokenLifetime is the access token duration (short-lived, requires refresh).
	TokenLifetime = 15 * time.Minute
	// SessionLifetime is how long a session stays valid before requiring re-login.
	SessionLifetime = 30 * 24 * time.Hour // 30 days
	// OTPExpiry is how long a one-time password is valid.
	OTPExpiry = 10 * time.Minute
	// MagicLinkExpiry is how long a magic link token is valid.
	MagicLinkExpiry = 15 * time.Minute
	// PasswordResetExpiry is how long a password reset token is valid.
	PasswordResetExpiry = 1 * time.Hour
	// EmailVerifyExpiry is how long an email verification code is valid.
	EmailVerifyExpiry = 24 * time.Hour
)

// AuthService handles all authentication and identity flows.
type AuthService struct {
	users               port.UserRepository
	sessions            port.SessionRepository
	codes               port.AuthCodeRepository
	email               port.EmailSender
	jwtSecret           []byte
	issuer              string
	registrationEnabled bool
	logger              *logging.Logger
}

// NewAuthService creates a new auth service.
func NewAuthService(
	users port.UserRepository,
	sessions port.SessionRepository,
	codes port.AuthCodeRepository,
	email port.EmailSender,
	jwtSecret string,
	registrationEnabled bool,
	logger *logging.Logger,
) *AuthService {
	return &AuthService{
		users:               users,
		sessions:            sessions,
		codes:               codes,
		email:               email,
		jwtSecret:           []byte(jwtSecret),
		issuer:              "persona-community-5",
		registrationEnabled: registrationEnabled,
		logger:              logger.WithService("AuthService"),
	}
}

// LoginOutput is the result of a successful login or registration.
type LoginOutput struct {
	Token string
	User  *domain.User
}

// Register creates a new user account with email and password.
func (s *AuthService) Register(ctx context.Context, email, password, name, ip, userAgent string) (*LoginOutput, error) {
	if !s.registrationEnabled {
		return nil, domain.ErrRegistrationDisabled
	}

	if err := auth.ValidatePasswordStrength(password); err != nil {
		return nil, fmt.Errorf("%w: %w", domain.ErrWeakPassword, err)
	}

	name = strings.TrimSpace(name)
	if len(name) > domain.MaxNameLen {
		return nil, domain.ErrNameTooLong
	}
	if len(email) > domain.MaxEmailLen {
		return nil, domain.ErrEmailTooLong
	}

	exists, err := s.users.ExistsByEmail(ctx, email)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, domain.ErrDuplicateEmail
	}

	hash, err := auth.HashPassword(password)
	if err != nil {
		return nil, fmt.Errorf("hashing password: %w", err)
	}

	userID := domain.UserID("usr_" + generateID())
	user := domain.NewUser(userID, email, name)

	if err := s.users.Create(ctx, user); err != nil {
		return nil, err
	}
	if err := s.users.SetPassword(ctx, userID, hash); err != nil {
		return nil, err
	}

	s.logger.Info("user registered", "user_id", string(userID), "email", email)

	return s.createSession(ctx, user, ip, userAgent)
}

// LoginWithPassword authenticates a user with email and password.
func (s *AuthService) LoginWithPassword(ctx context.Context, email, password, ip, userAgent string) (*LoginOutput, error) {
	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, domain.ErrUserNotFound) {
			return nil, domain.ErrInvalidCredentials
		}
		return nil, err
	}

	if user.Status == domain.UserStatusSuspended {
		return nil, domain.ErrUserSuspended
	}

	hash, err := s.users.GetPasswordHash(ctx, user.ID)
	if err != nil {
		return nil, err
	}
	if hash == "" || !auth.CheckPassword(password, hash) {
		s.logger.Warn("invalid password attempt", "email", email)
		return nil, domain.ErrInvalidCredentials
	}

	_ = s.users.UpdateLastLogin(ctx, user.ID)
	s.logger.Info("user logged in", "user_id", string(user.ID), "email", email)

	return s.createSession(ctx, user, ip, userAgent)
}

// RefreshToken issues a new access token if the session is still active.
func (s *AuthService) RefreshToken(ctx context.Context, sessionID string, userID string) (*LoginOutput, error) {
	sid := domain.SessionID(sessionID)
	session, err := s.sessions.Get(ctx, sid)
	if err != nil {
		return nil, domain.ErrSessionNotFound
	}
	if !session.IsActive() {
		return nil, domain.ErrSessionRevoked
	}

	user, err := s.users.Get(ctx, domain.UserID(userID))
	if err != nil {
		return nil, err
	}
	if user.Status == domain.UserStatusSuspended {
		return nil, domain.ErrUserSuspended
	}

	_ = s.sessions.UpdateLastActive(ctx, sid)

	token, err := s.generateToken(user, sessionID)
	if err != nil {
		return nil, err
	}

	return &LoginOutput{Token: token, User: user}, nil
}

// Logout revokes the current session.
func (s *AuthService) Logout(ctx context.Context, sessionID string) error {
	if sessionID == "" {
		return nil
	}
	return s.sessions.Revoke(ctx, domain.SessionID(sessionID))
}

// LogoutAll revokes all sessions for a user, optionally keeping one.
func (s *AuthService) LogoutAll(ctx context.Context, userID string, exceptSessionID *string) error {
	var except *domain.SessionID
	if exceptSessionID != nil {
		sid := domain.SessionID(*exceptSessionID)
		except = &sid
	}
	return s.sessions.RevokeAllForUser(ctx, domain.UserID(userID), except)
}

// CheckSession returns whether a session is active (not revoked, not expired).
// Used as auth.SessionChecker for the SessionCheck middleware.
func (s *AuthService) CheckSession(ctx context.Context, sessionID string) (bool, error) {
	session, err := s.sessions.Get(ctx, domain.SessionID(sessionID))
	if err != nil {
		return false, nil
	}
	return session.IsActive(), nil
}

// ListSessions returns all active sessions for a user.
func (s *AuthService) ListSessions(ctx context.Context, userID string) ([]domain.Session, error) {
	return s.sessions.ListByUser(ctx, domain.UserID(userID))
}

// RevokeSession revokes a specific session for a user.
func (s *AuthService) RevokeSession(ctx context.Context, userID, sessionID string) error {
	session, err := s.sessions.Get(ctx, domain.SessionID(sessionID))
	if err != nil {
		return err
	}
	if session.UserID != domain.UserID(userID) {
		return domain.ErrSessionNotFound
	}
	return s.sessions.Revoke(ctx, domain.SessionID(sessionID))
}

// GetCurrentUser returns the full user for the given ID.
func (s *AuthService) GetCurrentUser(ctx context.Context, userID string) (*domain.User, error) {
	return s.users.Get(ctx, domain.UserID(userID))
}

// UpdateProfile updates a user's name and avatar.
func (s *AuthService) UpdateProfile(ctx context.Context, userID, name, avatarURL string) (*domain.User, error) {
	user, err := s.users.Get(ctx, domain.UserID(userID))
	if err != nil {
		return nil, err
	}

	if name != "" {
		name = strings.TrimSpace(name)
		if len(name) > domain.MaxNameLen {
			return nil, domain.ErrNameTooLong
		}
		user.Name = name
	}
	if avatarURL != "" {
		if err := validateAvatarURL(avatarURL); err != nil {
			return nil, err
		}
		user.AvatarURL = avatarURL
	}

	if err := s.users.Update(ctx, user); err != nil {
		return nil, err
	}
	return user, nil
}

// ChangePassword changes a user's password after verifying the current one.
func (s *AuthService) ChangePassword(ctx context.Context, userID, currentPassword, newPassword string) error {
	uid := domain.UserID(userID)

	hash, err := s.users.GetPasswordHash(ctx, uid)
	if err != nil {
		return err
	}
	if hash == "" || !auth.CheckPassword(currentPassword, hash) {
		return domain.ErrInvalidCredentials
	}

	if err := auth.ValidatePasswordStrength(newPassword); err != nil {
		return fmt.Errorf("%w: %w", domain.ErrWeakPassword, err)
	}

	newHash, err := auth.HashPassword(newPassword)
	if err != nil {
		return fmt.Errorf("hashing password: %w", err)
	}

	return s.users.SetPassword(ctx, uid, newHash)
}

// SendOTP generates a one-time password for the given email.
// If the email is not registered and registration is enabled, the code is still
// sent — the account will be created when the code is verified. This supports a
// unified register+login flow with a single OTP email.
func (s *AuthService) SendOTP(ctx context.Context, email, ip string) error {
	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		if !errors.Is(err, domain.ErrUserNotFound) {
			return err
		}
		// Unknown email: only proceed if registration is open.
		if !s.registrationEnabled {
			s.logger.Info("OTP requested for unknown email (registration disabled)", "email", email)
			return nil
		}
		// Registration enabled — send code anyway. UserID will be nil until verify.
		user = nil
	}

	code := generateOTP()
	var uid *domain.UserID
	if user != nil {
		uid = &user.ID
	}
	authCode := &domain.AuthCode{
		ID:        "acd_" + generateID(),
		UserID:    uid,
		Email:     email,
		Code:      code,
		Purpose:   domain.PurposeLoginOTP,
		ExpiresAt: time.Now().Add(OTPExpiry),
		IPAddress: ip,
		CreatedAt: time.Now(),
	}

	if err := s.codes.Create(ctx, authCode); err != nil {
		return err
	}

	s.logger.Info("auth code created", "purpose", "login_otp", "email", email, "code_id", authCode.ID)
	if err := s.email.SendAuthCode(ctx, email, code, string(domain.PurposeLoginOTP)); err != nil {
		s.logger.Error("failed to send OTP email", "email", email, "error", err)
	}
	return nil
}

// VerifyOTP verifies a one-time password and returns a login token.
// If the email has no account yet and registration is enabled, the account is
// created automatically — OTP delivery proves email ownership.
func (s *AuthService) VerifyOTP(ctx context.Context, email, code, ip, userAgent string) (*LoginOutput, error) {
	authCode, err := s.codes.FindValid(ctx, email, code, domain.PurposeLoginOTP)
	if err != nil {
		return nil, domain.ErrInvalidAuthCode
	}

	if err := s.codes.MarkUsed(ctx, authCode.ID); err != nil {
		return nil, err
	}

	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		if !errors.Is(err, domain.ErrUserNotFound) {
			return nil, err
		}
		if !s.registrationEnabled {
			return nil, domain.ErrRegistrationDisabled
		}
		// Auto-register: OTP delivery already proved email ownership.
		user, err = s.autoRegisterViaOTP(ctx, email)
		if err != nil {
			return nil, err
		}
	}

	_ = s.users.UpdateLastLogin(ctx, user.ID)
	s.logger.Info("user logged in via OTP", "user_id", string(user.ID), "email", email)

	return s.createSession(ctx, user, ip, userAgent)
}

// autoRegisterViaOTP creates a minimal user account for an email that just
// verified an OTP. Email is considered verified because OTP delivery proved
// ownership. The name defaults to the local part of the email address.
func (s *AuthService) autoRegisterViaOTP(ctx context.Context, email string) (*domain.User, error) {
	name := email
	if at := strings.IndexByte(email, '@'); at > 0 {
		name = email[:at]
	}
	userID := domain.UserID("usr_" + generateID())
	user := domain.NewUser(userID, email, name)
	user.EmailVerified = true // OTP delivery proves ownership

	if err := s.users.Create(ctx, user); err != nil {
		return nil, err
	}
	s.logger.Info("user auto-registered via OTP", "user_id", string(userID), "email", email)
	return user, nil
}

// SendMagicLink generates and logs a magic link token.
func (s *AuthService) SendMagicLink(ctx context.Context, email, ip string) error {
	// Magic links can work for existing users.
	// Don't reveal whether email exists — but propagate infrastructure errors.
	user, err := s.users.GetByEmail(ctx, email)
	if err != nil && !errors.Is(err, domain.ErrUserNotFound) {
		return err
	}

	token := generateHexToken()
	var uid *domain.UserID
	if user != nil {
		uid = &user.ID
	}

	authCode := &domain.AuthCode{
		ID:        "acd_" + generateID(),
		UserID:    uid,
		Email:     email,
		Code:      token,
		Purpose:   domain.PurposeMagicLink,
		ExpiresAt: time.Now().Add(MagicLinkExpiry),
		IPAddress: ip,
		CreatedAt: time.Now(),
	}

	if err := s.codes.Create(ctx, authCode); err != nil {
		return err
	}

	s.logger.Info("auth code created", "purpose", "magic_link", "email", email, "code_id", authCode.ID)
	if err := s.email.SendAuthCode(ctx, email, token, string(domain.PurposeMagicLink)); err != nil {
		s.logger.Error("failed to send magic link email", "email", email, "error", err)
	}
	return nil
}

// VerifyMagicLink verifies a magic link token and returns a login token.
func (s *AuthService) VerifyMagicLink(ctx context.Context, email, token, ip, userAgent string) (*LoginOutput, error) {
	authCode, err := s.codes.FindValid(ctx, email, token, domain.PurposeMagicLink)
	if err != nil {
		return nil, domain.ErrInvalidAuthCode
	}

	if err := s.codes.MarkUsed(ctx, authCode.ID); err != nil {
		return nil, err
	}

	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		return nil, err
	}

	_ = s.users.UpdateLastLogin(ctx, user.ID)
	s.logger.Info("user logged in via magic link", "user_id", string(user.ID), "email", email)

	return s.createSession(ctx, user, ip, userAgent)
}

// ForgotPassword generates a password reset token.
func (s *AuthService) ForgotPassword(ctx context.Context, email, ip string) error {
	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, domain.ErrUserNotFound) {
			// Don't reveal whether email exists
			s.logger.Info("password reset requested for unknown email", "email", email)
			return nil
		}
		return err
	}

	token := generateHexToken()
	uid := user.ID
	authCode := &domain.AuthCode{
		ID:        "acd_" + generateID(),
		UserID:    &uid,
		Email:     email,
		Code:      token,
		Purpose:   domain.PurposePasswordReset,
		ExpiresAt: time.Now().Add(PasswordResetExpiry),
		IPAddress: ip,
		CreatedAt: time.Now(),
	}

	if err := s.codes.Create(ctx, authCode); err != nil {
		return err
	}

	s.logger.Info("auth code created", "purpose", "password_reset", "email", email, "code_id", authCode.ID)
	if err := s.email.SendAuthCode(ctx, email, token, string(domain.PurposePasswordReset)); err != nil {
		s.logger.Error("failed to send password reset email", "email", email, "error", err)
	}
	return nil
}

// ResetPassword sets a new password using a reset token and revokes all sessions.
func (s *AuthService) ResetPassword(ctx context.Context, email, token, newPassword string) error {
	authCode, err := s.codes.FindValid(ctx, email, token, domain.PurposePasswordReset)
	if err != nil {
		return domain.ErrInvalidAuthCode
	}

	if err := auth.ValidatePasswordStrength(newPassword); err != nil {
		return fmt.Errorf("%w: %w", domain.ErrWeakPassword, err)
	}

	user, err := s.users.GetByEmail(ctx, email)
	if err != nil {
		return err
	}

	hash, err := auth.HashPassword(newPassword)
	if err != nil {
		return fmt.Errorf("hashing password: %w", err)
	}

	if err := s.users.SetPassword(ctx, user.ID, hash); err != nil {
		return err
	}
	if err := s.codes.MarkUsed(ctx, authCode.ID); err != nil {
		return err
	}

	// Revoke all sessions — user must re-login with new password.
	_ = s.sessions.RevokeAllForUser(ctx, user.ID, nil)
	s.logger.Info("password reset completed", "user_id", string(user.ID), "email", email)

	return nil
}

// SendVerifyEmail generates an email verification code.
func (s *AuthService) SendVerifyEmail(ctx context.Context, userID string) error {
	user, err := s.users.Get(ctx, domain.UserID(userID))
	if err != nil {
		return err
	}
	if user.EmailVerified {
		return nil
	}

	code := generateOTP()
	uid := user.ID
	authCode := &domain.AuthCode{
		ID:        "acd_" + generateID(),
		UserID:    &uid,
		Email:     user.Email,
		Code:      code,
		Purpose:   domain.PurposeEmailVerify,
		ExpiresAt: time.Now().Add(EmailVerifyExpiry),
		CreatedAt: time.Now(),
	}

	if err := s.codes.Create(ctx, authCode); err != nil {
		return err
	}

	s.logger.Info("auth code created", "purpose", "email_verify", "email", user.Email, "code_id", authCode.ID)
	if err := s.email.SendAuthCode(ctx, user.Email, code, string(domain.PurposeEmailVerify)); err != nil {
		s.logger.Error("failed to send email verification", "email", user.Email, "error", err)
	}
	return nil
}

// VerifyEmail marks the user's email as verified.
func (s *AuthService) VerifyEmail(ctx context.Context, userID, code string) error {
	user, err := s.users.Get(ctx, domain.UserID(userID))
	if err != nil {
		return err
	}

	authCode, err := s.codes.FindValid(ctx, user.Email, code, domain.PurposeEmailVerify)
	if err != nil {
		return domain.ErrInvalidAuthCode
	}

	if err := s.codes.MarkUsed(ctx, authCode.ID); err != nil {
		return err
	}

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

	s.logger.Info("email verified", "user_id", userID, "email", user.Email)
	return nil
}

// createSession creates a session record and generates a JWT.
func (s *AuthService) createSession(ctx context.Context, user *domain.User, ip, userAgent string) (*LoginOutput, error) {
	sessionID := "ses_" + generateID()
	now := time.Now()

	session := &domain.Session{
		ID:           domain.SessionID(sessionID),
		UserID:       user.ID,
		IPAddress:    ip,
		UserAgent:    userAgent,
		DeviceLabel:  auth.ParseDeviceLabel(userAgent),
		LastActiveAt: now,
		ExpiresAt:    now.Add(SessionLifetime),
		CreatedAt:    now,
	}

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

	token, err := s.generateToken(user, sessionID)
	if err != nil {
		return nil, err
	}

	return &LoginOutput{Token: token, User: user}, nil
}

// generateToken creates a JWT for the user with the given session ID.
func (s *AuthService) generateToken(user *domain.User, sessionID string) (string, error) {
	authUser := &auth.User{
		ID:    string(user.ID),
		Email: user.Email,
		Roles: user.Roles,
	}
	return auth.GenerateTokenWithSession(
		s.jwtSecret, authUser, TokenLifetime, s.issuer, s.issuer, sessionID,
	)
}

// generateID returns a random hex string suitable for entity IDs.
func generateID() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic("crypto/rand failed: " + err.Error())
	}
	return hex.EncodeToString(b)
}

// generateOTP returns a 6-digit numeric one-time password.
func generateOTP() string {
	n, err := rand.Int(rand.Reader, big.NewInt(1000000))
	if err != nil {
		panic("crypto/rand failed: " + err.Error())
	}
	return fmt.Sprintf("%06d", n.Int64())
}

// validateAvatarURL checks that the URL uses http or https.
func validateAvatarURL(rawURL string) error {
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return domain.ErrInvalidAvatarURL
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return domain.ErrInvalidAvatarURL
	}
	return nil
}

// generateHexToken returns a 32-character hex token for magic links and resets.
func generateHexToken() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic("crypto/rand failed: " + err.Error())
	}
	return hex.EncodeToString(b)
}
