package main

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/google/uuid"
	"github.com/labstack/echo/v4"
	"github.com/samber/mo"
	"gorm.io/gorm"
	"io"
	"net/http"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const API_MAJOR_VERSION = 2

var DRASL_API_PREFIX = fmt.Sprintf("/drasl/api/v%d", API_MAJOR_VERSION)

//	@title			Drasl API
//	@version		2.0
//	@description	Manage Drasl users, players, and invites

//	@contact.name	Unmojang
//	@contact.url	https://github.com/unmojang/drasl

//	@license.name	GPLv3
//	@license.url	https://www.gnu.org/licenses/gpl-3.0.en.html

type APIError struct {
	Message string `json:"message" example:"An error occurred"`
}

func (app *App) HandleAPIError(err error, c *echo.Context) error {
	code := http.StatusInternalServerError
	message := "Internal server error"
	log := true

	if e, ok := err.(*echo.HTTPError); ok {
		code = e.Code

		if m, ok := e.Message.(string); ok {
			message = m
		}

		if code == http.StatusNotFound {
			baseRelative, err := app.BaseRelativePath((*c).Request().URL.Path)
			if err == nil {
				if version, ok := IsDeprecatedAPIPath(baseRelative).Get(); ok {
					switch version {
					case 1:
						message = "Version 1 of this API was deprecated in release 3.0.0."
					default:
						message = fmt.Sprintf("Version %d of this API is deprecated.", version)
					}
				}
			}
		}

		log = false
	}

	var userError *UserError
	if errors.As(err, &userError) {
		code = userError.Code.OrElse(http.StatusInternalServerError)
		message = userError.Error()
		log = false
	}

	if log {
		LogError(err, c)
	}

	return (*c).JSON(code, APIError{Message: message})
}

func IsDeprecatedAPIPath(baseRelative string) mo.Option[int] {
	if baseRelative == "/" {
		return mo.None[int]()
	}

	split := strings.Split(baseRelative, "/")
	if len(split) >= 3 && split[1] == "drasl" && split[2] == "api" {
		re := regexp.MustCompile(`v(\d+)`)
		match := re.FindStringSubmatch(split[3])
		if len(match) == 2 {
			version, err := strconv.Atoi(match[1])
			if err == nil && version != API_MAJOR_VERSION {
				return mo.Some(version)
			}
		}
	}

	return mo.None[int]()
}

func (app *App) APIRequestToMaybeUser(c echo.Context) (mo.Option[User], error) {
	authorizationHeader := c.Request().Header.Get("Authorization")
	if authorizationHeader == "" {
		return mo.None[User](), nil
	}

	bearerExp := regexp.MustCompile("^Bearer (.*)$")

	tokenMatch := bearerExp.FindStringSubmatch(authorizationHeader)
	if len(tokenMatch) < 2 {
		return mo.None[User](), NewUserErrorWithCode(http.StatusUnauthorized, "Malformed Authorization header")
	}
	token := tokenMatch[1]

	var user User
	if err := app.DB.First(&user, "api_token = ?", token).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return mo.None[User](), NewUserErrorWithCode(http.StatusUnauthorized, "Unknown API token")
		}
		return mo.None[User](), err
	}

	if user.IsLocked {
		return mo.None[User](), NewUserErrorWithCode(http.StatusForbidden, "Account is locked")
	}

	return mo.Some(user), nil
}

func (app *App) withAPIToken(requireLogin bool, f func(c echo.Context, user *User) error) func(c echo.Context) error {
	return func(c echo.Context) error {
		maybeUser, err := app.APIRequestToMaybeUser(c)
		if err != nil {
			return err
		}
		if maybeUser.IsAbsent() && requireLogin {
			return NewUserErrorWithCode(http.StatusUnauthorized, "Route requires authorization. Missing 'Bearer: abcdef' Authorization header")
		}
		return f(c, maybeUser.ToPointer())
	}
}

func (app *App) withAPITokenAdmin(f func(c echo.Context, user *User) error) func(c echo.Context) error {
	notAnAdminBlob := Unwrap(json.Marshal(map[string]string{
		"error": "You are not an admin.",
	}))
	return app.withAPIToken(true, func(c echo.Context, user *User) error {
		if !user.IsAdmin {
			return c.JSONBlob(http.StatusForbidden, notAnAdminBlob)
		}
		return f(c, user)
	})
}

type APIUser struct {
	IsAdmin           bool              `json:"isAdmin" example:"true"`   // Whether the user is an admin
	IsLocked          bool              `json:"isLocked" example:"false"` // Whether the user is locked (disabled)
	UUID              string            `json:"uuid" example:"557e0c92-2420-4704-8840-a790ea11551c"`
	Username          string            `json:"username" example:"MyUsername"`  // Username. Can be different from the user's player name.
	PreferredLanguage string            `json:"preferredLanguage" example:"en"` // One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft.
	MaxPlayerCount    int               `json:"maxPlayerCount" example:"3"`     // Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
	Players           []APIPlayer       `json:"players"`                        // A user can have multiple players.
	OIDCIdentities    []APIOIDCIdentity `json:"oidcIdentities"`                 // OIDC identities linked to the user
}

