package auth

import (
	"context"
	"crypto/rand"
	"fmt"
	"log/slog"
	"net/http"
	"regexp"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/sms"
)

const (
	mobileVerificationCodeLength = 6
	mobileVerificationCodeTTL    = 15 * time.Minute
	mobileVerificationHistoryMax = 5
)

// MobileHandler owns mobile verification routes.
type MobileHandler struct {
	logger                           *slog.Logger
	store                            Store
	mobileVerificationSenderProvider func() sms.Sender
	passwordAuthDisabled             bool
}

var twilioVerificationSendErrorPattern = regexp.MustCompile(`^send twilio verification SMS: status \d+(?: code \d+)?: (.+?)(?: \(https?://[^)]+\))?$`)

func newMobileVerificationCode(length int) (string, error) {
	if length <= 0 {
		return "", fmt.Errorf("mobile verification code length must be positive")
	}
	buffer := make([]byte, length)
	if _, err := rand.Read(buffer); err != nil {
		return "", err
	}
	for index := range buffer {
		buffer[index] = '0' + (buffer[index] % 10)
	}
	return string(buffer), nil
}

func mobileVerificationProvider(receipt sms.Receipt) string {
	return mobileVerificationProviderName(receipt.Provider)
}

func mobileVerificationProviderName(provider string) string {
	provider = strings.TrimSpace(provider)
	if provider == "" {
		return "unknown"
	}
	return provider
}

func sanitizeMobileVerificationFailureDetail(message string) string {
	trimmedMessage := strings.TrimSpace(message)
	if trimmedMessage == "" {
		return ""
	}

	if matches := twilioVerificationSendErrorPattern.FindStringSubmatch(trimmedMessage); len(matches) == 2 {
		return strings.TrimSpace(matches[1])
	}

	lowercaseMessage := strings.ToLower(trimmedMessage)
	switch {
	case strings.Contains(lowercaseMessage, "context deadline exceeded"),
		strings.Contains(lowercaseMessage, "client.timeout exceeded"):
		return "provider timeout"
	case strings.Contains(lowercaseMessage, "dial tcp"),
		strings.Contains(lowercaseMessage, "connection refused"),
		strings.Contains(lowercaseMessage, "no such host"):
		return "provider network error"
	default:
		return ""
	}
}

func mobileVerificationLogAttrs(attemptID string, userID string, mobile string, expiresAt time.Time, receipt sms.Receipt) []any {
	attrs := []any{
		slog.String("attempt_id", attemptID),
		slog.String("user_id", userID),
		slog.String("provider", mobileVerificationProvider(receipt)),
		slog.String("mobile_hint", authidentity.RedactMobile(mobile)),
		slog.String("expires_at", expiresAt.Format(time.RFC3339)),
	}
	if senderIdentity := strings.TrimSpace(receipt.SenderIdentity); senderIdentity != "" {
		attrs = append(attrs, slog.String("sender_identity", senderIdentity))
	}
	if providerMessageID := strings.TrimSpace(receipt.ProviderMessageID); providerMessageID != "" {
		attrs = append(attrs, slog.String("provider_message_id", providerMessageID))
	}
	if providerStatus := strings.TrimSpace(receipt.ProviderStatus); providerStatus != "" {
		attrs = append(attrs, slog.String("provider_status", providerStatus))
	}
	return attrs
}

func (handler *MobileHandler) rollbackMobileVerificationToken(ctx context.Context, userID string, tokenHash string) {
	if rollbackErr := handler.store.DeleteMobileVerificationToken(ctx, userID, tokenHash); rollbackErr != nil {
		if handler.logger != nil {
			handler.logger.ErrorContext(ctx, "rollback mobile verification token", slog.String("user_id", userID), slog.String("error_class", "token_rollback_failed"))
		}
	}
}

