package app

import (
	"errors"
	"fmt"

	"github.com/mattermost/focalboard/server/model"
	"github.com/mattermost/focalboard/server/utils"
)

var errCategoryNotFound = errors.New("category ID specified in input does not exist for user")
var errCategoriesLengthMismatch = errors.New("cannot update category order, passed list of categories different size than in database")
var ErrCannotDeleteSystemCategory = errors.New("cannot delete a system category")
var ErrCannotUpdateSystemCategory = errors.New("cannot update a system category")

func (a *App) GetCategory(categoryID string) (*model.Category, error) {
	return a.store.GetCategory(categoryID)
}

func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
	category.Hydrate()
	if err := category.IsValid(); err != nil {
		return nil, err
	}

	if err := a.store.CreateCategory(*category); err != nil {
		return nil, err
	}

	createdCategory, err := a.store.GetCategory(category.ID)
	if err != nil {
		return nil, err
	}

	go func() {
		a.wsAdapter.BroadcastCategoryChange(*createdCategory)
	}()

	return createdCategory, nil
}

func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) {
	category.Hydrate()

	if err := category.IsValid(); err != nil {
		return nil, err
	}

	// verify if category belongs to the user
	existingCategory, err := a.store.GetCategory(category.ID)
	if err != nil {
		return nil, err
	}

	if existingCategory.DeleteAt != 0 {
		return nil, model.ErrCategoryDeleted
	}

	if existingCategory.UserID != category.UserID {
		return nil, model.ErrCategoryPermissionDenied
	}

	if existingCategory.TeamID != category.TeamID {
		return nil, model.ErrCategoryPermissionDenied
	}

	// in case type was defaulted above, set to existingCategory.Type
	category.Type = existingCategory.Type
	if existingCategory.Type == model.CategoryTypeSystem {
		// You cannot rename or delete a system category,
		// So restoring its name and undeleting it if set so.
		category.Name = existingCategory.Name
		category.DeleteAt = 0
	}

	category.UpdateAt = utils.GetMillis()
	if err = category.IsValid(); err != nil {
		return nil, err
	}
	if err = a.store.UpdateCategory(*category); err != nil {
		return nil, err
	}

	updatedCategory, err := a.store.GetCategory(category.ID)
	if err != nil {
		return nil, err
	}

	go func() {
		a.wsAdapter.BroadcastCategoryChange(*updatedCategory)
	}()

	return updatedCategory, nil
}

func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) {
	existingCategory, err := a.store.GetCategory(categoryID)
	if err != nil {
		return nil, err
	}

	// category is already deleted. This avoids
	// overriding the original deleted at timestamp
	if existingCategory.DeleteAt != 0 {
		return existingCategory, nil
	}

	// verify if category belongs to the user
	if existingCategory.UserID != userID {
		return nil, model.ErrCategoryPermissionDenied
	}

	// verify if category belongs to the team
	if existingCategory.TeamID != teamID {
		return nil, model.NewErrInvalidCategory("category doesn't belong to the team")
	}

	if existingCategory.Type == model.CategoryTypeSystem {
		return nil, ErrCannotDeleteSystemCategory
	}

	if err = a.moveBoardsToDefaultCategory(userID, teamID, categoryID); err != nil {
		return nil, err
	}

	if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
		return nil, err
	}

	deletedCategory, err := a.store.GetCategory(categoryID)
	if err != nil {
		return nil, err
	}

	go func() {
		a.wsAdapter.BroadcastCategoryChange(*deletedCategory)
	}()

	return deletedCategory, nil
}

func (a *App) moveBoardsToDefaultCategory(userID, teamID, sourceCategoryID string) error {
	// we need a list of boards associated to this category
	// so we can move them to user's default Boards category
	categoryBoards, err := a.GetUserCategoryBoards(userID, teamID)
	if err != nil {
		return err
	}

	var sourceCategoryBoards *model.CategoryBoards
	defaultCategoryID := ""

	// iterate user's categories to find the source category
	// and the default category.
	// We need source category to get the list of its board
	// and the default category to know its ID to
	// move source category's boards to.
	for i := range categoryBoards {
		if categoryBoards[i].ID == sourceCategoryID {
			sourceCategoryBoards = &categoryBoards[i]
		}

		if categoryBoards[i].Name == defaultCategoryBoards {
			defaultCategoryID = categoryBoards[i].ID
		}

		// if both categories are found, no need to iterate furthur.
		if sourceCategoryBoards != nil && defaultCategoryID != "" {
			break
		}
	}

	if sourceCategoryBoards == nil {
		return errCategoryNotFound
	}

	if defaultCategoryID == "" {
		return fmt.Errorf("moveBoardsToDefaultCategory: %w", errNoDefaultCategoryFound)
	}

	boardIDs := make([]string, len(sourceCategoryBoards.BoardMetadata))
	for i := range sourceCategoryBoards.BoardMetadata {
		boardIDs[i] = sourceCategoryBoards.BoardMetadata[i].BoardID
	}

	if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil {
		return fmt.Errorf("moveBoardsToDefaultCategory: %w", err)
	}

	return nil
}

func (a *App) ReorderCategories(userID, teamID string, newCategoryOrder []string) ([]string, error) {
	if err := a.verifyNewCategoriesMatchExisting(userID, teamID, newCategoryOrder); err != nil {
		return nil, err
	}

	newOrder, err := a.store.ReorderCategories(userID, teamID, newCategoryOrder)
	if err != nil {
		return nil, err
	}

	go func() {
		a.wsAdapter.BroadcastCategoryReorder(teamID, userID, newOrder)
	}()

	return newOrder, nil
}

func (a *App) verifyNewCategoriesMatchExisting(userID, teamID string, newCategoryOrder []string) error {
	existingCategories, err := a.store.GetUserCategories(userID, teamID)
	if err != nil {
		return err
	}

	if len(newCategoryOrder) != len(existingCategories) {
		return fmt.Errorf(
			"%w length new categories: %d, length existing categories: %d, userID: %s, teamID: %s",
			errCategoriesLengthMismatch,
			len(newCategoryOrder),
			len(existingCategories),
			userID,
			teamID,
		)
	}

	existingCategoriesMap := map[string]bool{}
	for _, category := range existingCategories {
		existingCategoriesMap[category.ID] = true
	}

	for _, newCategoryID := range newCategoryOrder {
		if _, found := existingCategoriesMap[newCategoryID]; !found {
			return fmt.Errorf(
				"%w specified category ID: %s, userID: %s, teamID: %s",
				errCategoryNotFound,
				newCategoryID,
				userID,
				teamID,
			)
		}
	}

	return nil
}