func (app *App) userToAPIUser(user *User) (APIUser, error) {
	apiPlayers := make([]APIPlayer, 0, len(user.Players))
	for _, player := range user.Players {
		apiPlayer, err := app.playerToAPIPlayer(&player)
		if err != nil {
			return APIUser{}, err
		}
		apiPlayers = append(apiPlayers, apiPlayer)
	}

	apiOIDCIdentities := make([]APIOIDCIdentity, 0, len(user.OIDCIdentities))
	for _, oidcIdentity := range user.OIDCIdentities {
		apiOIDCIdentity, err := app.oidcIdentityToAPIOIDCIdentity(&oidcIdentity)
		if err != nil {
			return APIUser{}, err
		}
		apiOIDCIdentities = append(apiOIDCIdentities, apiOIDCIdentity)
	}

	return APIUser{
		IsAdmin:           user.IsAdmin,
		IsLocked:          user.IsLocked,
		UUID:              user.UUID,
		Username:          user.Username,
		PreferredLanguage: user.PreferredLanguage,
		Players:           apiPlayers,
		OIDCIdentities:    apiOIDCIdentities,
		MaxPlayerCount:    user.MaxPlayerCount,
	}, nil
}

type APIPlayer struct {
	UserUUID          string    `json:"userUuid" example:"918bd04e-1bc4-4ccd-860f-60c15c5f1cec"`                                                                             // UUID of the owning user.
	Name              string    `json:"name" example:"MyPlayerName"`                                                                                                         // Player name, seen by Minecraft. Can be different from the owning user's username.
	UUID              string    `json:"uuid" example:"e6d266d5-d559-4ec4-bc9b-1866d13d7f91"`                                                                                 // UUID of the player, seen by Minecraft. Not guaranteed to be different from the owning user's UUID.
	OfflineUUID       string    `json:"offlineUuid" example:"8dcf1aea-9b60-3d88-983b-185671d1a912"`                                                                          // UUID of the user in `online-mode=false` servers. Derived from the user's player name.
	FallbackPlayer    string    `json:"fallbackPlayer" example:"Notch"`                                                                                                      // UUID or player name. If the user doesn't have a skin or cape set, this player's skin on one of the fallback API servers will be used instead.
	SkinModel         string    `json:"skinModel" example:"slim"`                                                                                                            // Skin model. Either `"classic"` or `"slim"`.
	SkinURL           *string   `json:"skinUrl" example:"https://drasl.example.com/drasl/texture/skin/fa85a8f3d36beb9b6041b5f50a6b4c33970e281827effc1b22b0f04bcb017331.png"` // URL to the user's skin, if they have set one. If no skin is set, the Minecraft client may still see a skin if `FallbackAPIServers` or default skins are configured.
	CapeURL           *string   `json:"capeUrl" example:"https://drasl.example.com/drasl/texture/cape/bf74bd4d115c5da69754ebf86b5d33a03dd5ad48910b8c7ebf276bba6b3a5603.png"` // URL to the user's cape, if they have set one. If no cape is set, the Minecraft client may still see a cape if `FallbackAPIServers` or default capes are configured.
	CreatedAt         time.Time `json:"createdAt" example:"2024-05-18T01:11:32.836265485-04:00"`                                                                             // ISO datetime when the user was created
	NameLastChangedAt time.Time `json:"nameLastChangedAt" example:"2024-05-29T13:54:24.448081165-04:00"`                                                                     // ISO 8601 datetime when the user's player name was last changed
}

func (app *App) playerToAPIPlayer(player *Player) (APIPlayer, error) {
	skinURL, err := app.GetSkinURL(player)
	if err != nil {
		return APIPlayer{}, err
	}
	capeURL, err := app.GetCapeURL(player)
	if err != nil {
		return APIPlayer{}, err
	}
	return APIPlayer{
		UserUUID:          player.UserUUID,
		Name:              player.Name,
		UUID:              player.UUID,
		OfflineUUID:       player.OfflineUUID,
		FallbackPlayer:    player.FallbackPlayer,
		SkinURL:           skinURL,
		SkinModel:         player.SkinModel,
		CapeURL:           capeURL,
		CreatedAt:         player.CreatedAt,
		NameLastChangedAt: player.NameLastChangedAt,
	}, nil
}

type APIOIDCIdentity struct {
	UserUUID         string `json:"userUuid" example:"918bd04e-1bc4-4ccd-860f-60c15c5f1cec"`
	OIDCProviderName string `json:"oidcProviderName" example:"Kanidm"`
	Issuer           string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
	Subject          string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
}

func (app *App) oidcIdentityToAPIOIDCIdentity(oidcIdentity *UserOIDCIdentity) (APIOIDCIdentity, error) {
	oidcProvider, ok := app.OIDCProvidersByIssuer[oidcIdentity.Issuer]
	if !ok {
		return APIOIDCIdentity{}, InternalServerError
	}
	return APIOIDCIdentity{
		UserUUID:         oidcIdentity.UserUUID,
		OIDCProviderName: oidcProvider.Config.Name,
		Issuer:           oidcIdentity.Issuer,
		Subject:          oidcIdentity.Subject,
	}, nil
}

