package api

import (
	"context"
	"net/http"
	"time"

	"github.com/fatih/structs"
	"github.com/gofrs/uuid"
	"github.com/pkg/errors"
	"github.com/supabase/auth/internal/api/provider"
	"github.com/supabase/auth/internal/api/sms_provider"
	"github.com/supabase/auth/internal/metering"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/storage"
)

// SignupParams are the parameters the Signup endpoint accepts
type SignupParams struct {
	Email               string                 `json:"email"`
	Phone               string                 `json:"phone"`
	Password            string                 `json:"password"`
	Data                map[string]interface{} `json:"data"`
	Provider            string                 `json:"-"`
	Aud                 string                 `json:"-"`
	Channel             string                 `json:"channel"`
	CodeChallengeMethod string                 `json:"code_challenge_method"`
	CodeChallenge       string                 `json:"code_challenge"`
}

func (a *API) validateSignupParams(ctx context.Context, p *SignupParams) error {
	config := a.config

	if p.Password == "" {
		return badRequestError(ErrorCodeValidationFailed, "Signup requires a valid password")
	}

	if err := a.checkPasswordStrength(ctx, p.Password); err != nil {
		return err
	}
	if p.Email != "" && p.Phone != "" {
		return badRequestError(ErrorCodeValidationFailed, "Only an email address or phone number should be provided on signup.")
	}
	if p.Provider == "phone" && !sms_provider.IsValidMessageChannel(p.Channel, config) {
		return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
	}
	// PKCE not needed as phone signups already return access token in body
	if p.Phone != "" && p.CodeChallenge != "" {
		return badRequestError(ErrorCodeValidationFailed, "PKCE not supported for phone signups")
	}
	if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
		return err
	}

	return nil
}

func (p *SignupParams) ConfigureDefaults() {
	if p.Email != "" {
		p.Provider = "email"
	} else if p.Phone != "" {
		p.Provider = "phone"
	}
	if p.Data == nil {
		p.Data = make(map[string]interface{})
	}

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

func (params *SignupParams) ToUserModel(isSSOUser bool) (user *models.User, err error) {
	switch params.Provider {
	case "email":
		user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data)
	case "phone":
		user, err = models.NewUser(params.Phone, "", params.Password, params.Aud, params.Data)
	case "anonymous":
		user, err = models.NewUser("", "", "", params.Aud, params.Data)
		user.IsAnonymous = true
	default:
		// handles external provider case
		user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data)
	}
	if err != nil {
		err = internalServerError("Database error creating user").WithInternalError(err)
		return
	}
	user.IsSSOUser = isSSOUser
	if user.AppMetaData == nil {
		user.AppMetaData = make(map[string]interface{})
	}

	user.Identities = make([]models.Identity, 0)

	if params.Provider != "anonymous" {
		// TODO: Deprecate "provider" field
		user.AppMetaData["provider"] = params.Provider

		user.AppMetaData["providers"] = []string{params.Provider}
	}

	return user, nil
}

