package api

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"

	"github.com/sethvargo/go-password/password"
	"github.com/supabase/auth/internal/api/sms_provider"
	"github.com/supabase/auth/internal/conf"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/storage"
)

// OtpParams contains the request body params for the otp endpoint
type OtpParams struct {
	Email               string                 `json:"email"`
	Phone               string                 `json:"phone"`
	CreateUser          bool                   `json:"create_user"`
	Data                map[string]interface{} `json:"data"`
	Channel             string                 `json:"channel"`
	CodeChallengeMethod string                 `json:"code_challenge_method"`
	CodeChallenge       string                 `json:"code_challenge"`
}

// SmsParams contains the request body params for sms otp
type SmsParams struct {
	Phone               string                 `json:"phone"`
	Channel             string                 `json:"channel"`
	Data                map[string]interface{} `json:"data"`
	CodeChallengeMethod string                 `json:"code_challenge_method"`
	CodeChallenge       string                 `json:"code_challenge"`
}

func (p *OtpParams) Validate() error {
	if p.Email != "" && p.Phone != "" {
		return badRequestError(ErrorCodeValidationFailed, "Only an email address or phone number should be provided")
	}
	if p.Email != "" && p.Channel != "" {
		return badRequestError(ErrorCodeValidationFailed, "Channel should only be specified with Phone OTP")
	}
	if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
		return err
	}
	return nil
}

func (p *SmsParams) Validate(config *conf.GlobalConfiguration) error {
	var err error
	p.Phone, err = validatePhone(p.Phone)
	if err != nil {
		return err
	}
	if !sms_provider.IsValidMessageChannel(p.Channel, config) {
		return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
	}
	return nil
}

// Otp returns the MagicLink or SmsOtp handler based on the request body params
func (a *API) Otp(w http.ResponseWriter, r *http.Request) error {
	params := &OtpParams{
		CreateUser: true,
	}
	if params.Data == nil {
		params.Data = make(map[string]interface{})
	}

	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}

	if err := params.Validate(); err != nil {
		return err
	}
	if params.Data == nil {
		params.Data = make(map[string]interface{})
	}

	if ok, err := a.shouldCreateUser(r, params); !ok {
		return unprocessableEntityError(ErrorCodeOTPDisabled, "Signups not allowed for otp")
	} else if err != nil {
		return err
	}

	if params.Email != "" {
		return a.MagicLink(w, r)
	} else if params.Phone != "" {
		return a.SmsOtp(w, r)
	}

	return badRequestError(ErrorCodeValidationFailed, "One of email or phone must be set")
}

type SmsOtpResponse struct {
	MessageID string `json:"message_id,omitempty"`
}

// SmsOtp sends the user an otp via sms
func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	db := a.db.WithContext(ctx)
	config := a.config

	if !config.External.Phone.Enabled {
		return badRequestError(ErrorCodePhoneProviderDisabled, "Unsupported phone provider")
	}
	var err error

	params := &SmsParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}

	// For backwards compatibility, we default to SMS if params Channel is not specified
	if params.Phone != "" && params.Channel == "" {
		params.Channel = sms_provider.SMSProvider
	}

	if err := params.Validate(config); err != nil {
		return err
	}

	var isNewUser bool
	aud := a.requestAud(ctx, r)
	user, err := models.FindUserByPhoneAndAudience(db, params.Phone, aud)
	if err != nil {
		if models.IsNotFoundError(err) {
			isNewUser = true
		} else {
			return internalServerError("Database error finding user").WithInternalError(err)
		}
	}
	if user != nil {
		isNewUser = !user.IsPhoneConfirmed()
	}
	if isNewUser {
		// User either doesn't exist or hasn't completed the signup process.
		// Sign them up with temporary password.
		password, err := password.Generate(64, 10, 1, false, true)
		if err != nil {
			return internalServerError("error creating user").WithInternalError(err)
		}

		signUpParams := &SignupParams{
			Phone:    params.Phone,
			Password: password,
			Data:     params.Data,
			Channel:  params.Channel,
		}
		newBodyContent, err := json.Marshal(signUpParams)
		if err != nil {
			// SignupParams must be marshallable
			panic(err)
		}
		r.Body = io.NopCloser(bytes.NewReader(newBodyContent))

		fakeResponse := &responseStub{}

		if config.Sms.Autoconfirm {
			// signups are autoconfirmed, send otp after signup
			if err := a.Signup(fakeResponse, r); err != nil {
				return err
			}

			signUpParams := &SignupParams{
				Phone:   params.Phone,
				Channel: params.Channel,
			}
			newBodyContent, err := json.Marshal(signUpParams)
			if err != nil {
				// SignupParams must be marshallable
				panic(err)
			}
			r.Body = io.NopCloser(bytes.NewReader(newBodyContent))
			return a.SmsOtp(w, r)
		}

		if err := a.Signup(fakeResponse, r); err != nil {
			return err
		}
		return sendJSON(w, http.StatusOK, make(map[string]string))
	}

	messageID := ""
	err = db.Transaction(func(tx *storage.Connection) error {
		if err := models.NewAuditLogEntry(r, tx, user, models.UserRecoveryRequestedAction, "", map[string]interface{}{
			"channel": params.Channel,
		}); err != nil {
			return err
		}
		mID, serr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneConfirmationOtp, params.Channel)
		if serr != nil {
			return serr
		}
		messageID = mID
		return nil
	})

	if err != nil {
		return err
	}

	return sendJSON(w, http.StatusOK, SmsOtpResponse{
		MessageID: messageID,
	})
}

func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) (bool, error) {
	ctx := r.Context()
	db := a.db.WithContext(ctx)

	if !params.CreateUser {
		ctx := r.Context()
		aud := a.requestAud(ctx, r)
		var err error
		if params.Email != "" {
			params.Email, err = a.validateEmail(params.Email)
			if err != nil {
				return false, err
			}
			_, err = models.FindUserByEmailAndAudience(db, params.Email, aud)
		} else if params.Phone != "" {
			params.Phone, err = validatePhone(params.Phone)
			if err != nil {
				return false, err
			}
			_, err = models.FindUserByPhoneAndAudience(db, params.Phone, aud)
		}

		if err != nil && models.IsNotFoundError(err) {
			return false, nil
		}
	}
	return true, nil
}