type APIInvite struct {
	Code      string    `json:"code" example:"rqjJwh0yMjO"`                                                    // The base62 invite code
	URL       string    `json:"url" example:"https://drasl.example.com/drasl/registration?invite=rqjJwh0yMjO"` // Link to register using the invite
	CreatedAt time.Time `json:"createdAt" example:"2024-05-18T01:11:32.836265485-04:00"`                       // ISO 8601 datetime when the invite was created
}

func (app *App) inviteToAPIInvite(invite *Invite) (APIInvite, error) {
	url, err := app.InviteURL(invite)
	if err != nil {
		return APIInvite{}, err
	}
	return APIInvite{
		Code:      invite.Code,
		URL:       url,
		CreatedAt: invite.CreatedAt,
	}, nil
}

func (app *App) APISwagger() func(c echo.Context) error {
	swaggerPath := path.Join(app.Config.DataDirectory, "assets", "swagger.json")
	swaggerBlob := Unwrap(os.ReadFile(swaggerPath))
	return func(c echo.Context) error {
		return c.JSONBlob(http.StatusOK, swaggerBlob)
	}
}

// APIGetUsers godoc
//
//	@Summary		Get users
//	@Description	Get details of all users. Requires admin privileges.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Success		200	{array}		APIUser
//	@Failure		401	{object}	APIError
//	@Failure		403	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/users [get]
func (app *App) APIGetUsers() func(c echo.Context) error {
	return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
		var users []User
		result := app.DB.Find(&users)
		if result.Error != nil {
			return result.Error
		}

		apiUsers := make([]APIUser, 0, len(users))
		for _, user := range users {
			apiUser, err := app.userToAPIUser(&user)
			if err != nil {
				return err
			}
			apiUsers = append(apiUsers, apiUser)
		}

		return c.JSON(http.StatusOK, apiUsers)
	})
}

// APIGetUser godoc
//
//	@Summary		Get user details
//	@Description	Get details of a user, either the calling user (GET /user) or the user with the specified UUID (GET /users/{uuid}). Getting details of another user requires admin privileges.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Param			uuid	path		string	true	"User UUID"
//	@Success		200		{object}	APIUser
//	@Failure		400		{object}	APIError
//	@Failure		401		{object}	APIError
//	@Failure		403		{object}	APIError
//	@Failure		404		{object}	APIError
//	@Failure		500		{object}	APIError
//	@Router			/drasl/api/v2/users/{uuid} [get]
//	@Router			/drasl/api/v2/user [get]
func (app *App) APIGetUser() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		targetUser := caller

		uuidParam := c.Param("uuid")
		if uuidParam != "" {
			if !caller.IsAdmin && (caller.UUID != uuidParam) {
				return NewUserErrorWithCode(http.StatusForbidden, "You are not authorized to access that user.")
			}

			_, err := uuid.Parse(uuidParam)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
			}

			var targetUserStruct User
			if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
				}
				return err
			}

			targetUser = &targetUserStruct
		}

		apiUser, err := app.userToAPIUser(targetUser)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiUser)
	})
}

type APIOIDCIdentitySpec struct {
	Issuer  string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
	Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
}

type APICreateUserRequest struct {
	Username          string                `json:"username" example:"MyUsername"` // Username of the new user. Can be different from the user's player name.
	Password          *string               `json:"password" example:"hunter2"`    // Plaintext password. Not needed if OIDCIdentitySpecs are supplied.
	OIDCIdentitySpecs []APIOIDCIdentitySpec `json:"oidcIdentities"`
	IsAdmin           bool                  `json:"isAdmin" example:"true"`                                                                               // Whether the user is an admin
	IsLocked          bool                  `json:"isLocked" example:"false"`                                                                             // Whether the user is locked (disabled)
	RequestAPIToken   bool                  `json:"requestApiToken" example:"true"`                                                                       // Whether to include an API token for the user in the response
	ChosenUUID        *string               `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"`                                            // Optional. Specify a UUID for the player of the new user. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.
	ExistingPlayer    bool                  `json:"existingPlayer" example:"false"`                                                                       // If true, the new user's player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
	InviteCode        *string               `json:"inviteCode" example:"rqjJwh0yMjO"`                                                                     // Invite code to use. Optional even if the `RequireInvite` configuration option is set; admin API users can bypass `RequireInvite`.
	PlayerName        *string               `json:"playerName" example:"MyPlayerName"`                                                                    // Optional. Player name. Can be different from the user's username. If omitted, the user's username will be used.
	FallbackPlayer    *string               `json:"fallbackPlayer" example:"Notch"`                                                                       // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
	PreferredLanguage *string               `json:"preferredLanguage" example:"en"`                                                                       // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft. If omitted, the value of the `DefaultPreferredLanguage` configuration option will be used.
	SkinModel         *string               `json:"skinModel" example:"classic"`                                                                          // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
	SkinBase64        *string               `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
	SkinURL           *string               `json:"skinUrl" example:"https://example.com/skin.png"`                                                       // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
	CapeBase64        *string               `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
	CapeURL           *string               `json:"capeUrl" example:"https://example.com/cape.png"`                                                       // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
	MaxPlayerCount    *int                  `json:"maxPlayerCount" example:"3"`                                                                           // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
}