// Signup is the endpoint for registering a new user
func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	config := a.config
	db := a.db.WithContext(ctx)

	if config.DisableSignup {
		return unprocessableEntityError(ErrorCodeSignupDisabled, "Signups not allowed for this instance")
	}

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

	params.ConfigureDefaults()

	if err := a.validateSignupParams(ctx, params); err != nil {
		return err
	}

	var err error
	flowType := getFlowFromChallenge(params.CodeChallenge)

	var user *models.User
	var grantParams models.GrantParams

	grantParams.FillGrantParams(r)

	params.Aud = a.requestAud(ctx, r)

	switch params.Provider {
	case "email":
		if !config.External.Email.Enabled {
			return badRequestError(ErrorCodeEmailProviderDisabled, "Email signups are disabled")
		}
		params.Email, err = a.validateEmail(params.Email)
		if err != nil {
			return err
		}
		user, err = models.IsDuplicatedEmail(db, params.Email, params.Aud, nil)
	case "phone":
		if !config.External.Phone.Enabled {
			return badRequestError(ErrorCodePhoneProviderDisabled, "Phone signups are disabled")
		}
		params.Phone, err = validatePhone(params.Phone)
		if err != nil {
			return err
		}
		user, err = models.FindUserByPhoneAndAudience(db, params.Phone, params.Aud)
	default:
		msg := ""
		if config.External.Email.Enabled && config.External.Phone.Enabled {
			msg = "Sign up only available with email or phone provider"
		} else if config.External.Email.Enabled {
			msg = "Sign up only available with email provider"
		} else if config.External.Phone.Enabled {
			msg = "Sign up only available with phone provider"
		} else {
			msg = "Sign up with this provider not possible"
		}

		return badRequestError(ErrorCodeValidationFailed, msg)
	}

	if err != nil && !models.IsNotFoundError(err) {
		return internalServerError("Database error finding user").WithInternalError(err)
	}

	var signupUser *models.User
	if user == nil {
		// always call this outside of a database transaction as this method
		// can be computationally hard and block due to password hashing
		signupUser, err = params.ToUserModel(false /* <- isSSOUser */)
		if err != nil {
			return err
		}
	}

	err = db.Transaction(func(tx *storage.Connection) error {
		var terr error
		if user != nil {
			if (params.Provider == "email" && user.IsConfirmed()) || (params.Provider == "phone" && user.IsPhoneConfirmed()) {
				return UserExistsError
			}
			// do not update the user because we can't be sure of their claimed identity
		} else {
			user, terr = a.signupNewUser(tx, signupUser)
			if terr != nil {
				return terr
			}
		}
		identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), params.Provider)
		if terr != nil {
			if !models.IsNotFoundError(terr) {
				return terr
			}
			identityData := structs.Map(provider.Claims{
				Subject: user.ID.String(),
				Email:   user.GetEmail(),
			})
			for k, v := range params.Data {
				if _, ok := identityData[k]; !ok {
					identityData[k] = v
				}
			}
			identity, terr = a.createNewIdentity(tx, user, params.Provider, identityData)
			if terr != nil {
				return terr
			}
			if terr := user.RemoveUnconfirmedIdentities(tx, identity); terr != nil {
				return terr
			}
		}
		user.Identities = []models.Identity{*identity}

		if params.Provider == "email" && !user.IsConfirmed() {
			if config.Mailer.Autoconfirm {
				if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{
					"provider": params.Provider,
				}); terr != nil {
					return terr
				}
				if terr = user.Confirm(tx); terr != nil {
					return internalServerError("Database error updating user").WithInternalError(terr)
				}
			} else {
				if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{
					"provider": params.Provider,
				}); terr != nil {
					return terr
				}
				if isPKCEFlow(flowType) {
					_, terr := generateFlowState(tx, params.Provider, models.EmailSignup, params.CodeChallengeMethod, params.CodeChallenge, &user.ID)
					if terr != nil {
						return terr
					}
				}
				if terr = a.sendConfirmation(r, tx, user, flowType); terr != nil {
					return terr
				}
			}
		} else if params.Provider == "phone" && !user.IsPhoneConfirmed() {
			if config.Sms.Autoconfirm {
				if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{
					"provider": params.Provider,
					"channel":  params.Channel,
				}); terr != nil {
					return terr
				}
				if terr = user.ConfirmPhone(tx); terr != nil {
					return internalServerError("Database error updating user").WithInternalError(terr)
				}
			} else {
				if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{
					"provider": params.Provider,
				}); terr != nil {
					return terr
				}
				if _, terr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneConfirmationOtp, params.Channel); terr != nil {
					return terr
				}
			}
		}

		return nil
	})

	if err != nil {
		if errors.Is(err, UserExistsError) {
			err = db.Transaction(func(tx *storage.Connection) error {
				if terr := models.NewAuditLogEntry(r, tx, user, models.UserRepeatedSignUpAction, "", map[string]interface{}{
					"provider": params.Provider,
				}); terr != nil {
					return terr
				}
				return nil
			})
			if err != nil {
				return err
			}
			if config.Mailer.Autoconfirm || config.Sms.Autoconfirm {
				return unprocessableEntityError(ErrorCodeUserAlreadyExists, "User already registered")
			}
			sanitizedUser, err := sanitizeUser(user, params)
			if err != nil {
				return err
			}
			return sendJSON(w, http.StatusOK, sanitizedUser)
		}
		return err
	}

	// handles case where Mailer.Autoconfirm is true or Phone.Autoconfirm is true
	if user.IsConfirmed() || user.IsPhoneConfirmed() {
		var token *AccessTokenResponse
		err = db.Transaction(func(tx *storage.Connection) error {
			var terr error
			if terr = models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{
				"provider": params.Provider,
			}); terr != nil {
				return terr
			}
			token, terr = a.issueRefreshToken(r, tx, user, models.PasswordGrant, grantParams)

			if terr != nil {
				return terr
			}
			return nil
		})
		if err != nil {
			return err
		}
		metering.RecordLogin("password", user.ID)
		return sendJSON(w, http.StatusOK, token)
	}
	if user.HasBeenInvited() {
		// Remove sensitive fields
		user.UserMetaData = map[string]interface{}{}
		user.Identities = []models.Identity{}
	}
	return sendJSON(w, http.StatusOK, user)
}

// sanitizeUser removes all user sensitive information from the user object
// Should be used whenever we want to prevent information about whether a user is registered or not from leaking
func sanitizeUser(u *models.User, params *SignupParams) (*models.User, error) {
	now := time.Now()

	u.ID = uuid.Must(uuid.NewV4())

	u.Role, u.EmailChange = "", ""
	u.CreatedAt, u.UpdatedAt, u.ConfirmationSentAt = now, now, &now
	u.LastSignInAt, u.ConfirmedAt, u.EmailChangeSentAt, u.EmailConfirmedAt, u.PhoneConfirmedAt = nil, nil, nil, nil, nil
	u.Identities = make([]models.Identity, 0)
	u.UserMetaData = params.Data
	u.Aud = params.Aud

	// sanitize app_metadata
	u.AppMetaData = map[string]interface{}{
		"provider":  params.Provider,
		"providers": []string{params.Provider},
	}

	// sanitize param fields
	switch params.Provider {
	case "email":
		u.Phone = ""
	case "phone":
		u.Email = ""
	default:
		u.Phone, u.Email = "", ""
	}

	return u, nil
}

func (a *API) signupNewUser(conn *storage.Connection, user *models.User) (*models.User, error) {
	config := a.config

	err := conn.Transaction(func(tx *storage.Connection) error {
		var terr error
		if terr = tx.Create(user); terr != nil {
			return internalServerError("Database error saving new user").WithInternalError(terr)
		}
		if terr = user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil {
			return internalServerError("Database error updating user").WithInternalError(terr)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	// there may be triggers or generated column values in the database that will modify the
	// user data as it is being inserted. thus we load the user object
	// again to fetch those changes.
	if err := conn.Reload(user); err != nil {
		return nil, internalServerError("Database error loading user after sign-up").WithInternalError(err)
	}

	return user, nil
}