// RequestVerification handles POST /api/v1/auth/mobile/request-verification.
// Sends a verification code to the user's mobile number.
func (handler *MobileHandler) RequestVerification(r api.AuthenticatedJSON[dto.MobileVerificationRequest]) (api.Response, error) {
	if handler.passwordAuthDisabled {
		return api.Response{}, api.NewHTTPError(http.StatusServiceUnavailable, "Mobile verification is disabled when password auth is disabled")
	}

	request := r.Body

	resolvedMobile, err := authidentity.NormalizeMobile(request.Mobile)
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if r.User.User.HasVerifiedMobile() && r.User.User.Mobile == resolvedMobile {
		return api.OK(gin.H{
			"alreadyVerified": true,
			"mobileHint":      authidentity.RedactMobile(resolvedMobile),
			"expiresAt":       nil,
		}), nil
	}

	verificationCode, err := newMobileVerificationCode(mobileVerificationCodeLength)
	if err != nil {
		return api.Response{}, err
	}

	expiresAt := time.Now().UTC().Add(mobileVerificationCodeTTL)
	tokenHash, err := authidentity.HashMobileVerificationCode(verificationCode)
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.store.CreateMobileVerificationToken(r.Context(), r.UserID(), resolvedMobile, tokenHash, expiresAt); err != nil {
		return api.Response{}, err
	}

	attempt, err := handler.store.CreateMobileVerificationAttempt(r.Context(), r.UserID(), resolvedMobile, expiresAt)
	if err != nil {
		handler.rollbackMobileVerificationToken(r.Context(), r.UserID(), tokenHash)
		return api.Response{}, err
	}

	receipt, err := handler.mobileVerificationSenderValue().Send(r.Context(), resolvedMobile, verificationCode)
	completedAt := time.Now().UTC()
	if err != nil {
		handler.completeMobileVerificationAttempt(r.Context(), attempt.ID, mobileVerificationAttemptParams(MobileVerificationAttemptStatusFailed, completedAt, receipt, err))
		if handler.logger != nil {
			handler.logger.WarnContext(r.Context(), "send mobile verification code failed", append(mobileVerificationLogAttrs(attempt.ID, r.UserID(), resolvedMobile, expiresAt, receipt), slog.String("error_class", "provider_send_failed"))...)
		}
		handler.rollbackMobileVerificationToken(r.Context(), r.UserID(), tokenHash)
		return api.Response{}, api.NewHTTPError(http.StatusServiceUnavailable, "Failed to send mobile verification code")
	}
	handler.completeMobileVerificationAttempt(r.Context(), attempt.ID, mobileVerificationAttemptParams(MobileVerificationAttemptStatusSent, completedAt, receipt, nil))
	if handler.logger != nil {
		handler.logger.InfoContext(r.Context(), "sent mobile verification code", mobileVerificationLogAttrs(attempt.ID, r.UserID(), resolvedMobile, expiresAt, receipt)...)
	}

	return api.OK(gin.H{
		"alreadyVerified": false,
		"mobileHint":      authidentity.RedactMobile(resolvedMobile),
		"expiresAt":       expiresAt.Format(time.RFC3339),
	}), nil
}

// Verify handles POST /api/v1/auth/mobile/verify.
// Verifies a mobile number using a previously sent verification code.
func (handler *MobileHandler) Verify(r api.AuthenticatedJSON[dto.MobileVerifyRequest]) (api.Response, error) {
	if handler.passwordAuthDisabled {
		return api.Response{}, api.NewHTTPError(http.StatusServiceUnavailable, "Mobile verification is disabled when password auth is disabled")
	}

	request := r.Body

	verificationCode := strings.TrimSpace(request.Code)
	if verificationCode == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "code is required")
	}

	user, err := handler.store.VerifyMobileToken(r.Context(), r.UserID(), verificationCode)
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(userResponseFromDomain(user)), nil
}

// History handles GET /api/v1/auth/mobile/history.
// Returns recent mobile verification attempts for the authenticated user.
func (handler *MobileHandler) History(r api.AuthenticatedRequest) (api.Response, error) {

	attempts, err := handler.store.ListMobileVerificationAttemptsForUser(r.Context(), r.UserID(), mobileVerificationHistoryMax)
	if err != nil {
		return api.Response{}, err
	}

	activeCode, hasActiveCode, err := handler.store.GetActiveMobileVerificationCodeForUser(r.Context(), r.UserID())
	if err != nil {
		return api.Response{}, err
	}
	views := mobileVerificationAttemptViews(attempts, activeCode, hasActiveCode)

	return api.OK(gin.H{
		"attempts": views,
	}), nil
}

func (handler *MobileHandler) mobileVerificationSenderValue() sms.Sender {
	if handler.mobileVerificationSenderProvider == nil {
		return sms.NewNoop(handler.logger)
	}
	sender := handler.mobileVerificationSenderProvider()
	if sender == nil {
		return sms.NewNoop(handler.logger)
	}
	return sender
}