type APICreateUserResponse struct {
	User     APIUser `json:"user"`                                                // The new user.
	APIToken *string `json:"apiToken,omitempty" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the new user, if requested.
}

// APICreateUser godoc
//
//	@Summary		Create a new user
//	@Description	Register and create a new user. Can be called without an API token.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Param			APICreateUserRequest	body		APICreateUserRequest	true	"Properties of the new user"
//	@Success		200						{object}	APICreateUserResponse
//	@Failure		400						{object}	APIError
//	@Failure		401						{object}	APIError
//	@Failure		403						{object}	APIError
//	@Failure		429						{object}	APIError
//	@Failure		500						{object}	APIError
//	@Router			/drasl/api/v2/users [post]
func (app *App) APICreateUser() func(c echo.Context) error {
	return app.withAPIToken(false, func(c echo.Context, caller *User) error {
		callerIsAdmin := caller != nil && caller.IsAdmin

		req := new(APICreateUserRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		var skinReader *io.Reader
		if req.SkinBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.SkinBase64))
			skinReader = &decoder
		}

		var capeReader *io.Reader
		if req.CapeBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.CapeBase64))
			capeReader = &decoder
		}

		if !callerIsAdmin && len(req.OIDCIdentitySpecs) > 0 {
			return NewBadRequestUserError("Can't create a user with OIDC identities without admin privileges.")
		}
		oidcIdentitySpecs := make([]OIDCIdentitySpec, 0, len(req.OIDCIdentitySpecs))
		for _, ois := range req.OIDCIdentitySpecs {
			oidcIdentitySpecs = append(oidcIdentitySpecs, OIDCIdentitySpec(ois))
		}

		user, err := app.CreateUser(
			caller,
			req.Username,
			req.Password,
			PotentiallyInsecure[[]OIDCIdentitySpec]{Value: oidcIdentitySpecs},
			req.IsAdmin,
			req.IsLocked,
			req.InviteCode,
			req.PreferredLanguage,
			req.PlayerName,
			req.ChosenUUID,
			req.ExistingPlayer,
			nil, // challengeToken
			req.FallbackPlayer,
			req.MaxPlayerCount,
			req.SkinModel,
			skinReader,
			req.SkinURL,
			capeReader,
			req.CapeURL,
		)
		if err != nil {
			return err
		}

		apiUser, err := app.userToAPIUser(&user)
		if err != nil {
			return err
		}
		var response APICreateUserResponse
		response.User = apiUser
		if req.RequestAPIToken {
			response.APIToken = &user.APIToken
		}
		return c.JSON(http.StatusOK, response)
	})
}

type APIUpdateUserRequest struct {
	Password            *string `json:"password" example:"hunter2"`          // Optional. New plaintext password
	IsAdmin             *bool   `json:"isAdmin" example:"true"`              // Optional. Pass`true` to grant, `false` to revoke admin privileges.
	IsLocked            *bool   `json:"isLocked" example:"false"`            // Optional. Pass `true` to lock (disable), `false` to unlock user.
	ResetAPIToken       bool    `json:"resetApiToken" example:"false"`       // Pass `true` to reset the user's API token
	ResetMinecraftToken bool    `json:"resetMinecraftToken" example:"false"` // Pass `true` to reset the user's Minecraft token
	PreferredLanguage   *string `json:"preferredLanguage" example:"en"`      // Optional. One of the two-letter codes in https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html. Used by Minecraft.
	MaxPlayerCount      *int    `json:"maxPlayerCount" example:"3"`          // Optional. Maximum number of players a user is allowed to own. -1 means unlimited players. -2 means use the default configured value.
}

// APIUpdateUser godoc
//
//	@Summary		Update a user
//	@Description	Update an existing user, either the calling user (PATCH /user) or the user with the specified UUID (PATCH /users/{uuid}). Updating another user requires admin privileges.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Param			uuid					path		string					true	"User UUID"
//	@Param			APIUpdateUserRequest	body		APIUpdateUserRequest	true	"New properties of the user"
//	@Success		200						{object}	APIUser
//	@Failure		400						{object}	APIError
//	@Failure		403						{object}	APIError
//	@Failure		404						{object}	APIError
//	@Failure		429						{object}	APIError
//	@Failure		500						{object}	APIError
//	@Router			/drasl/api/v2/users/{uuid} [patch]
//	@Router			/drasl/api/v2/user [patch]
func (app *App) APIUpdateUser() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		req := new(APIUpdateUserRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		targetUser := caller
		uuidParam := c.Param("uuid")
		if uuidParam != "" {
			if !caller.IsAdmin && (caller.UUID != uuidParam) {
				return NewUserErrorWithCode(http.StatusForbidden, "You are not authorized to update that user.")
			}

			_, err := uuid.Parse(uuidParam)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
			}

			var targetUserStruct User
			if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
				}
				return err
			}

			targetUser = &targetUserStruct
		}

		updatedUser, err := app.UpdateUser(
			app.DB,
			caller,
			*targetUser,
			req.Password,
			req.IsAdmin,
			req.IsLocked,
			req.ResetAPIToken,
			req.ResetMinecraftToken,
			req.PreferredLanguage,
			req.MaxPlayerCount,
		)
		if err != nil {
			return err
		}

		apiUser, err := app.userToAPIUser(&updatedUser)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiUser)
	})
}

// APIDeleteUser godoc
//
//	@Summary		Delete user
//	@Description	Delete a user, either the calling user (DELETE /user) or the user with the specified UUID (DELETE /users/{uuid}). This action cannot be undone. Deleting another user requires admin privileges.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Param			uuid	path	string	true	"User UUID"
//	@Success		204
//	@Failure		403	{object}	APIError
//	@Failure		404	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/user [delete]
//	@Router			/drasl/api/v2/users/{uuid} [delete]
func (app *App) APIDeleteUser() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		targetUser := caller
		uuidParam := c.Param("uuid")
		if uuidParam != "" {
			if !caller.IsAdmin && (caller.UUID != uuidParam) {
				return NewUserErrorWithCode(http.StatusForbidden, "You are not authorized to update that user.")
			}

			_, err := uuid.Parse(uuidParam)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
			}

			var targetUserStruct User
			if err := app.DB.First(&targetUserStruct, "uuid = ?", uuidParam).Error; err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
				}
				return err
			}

			targetUser = &targetUserStruct
		}

		err := app.DeleteUser(caller, targetUser)
		if err != nil {
			return err
		}

		return c.NoContent(http.StatusNoContent)
	})
}

// APIGetPlayer godoc
//
//	@Summary		Get player by UUID
//	@Description	Get details of a player by their UUID. Requires admin privileges unless you own the player.
//	@Tags			players
//	@Accept			json
//	@Produce		json
//	@Param			uuid	path		string	true	"Player UUID"
//	@Success		200		{object}	APIPlayer
//	@Failure		400		{object}	APIError
//	@Failure		401		{object}	APIError
//	@Failure		403		{object}	APIError
//	@Failure		404		{object}	APIError
//	@Failure		500		{object}	APIError
//	@Router			/drasl/api/v2/players/{uuid} [get]
func (app *App) APIGetPlayer() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, user *User) error {
		uuid_ := c.Param("uuid")
		_, err := uuid.Parse(uuid_)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
		}

		var player Player
		result := app.DB.Preload("User").First(&player, "uuid = ?", uuid_)
		if result.Error != nil {
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return echo.NewHTTPError(http.StatusNotFound, "Player not found.")
			}
			return result.Error
		}
		if !user.IsAdmin && (player.User.UUID != user.UUID) {
			return echo.NewHTTPError(http.StatusForbidden, "You don't own that player.")
		}

		apiPlayer, err := app.playerToAPIPlayer(&player)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiPlayer)
	})
}

// APIGetPlayers godoc
//
//	@Summary		Get players
//	@Description	Get details of all players. Requires admin privileges.
//	@Tags			players
//	@Accept			json
//	@Produce		json
//	@Success		200	{array}		APIPlayer
//	@Failure		401	{object}	APIError
//	@Failure		403	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/players [get]
func (app *App) APIGetPlayers() func(c echo.Context) error {
	return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
		var players []Player
		result := app.DB.Find(&players)
		if result.Error != nil {
			return result.Error
		}

		apiPlayers := make([]APIPlayer, 0, len(players))
		for _, player := range players {
			apiPlayer, err := app.playerToAPIPlayer(&player)
			if err != nil {
				return err
			}
			apiPlayers = append(apiPlayers, apiPlayer)
		}

		return c.JSON(http.StatusOK, apiPlayers)
	})
}

type APICreatePlayerRequest struct {
	Name           string  `json:"name" example:"MyPlayerName"`                                                                          // Player name.
	UserUUID       *string `json:"userUuid" example:"f9b9af62-da83-4ec7-aeea-de48c621822c"`                                              // Optional. UUID of the owning user. If omitted, the player will be added to the calling user's account.
	ChosenUUID     *string `json:"chosenUuid" example:"557e0c92-2420-4704-8840-a790ea11551c"`                                            // Optional. Specify a UUID for the new player. If omitted, a UUID will be generated according to the `PlayerUUIDGeneration` configuration option.
	ExistingPlayer bool    `json:"existingPlayer" example:"false"`                                                                       // If true, the new player will get the UUID of the existing player with the specified PlayerName. See `RegistrationExistingPlayer` in configuration.md.
	FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"`                                                                       // Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
	ChallengeToken *string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"`                                                      // Challenge token to use when verifying ownership of another player. Call /drasl/api/v2/challenge-skin first to get a skin and token. See `RequireSkinVerification` in configuration.md.
	SkinModel      *string `json:"skinModel" example:"classic"`                                                                          // Skin model. Either "classic" or "slim". If omitted, `"classic"` will be assumed.
	SkinBase64     *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity. Do not specify both `skinBase64` and `skinUrl`.
	SkinURL        *string `json:"skinUrl" example:"https://example.com/skin.png"`                                                       // Optional. URL to skin file. Do not specify both `skinBase64` and `skinUrl`.
	CapeBase64     *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity. Do not specify both `capeBase64` and `capeUrl`.
	CapeURL        *string `json:"capeUrl" example:"https://example.com/cape.png"`                                                       // Optional. URL to cape file. Do not specify both `capeBase64` and `capeUrl`.
}

