package main

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"net/url"

	mapset "github.com/deckarep/golang-set/v2"
	"github.com/labstack/echo/v5"
	"github.com/samber/mo"
)

type authlibInjectorLinks struct {
	Homepage string `json:"homepage"`
	Register string `json:"register"`
}

type authlibInjectorMeta struct {
	ImplementationName      string               `json:"implementationName"`
	ImplementationVersion   string               `json:"implementationVersion"`
	Links                   authlibInjectorLinks `json:"links"`
	ServerName              string               `json:"serverName"`
	FeatureEnableProfileKey bool                 `json:"feature.enable_profile_key"`
	FeatureNonEmailLogin    bool                 `json:"feature.non_email_login"`
}

type authlibInjectorResponse struct {
	Meta               authlibInjectorMeta `json:"meta"`
	SignaturePublickey string              `json:"signaturePublickey"`
	SkinDomains        []string            `json:"skinDomains"`
}

func authlibInjectorSerializeKey(key *rsa.PublicKey) (string, error) {
	pubDER, err := x509.MarshalPKIXPublicKey(key)
	if err != nil {
		return "", err
	}
	pubPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pubDER,
	})
	return string(pubPEM[:]), nil
}

func AuthlibInjectorRoot(app *App) func(c *echo.Context) error {
	skinDomains := mapset.NewSet[string]()
	skinDomains.Add(app.Config.Domain)
	for _, fallbackAPIServer := range app.FallbackAPIServers {
		skinDomains = skinDomains.Union(fallbackAPIServer.SkinDomains)
	}

	signaturePublicKey, err := authlibInjectorSerializeKey(&app.PrivateKey.PublicKey)
	Check(err)

	responseBlob := Unwrap(json.Marshal(authlibInjectorResponse{
		Meta: authlibInjectorMeta{
			ImplementationName:    "Drasl",
			ImplementationVersion: Constants.Version,
			Links: authlibInjectorLinks{
				Homepage: app.FrontEndURL,
				Register: Unwrap(url.JoinPath(app.FrontEndURL, "web/registration")),
			},
			ServerName:              app.Config.InstanceName,
			FeatureEnableProfileKey: true,
			FeatureNonEmailLogin:    true,
		},
		SignaturePublickey: signaturePublicKey,
		SkinDomains:        skinDomains.ToSlice(),
	}))

	return func(c *echo.Context) error {
		return c.JSONBlob(http.StatusOK, responseBlob)
	}
}

func (app *App) AuthlibInjectorUploadTexture(textureType string) func(c *echo.Context) error {
	return func(c *echo.Context) error {
		caller := c.Get(CONTEXT_KEY_USER).(*User)
		playerID := c.Param("id")
		playerUUID, err := IDToUUID(playerID)
		if err != nil {
			return &YggdrasilError{Code: http.StatusBadRequest, ErrorMessage: mo.Some("Invalid UUID format")}
		}

		textureFile, err := c.FormFile("file")
		if err != nil {
			return &YggdrasilError{Code: http.StatusBadRequest, ErrorMessage: mo.Some("Missing texture file")}
		}
		textureHandle, err := textureFile.Open()
		if err != nil {
			return err
		}
		defer textureHandle.Close()
		var textureReader io.Reader = textureHandle

		var targetPlayer Player
		result := app.DB.Preload("User").First(&targetPlayer, "uuid = ?", playerUUID)
		if result.Error != nil {
			return &YggdrasilError{Code: http.StatusBadRequest, ErrorMessage: mo.Some("Player not found")}
		}

		switch textureType {
		case TextureTypeSkin:
			var model string
			switch m := c.FormValue("model"); m {
			case "slim":
				model = SkinModelSlim
			case "":
				model = SkinModelClassic
			default:
				message := fmt.Sprintf("Unknown model: %s", m)
				return &YggdrasilError{Code: http.StatusBadRequest, ErrorMessage: mo.Some(message)}
			}
			_, err = app.UpdatePlayer(
				caller,
				targetPlayer,
				nil,            // playerName
				nil,            // fallbackPlayer
				&model,         // skinModel
				&textureReader, // skinReader
				nil,            // skinURL
				false,          // deleteSkin
				nil,            // capeReader
				nil,            // capeURL
				false,          // deleteCape
			)
			if err != nil {
				return err
			}
		case TextureTypeCape:
			_, err = app.UpdatePlayer(
				caller,
				targetPlayer,
				nil,            // playerName
				nil,            // fallbackPlayer
				nil,            // skinModel
				nil,            // skinReader
				nil,            // skinURL
				false,          // deleteSkin
				&textureReader, // capeReader
				nil,            // capeURL
				false,          // deleteCape
			)
			if err != nil {
				return err
			}
		}
		return c.NoContent(http.StatusNoContent)
	}
}

func (app *App) AuthlibInjectorDeleteTexture(textureType string) func(c *echo.Context) error {
	return func(c *echo.Context) error {
		caller := c.Get(CONTEXT_KEY_USER).(*User)
		playerID := c.Param("id")
		playerUUID, err := IDToUUID(playerID)
		if err != nil {
			return &YggdrasilError{Code: http.StatusBadRequest, ErrorMessage: mo.Some("Invalid player UUID")}
		}

		var targetPlayer Player
		result := app.DB.Preload("User").First(&targetPlayer, "uuid = ?", playerUUID)
		if result.Error != nil {
			return &YggdrasilError{Code: http.StatusNotFound, ErrorMessage: mo.Some("Player not found")}
		}

		_, err = app.UpdatePlayer(
			caller,
			targetPlayer,
			nil,                            // playerName
			nil,                            // fallbackPlayer
			nil,                            // skinModel
			nil,                            // skinReader
			nil,                            // skinURL
			textureType == TextureTypeSkin, // deleteSkin
			nil,                            // capeReader
			nil,                            // capeURL
			textureType == TextureTypeCape, // deleteCape
		)
		if err != nil {
			return err
		}

		return c.NoContent(http.StatusNoContent)
	}
}
