package v1

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/store"
)

const (
	// LocalStorage means the storage service is local file system.
	LocalStorage int32 = -1
	// DatabaseStorage means the storage service is database.
	DatabaseStorage int32 = 0
	// Default storage service is database.
	DefaultStorage int32 = DatabaseStorage
)

type StorageType string

const (
	StorageS3 StorageType = "S3"
)

func (t StorageType) String() string {
	return string(t)
}

type StorageConfig struct {
	S3Config *StorageS3Config `json:"s3Config"`
}

type StorageS3Config struct {
	EndPoint  string `json:"endPoint"`
	Path      string `json:"path"`
	Region    string `json:"region"`
	AccessKey string `json:"accessKey"`
	SecretKey string `json:"secretKey"`
	Bucket    string `json:"bucket"`
	URLPrefix string `json:"urlPrefix"`
	URLSuffix string `json:"urlSuffix"`
	PreSign   bool   `json:"presign"`
}

type Storage struct {
	ID     int32          `json:"id"`
	Name   string         `json:"name"`
	Type   StorageType    `json:"type"`
	Config *StorageConfig `json:"config"`
}

type CreateStorageRequest struct {
	Name   string         `json:"name"`
	Type   StorageType    `json:"type"`
	Config *StorageConfig `json:"config"`
}

type UpdateStorageRequest struct {
	Type   StorageType    `json:"type"`
	Name   *string        `json:"name"`
	Config *StorageConfig `json:"config"`
}

func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
	g.GET("/storage", s.GetStorageList)
	g.POST("/storage", s.CreateStorage)
	g.PATCH("/storage/:storageId", s.UpdateStorage)
	g.DELETE("/storage/:storageId", s.DeleteStorage)
}

// GetStorageList godoc
//
//	@Summary	Get a list of storages
//	@Tags		storage
//	@Produce	json
//	@Success	200	{object}	[]store.Storage	"List of storages"
//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
//	@Failure	500	{object}	nil				"Failed to find user | Failed to convert storage"
//	@Router		/api/v1/storage [GET]
func (s *APIV1Service) GetStorageList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	// We should only show storage list to host user.
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
	}

	storageList := []*Storage{}
	for _, storage := range list {
		storageMessage, err := ConvertStorageFromStore(storage)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
		}
		storageList = append(storageList, storageMessage)
	}
	return c.JSON(http.StatusOK, storageList)
}

// CreateStorage godoc
//
//	@Summary	Create storage
//	@Tags		storage
//	@Accept		json
//	@Produce	json
//	@Param		body	body		CreateStorageRequest	true	"Request object."
//	@Success	200		{object}	store.Storage			"Created storage"
//	@Failure	400		{object}	nil						"Malformatted post storage request"
//	@Failure	401		{object}	nil						"Missing user in session"
//	@Failure	500		{object}	nil						"Failed to find user | Failed to create storage | Failed to convert storage"
//	@Router		/api/v1/storage [POST]
func (s *APIV1Service) CreateStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	create := &CreateStorageRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
	}

	configString := ""
	if create.Type == StorageS3 && create.Config.S3Config != nil {
		configBytes, err := json.Marshal(create.Config.S3Config)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
		}
		configString = string(configBytes)
	}

	storage, err := s.Store.CreateStorage(ctx, &store.Storage{
		Name:   create.Name,
		Type:   create.Type.String(),
		Config: configString,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
	}
	storageMessage, err := ConvertStorageFromStore(storage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, storageMessage)
}

// DeleteStorage godoc
//
//	@Summary	Delete a storage
//	@Tags		storage
//	@Produce	json
//	@Param		storageId	path		int		true	"Storage ID"
//	@Success	200			{boolean}	true	"Storage deleted"
//	@Failure	400			{object}	nil		"ID is not a number: %s | Storage service %d is using"
//	@Failure	401			{object}	nil		"Missing user in session | Unauthorized"
//	@Failure	500			{object}	nil		"Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
//	@Router		/api/v1/storage/{storageId} [DELETE]
//
// NOTES:
// - error message "Storage service %d is using" probably should be "Storage service %d is in use".
func (s *APIV1Service) DeleteStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
	}

	systemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
	}
	if systemSetting != nil {
		storageServiceID := DefaultStorage
		err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
		}
		if storageServiceID == storageID {
			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
		}
	}

	if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// UpdateStorage godoc
//
//	@Summary	Update a storage
//	@Tags		storage
//	@Produce	json
//	@Param		storageId	path		int						true	"Storage ID"
//	@Param		patch		body		UpdateStorageRequest	true	"Patch request"
//	@Success	200			{object}	store.Storage			"Updated resource"
//	@Failure	400			{object}	nil						"ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
//	@Failure	401			{object}	nil						"Missing user in session | Unauthorized"
//	@Failure	500			{object}	nil						"Failed to find user | Failed to patch storage | Failed to convert storage"
//	@Router		/api/v1/storage/{storageId} [PATCH]
func (s *APIV1Service) UpdateStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
	}

	update := &UpdateStorageRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
	}
	storageUpdate := &store.UpdateStorage{
		ID: storageID,
	}
	if update.Name != nil {
		storageUpdate.Name = update.Name
	}
	if update.Config != nil {
		if update.Type == StorageS3 {
			configBytes, err := json.Marshal(update.Config.S3Config)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
			}
			configString := string(configBytes)
			storageUpdate.Config = &configString
		}
	}

	storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
	}
	storageMessage, err := ConvertStorageFromStore(storage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, storageMessage)
}

func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
	storageMessage := &Storage{
		ID:     storage.ID,
		Name:   storage.Name,
		Type:   StorageType(storage.Type),
		Config: &StorageConfig{},
	}
	if storageMessage.Type == StorageS3 {
		s3Config := &StorageS3Config{}
		if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
			return nil, err
		}
		storageMessage.Config = &StorageConfig{
			S3Config: s3Config,
		}
	}
	return storageMessage, nil
}