// APICreatePlayer godoc
//
//	@Summary		Create a new player
//	@Description	Create a new player for an existing Drasl user.
//	@Tags			players
//	@Accept			json
//	@Produce		json
//	@Param			APICreatePlayerRequest	body		APICreatePlayerRequest	true	"Properties of the new player"
//	@Success		200						{object}	APIPlayer
//	@Failure		400						{object}	APIError
//	@Failure		401						{object}	APIError
//	@Failure		403						{object}	APIError
//	@Failure		500						{object}	APIError
//	@Router			/drasl/api/v2/players [post]
func (app *App) APICreatePlayer() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		req := new(APICreatePlayerRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		var skinReader *io.Reader
		if req.SkinBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.SkinBase64))
			skinReader = &decoder
		}

		var capeReader *io.Reader
		if req.CapeBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.CapeBase64))
			capeReader = &decoder
		}

		userUUID := caller.UUID
		if req.UserUUID != nil {
			userUUID = *req.UserUUID
		}

		player, err := app.CreatePlayer(
			caller,
			userUUID,
			req.Name,
			req.ChosenUUID,
			req.ExistingPlayer,
			req.ChallengeToken,
			req.FallbackPlayer,
			req.SkinModel,
			skinReader,
			req.SkinURL,
			capeReader,
			req.CapeURL,
		)

		if err != nil {
			return err
		}

		apiPlayer, err := app.playerToAPIPlayer(&player)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiPlayer)
	})
}

