package api

import (
	"bytes"
	"net/http"
	"regexp"
	"strings"
	"text/template"
	"time"

	"github.com/supabase/auth/internal/hooks"

	"github.com/pkg/errors"
	"github.com/supabase/auth/internal/api/sms_provider"
	"github.com/supabase/auth/internal/crypto"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/storage"
)

var e164Format = regexp.MustCompile("^[1-9][0-9]{1,14}$")

const (
	phoneConfirmationOtp     = "confirmation"
	phoneReauthenticationOtp = "reauthentication"
)

func validatePhone(phone string) (string, error) {
	phone = formatPhoneNumber(phone)
	if isValid := validateE164Format(phone); !isValid {
		return "", badRequestError(ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)")
	}
	return phone, nil
}

// validateE164Format checks if phone number follows the E.164 format
func validateE164Format(phone string) bool {
	return e164Format.MatchString(phone)
}

// formatPhoneNumber removes "+" and whitespaces in a phone number
func formatPhoneNumber(phone string) string {
	return strings.ReplaceAll(strings.TrimPrefix(phone, "+"), " ", "")
}

// sendPhoneConfirmation sends an otp to the user's phone number
func (a *API) sendPhoneConfirmation(r *http.Request, tx *storage.Connection, user *models.User, phone, otpType string, channel string) (string, error) {
	config := a.config

	var token *string
	var sentAt *time.Time

	includeFields := []string{}
	switch otpType {
	case phoneChangeVerification:
		token = &user.PhoneChangeToken
		sentAt = user.PhoneChangeSentAt
		user.PhoneChange = phone
		includeFields = append(includeFields, "phone_change", "phone_change_token", "phone_change_sent_at")
	case phoneConfirmationOtp:
		token = &user.ConfirmationToken
		sentAt = user.ConfirmationSentAt
		includeFields = append(includeFields, "confirmation_token", "confirmation_sent_at")
	case phoneReauthenticationOtp:
		token = &user.ReauthenticationToken
		sentAt = user.ReauthenticationSentAt
		includeFields = append(includeFields, "reauthentication_token", "reauthentication_sent_at")
	default:
		return "", internalServerError("invalid otp type")
	}

	// intentionally keeping this before the test OTP, so that the behavior
	// of regular and test OTPs is similar
	if sentAt != nil && !sentAt.Add(config.Sms.MaxFrequency).Before(time.Now()) {
		return "", tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, generateFrequencyLimitErrorMessage(sentAt, config.Sms.MaxFrequency))
	}

	now := time.Now()

	var otp, messageID string

	if testOTP, ok := config.Sms.GetTestOTP(phone, now); ok {
		otp = testOTP
		messageID = "test-otp"
	}

	// not using test OTPs
	if otp == "" {
		// TODO(km): Deprecate this behaviour - rate limits should still be applied to autoconfirm
		if !config.Sms.Autoconfirm {
			// apply rate limiting before the sms is sent out
			if ok := a.limiterOpts.Phone.Allow(); !ok {
				return "", tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, "SMS rate limit exceeded")
			}
		}
		otp = crypto.GenerateOtp(config.Sms.OtpLength)

		if config.Hook.SendSMS.Enabled {
			input := hooks.SendSMSInput{
				User: user,
				SMS: hooks.SMS{
					OTP: otp,
				},
			}
			output := hooks.SendSMSOutput{}
			err := a.invokeHook(tx, r, &input, &output)
			if err != nil {
				return "", err
			}
		} else {
			smsProvider, err := sms_provider.GetSmsProvider(*config)
			if err != nil {
				return "", internalServerError("Unable to get SMS provider").WithInternalError(err)
			}
			message, err := generateSMSFromTemplate(config.Sms.SMSTemplate, otp)
			if err != nil {
				return "", internalServerError("error generating sms template").WithInternalError(err)
			}
			messageID, err := smsProvider.SendMessage(phone, message, channel, otp)
			if err != nil {
				return messageID, unprocessableEntityError(ErrorCodeSMSSendFailed, "Error sending %s OTP to provider: %v", otpType, err)
			}
		}
	}

	*token = crypto.GenerateTokenHash(phone, otp)

	switch otpType {
	case phoneConfirmationOtp:
		user.ConfirmationSentAt = &now
	case phoneChangeVerification:
		user.PhoneChangeSentAt = &now
	case phoneReauthenticationOtp:
		user.ReauthenticationSentAt = &now
	}

	if err := tx.UpdateOnly(user, includeFields...); err != nil {
		return messageID, errors.Wrap(err, "Database error updating user for phone")
	}

	var ottErr error
	switch otpType {
	case phoneConfirmationOtp:
		if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ConfirmationToken, models.ConfirmationToken); err != nil {
			ottErr = errors.Wrap(err, "Database error creating confirmation token for phone")
		}
	case phoneChangeVerification:
		if err := models.CreateOneTimeToken(tx, user.ID, user.PhoneChange, user.PhoneChangeToken, models.PhoneChangeToken); err != nil {
			ottErr = errors.Wrap(err, "Database error creating phone change token")
		}
	case phoneReauthenticationOtp:
		if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ReauthenticationToken, models.ReauthenticationToken); err != nil {
			ottErr = errors.Wrap(err, "Database error creating reauthentication token for phone")
		}
	}
	if ottErr != nil {
		return messageID, internalServerError("error creating one time token").WithInternalError(ottErr)
	}
	return messageID, nil
}

func generateSMSFromTemplate(SMSTemplate *template.Template, otp string) (string, error) {
	var message bytes.Buffer
	if err := SMSTemplate.Execute(&message, struct {
		Code string
	}{Code: otp}); err != nil {
		return "", err
	}
	return message.String(), nil
}