type APIUpdatePlayerRequest struct {
	Name           *string `json:"name" example:"MyPlayerName"`                                                                          // Optional. New player name. Can be different from the user's username.
	FallbackPlayer *string `json:"fallbackPlayer" example:"Notch"`                                                                       // Optional. New fallback player. Can be a UUID or a player name. If you don't set a skin or cape, this player's skin on one of the fallback API servers will be used instead.
	SkinModel      *string `json:"skinModel" example:"classic"`                                                                          // Optional. New skin model. Either "classic" or "slim".
	SkinBase64     *string `json:"skinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Optional. Base64-encoded skin PNG. Example value truncated for brevity.
	SkinURL        *string `json:"skinUrl" example:"https://example.com/skin.png"`                                                       // Optional. URL to skin file
	DeleteSkin     bool    `json:"deleteSkin"`                                                                                           // Pass `true` to delete the user's existing skin
	CapeBase64     *string `json:"capeBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAABcGlDQ1BpY2MAACiRdZG9S8NAGMaf..."` // Optional. Base64-encoded cape PNG. Example value truncated for brevity.
	CapeURL        *string `json:"capeUrl" example:"https://example.com/cape.png"`                                                       // Optional. URL to cape file
	DeleteCape     bool    `json:"deleteCape"`                                                                                           // Pass `true` to delete the user's existing cape
}

// APIUpdatePlayer godoc
//
//	@Summary		Update a player
//	@Description	Update an existing player. Requires admin privileges unless you own the player.
//	@Tags			players
//	@Accept			json
//	@Produce		json
//	@Param			uuid					path		string					true	"Player UUID"
//	@Param			APIUpdatePlayerRequest	body		APIUpdatePlayerRequest	true	"New properties of the player"
//	@Success		200						{object}	APIUser
//	@Failure		400						{object}	APIError
//	@Failure		403						{object}	APIError
//	@Failure		404						{object}	APIError
//	@Failure		429						{object}	APIError
//	@Failure		500						{object}	APIError
//	@Router			/drasl/api/v2/players/{uuid} [patch]
func (app *App) APIUpdatePlayer() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		req := new(APIUpdatePlayerRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		uuid_ := c.Param("uuid")
		_, err := uuid.Parse(uuid_)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
		}

		var player Player
		if err := app.DB.First(&player, "uuid = ?", uuid_).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return echo.NewHTTPError(http.StatusNotFound, "Unknown UUID")
			}
			return err
		}

		var skinReader *io.Reader
		if req.SkinBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.SkinBase64))
			skinReader = &decoder
		}

		var capeReader *io.Reader
		if req.CapeBase64 != nil {
			decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.CapeBase64))
			capeReader = &decoder
		}

		updatedPlayer, err := app.UpdatePlayer(
			caller,
			player,
			req.Name,
			req.FallbackPlayer,
			req.SkinModel,
			skinReader,
			req.SkinURL,
			req.DeleteSkin,
			capeReader,
			req.CapeURL,
			req.DeleteCape,
		)
		if err != nil {
			return err
		}

		apiPlayer, err := app.playerToAPIPlayer(&updatedPlayer)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiPlayer)
	})
}

// APIDeletePlayer godoc
//
//	@Summary		Delete player
//	@Description	Delete a player. This action cannot be undone. Requires admin privileges unless you own the player.
//	@Tags			players
//	@Accept			json
//	@Produce		json
//	@Param			uuid	path	string	true	"Player UUID"
//	@Success		204
//	@Failure		401	{object}	APIError
//	@Failure		403	{object}	APIError
//	@Failure		404	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/players/{uuid} [delete]
func (app *App) APIDeletePlayer() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, user *User) error {
		uuid_ := c.Param("uuid")
		_, err := uuid.Parse(uuid_)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
		}

		var player Player
		result := app.DB.First(&player, "uuid = ?", uuid_)
		if result.Error != nil {
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return echo.NewHTTPError(http.StatusNotFound, "Player not found.")
			}
			return err
		}
		err = app.DeletePlayer(user, &player)
		if err != nil {
			return err
		}

		return c.NoContent(http.StatusNoContent)
	})
}

type APICreateOIDCIdentityRequest struct {
	Issuer  string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
	Subject string `json:"subject" example:"f85f8c18-9bdf-49ad-a76e-719f9ba3ed25"`
}

// APICreateOIDCIdentity godoc
//
//	@Summary	Link an OIDC identity to a user
//	@Tags		users
//	@Accept		json
//	@Produce	json
//	@Param		uuid							path		string							true	"User UUID"
//	@Param		APICreateOIDCIdentityRequest	body		APICreateOIDCIdentityRequest	true	"OIDC identity to link to the user"
//	@Success	200								{object}	APIOIDCIdentity
//	@Failure	400								{object}	APIError
//	@Failure	401								{object}	APIError
//	@Failure	403								{object}	APIError
//	@Failure	404								{object}	APIError
//	@Failure	500								{object}	APIError
//	@Router		/drasl/api/v2/user/oidc-identities [post]
//	@Router		/drasl/api/v2/users/{uuid}/oidc-identities [post]
func (app *App) APICreateOIDCIdentity() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		req := new(APICreateOIDCIdentityRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		userUUID := caller.UUID
		uuidParam := c.Param("uuid")
		if uuidParam != "" {
			_, err := uuid.Parse(uuidParam)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
			}
			userUUID = uuidParam
		}

		oidcIdentity, err := app.CreateOIDCIdentity(caller, userUUID, req.Issuer, req.Subject)
		if err != nil {
			return err
		}

		apiOIDCIdentity, err := app.oidcIdentityToAPIOIDCIdentity(&oidcIdentity)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiOIDCIdentity)
	})
}

type APIDeleteOIDCIdentityRequest struct {
	Issuer string `json:"issuer" example:"https://idm.example.com/oauth2/openid/drasl"`
}

// APIDeleteOIDCIdentity godoc
//
//	@Summary	Unlink an OIDC identity from a user
//	@Tags		users
//	@Accept		json
//	@Produce	json
//	@Param		uuid							path	string							true	"User UUID"
//	@Param		APIDeleteOIDCIdentityRequest	body	APIDeleteOIDCIdentityRequest	true	"Issuer of the OIDC provider to unlink from the user"
//	@Success	204
//	@Failure	400	{object}	APIError
//	@Failure	401	{object}	APIError
//	@Failure	403	{object}	APIError
//	@Failure	404	{object}	APIError
//	@Failure	500	{object}	APIError
//	@Router		/drasl/api/v2/user/oidc-identities [delete]
//	@Router		/drasl/api/v2/users/{uuid}/oidc-identities [delete]
func (app *App) APIDeleteOIDCIdentity() func(c echo.Context) error {
	return app.withAPIToken(true, func(c echo.Context, caller *User) error {
		req := new(APIDeleteOIDCIdentityRequest)
		if err := c.Bind(req); err != nil {
			return err
		}

		userUUID := caller.UUID
		uuidParam := c.Param("uuid")
		if uuidParam != "" {
			_, err := uuid.Parse(uuidParam)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid UUID")
			}
			userUUID = uuidParam
		}

		oidcProvider, ok := app.OIDCProvidersByIssuer[req.Issuer]
		if !ok {
			return NewBadRequestUserError("Unknown OIDC provider: %s", req.Issuer)
		}

		err := app.DeleteOIDCIdentity(caller, userUUID, oidcProvider.Config.Name)
		if err != nil {
			return err
		}

		return c.NoContent(http.StatusNoContent)
	})
}

// APIGetInvites godoc
//
//	@Summary		Get invites
//	@Description	Get details of all invites. Requires admin privileges.
//	@Tags			invites
//	@Accept			json
//	@Produce		json
//	@Success		200	{array}		APIInvite
//	@Failure		403	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/invites [get]
func (app *App) APIGetInvites() func(c echo.Context) error {
	return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
		var invites []Invite
		result := app.DB.Find(&invites)
		if result.Error != nil {
			return result.Error
		}

		apiInvites := make([]APIInvite, 0, len(invites))
		for _, invite := range invites {
			apiInvite, err := app.inviteToAPIInvite(&invite)
			if err != nil {
				return err
			}
			apiInvites = append(apiInvites, apiInvite)
		}

		return c.JSON(http.StatusOK, apiInvites)
	})
}

// APICreateInvite godoc
//
//	@Summary		Create a new invite
//	@Description	Create a new invite with a random code. Requires admin privileges.
//	@Tags			invites
//	@Accept			json
//	@Produce		json
//	@Success		200	{object}	APIInvite
//	@Failure		403	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/invites [post]
func (app *App) APICreateInvite() func(c echo.Context) error {
	return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
		invite, err := app.CreateInvite()
		if err != nil {
			return err
		}
		apiInvite, err := app.inviteToAPIInvite(&invite)
		if err != nil {
			return err
		}
		return c.JSON(http.StatusOK, apiInvite)
	})
}

// APIDeleteInvite godoc
//
//	@Summary		Delete an invite
//	@Description	Delete an invite given its code. Requires admin privileges.
//	@Tags			invites
//	@Accept			json
//	@Produce		json
//	@Param			code	path	string	true	"Invite code"
//	@Success		204
//	@Failure		403	{object}	APIError
//	@Failure		404	{object}	APIError
//	@Failure		500	{object}	APIError
//	@Router			/drasl/api/v2/invites/{code} [delete]
func (app *App) APIDeleteInvite() func(c echo.Context) error {
	return app.withAPITokenAdmin(func(c echo.Context, user *User) error {
		code := c.Param("code")

		result := app.DB.Where("code = ?", code).Delete(&Invite{})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return NewUserErrorWithCode(http.StatusNotFound, "Unknown invite code")
		}

		return c.NoContent(http.StatusNoContent)
	})
}

type APIGetChallengeSkinRequest struct {
	PlayerName string `json:"playerName" example:"Notch"`
}

type APIChallenge struct {
	ChallengeSkinBase64 string `json:"challengeSkinBase64" example:"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgI..."` // Base64-encoded skin PNG. Example value truncated for brevity.
	ChallengeToken      string `json:"challengeToken" example:"iK1B2FzLc5fMP94VmUR3KC"`                                                               // Challenge token that must be passed when registering with a challenge skin
}

// APIGetChallengeSkin godoc
//
//	@Summary		Get a challenge skin/token
//	@Description	Get a challenge skin and challenge token for a player name for registration or player creation purposes. See the `ImportExistingPlayer.RequireSkinVerification` configuration option.
//	@Tags			users, players
//	@Accept			json
//	@Produce		json
//	@Param			APIGetChallengeSkinRequest	body		APIGetChallengeSkinRequest	true	"Player name for the challenge skin"
//	@Success		200							{object}	APIChallenge
//	@Success		400							{object}	APIError
//	@Failure		500							{object}	APIError
//	@Router			/drasl/api/v2/challenge-skin [get]
func (app *App) APIGetChallengeSkin() func(c echo.Context) error {
	return app.withAPIToken(false, func(c echo.Context, _ *User) error {
		req := new(APIGetChallengeSkinRequest)
		if err := c.Bind(req); err != nil {
			return err
		}
		playerName := req.PlayerName

		challengeToken, err := MakeChallengeToken()
		if err != nil {
			return err
		}

		challengeSkinBytes, err := app.GetChallengeSkin(playerName, challengeToken)
		if err != nil {
			return err
		}
		challengeSkinBase64 := base64.StdEncoding.EncodeToString(challengeSkinBytes)

		return c.JSON(http.StatusOK, APIChallenge{
			ChallengeSkinBase64: challengeSkinBase64,
			ChallengeToken:      challengeToken,
		})
	})
}

type APILoginResponse struct {
	User     APIUser `json:"user"`                                      // The logged-in user
	APIToken string  `json:"apiToken" example:"Bq608AtLeG7emJOdvXHYxL"` // An API token for the user
}

type APILoginRequest struct {
	Username string `json:"username" example:"Notch"`
	Password string `json:"password" example:"hunter2"`
}

// APILogin godoc
//
//	@Summary		Authenticate and receive an API token
//	@Description	Authenticate with username and password and receive an API token. Can be called without an API token.
//	@Tags			users
//	@Accept			json
//	@Produce		json
//	@Param			APILoginRequest	body		APILoginRequest	true	"Drasl username and password"
//	@Success		200				{object}	APILoginResponse
//	@Failure		400				{object}	APIError
//	@Failure		401				{object}	APIError
//	@Failure		403				{object}	APIError
//	@Failure		429				{object}	APIError
//	@Failure		500				{object}	APIError
//	@Router			/drasl/api/v2/login [post]
func (app *App) APILogin() func(c echo.Context) error {
	return app.withAPIToken(false, func(c echo.Context, _ *User) error {
		var req APILoginRequest
		err := c.Bind(&req)
		if err != nil {
			return err
		}

		user, err := app.AuthenticateUser(req.Username, req.Password)
		if err != nil {
			return err
		}

		apiUser, err := app.userToAPIUser(&user)
		if err != nil {
			return err
		}

		return c.JSON(http.StatusOK, APILoginResponse{User: apiUser, APIToken: user.APIToken})
	})
}
