package service

import (
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
	domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
	domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
)

// RecipeService implements the domain.RecipeService defined in the domain module.
type RecipeService struct {
	recipeRepository     domain.RecipeRepository
	engagementRepository domainEngagement.EngagementRepository
}

// Compile-time check to ensure the RecipeService implements domain.RecipeService
var _ domain.RecipeService = (*RecipeService)(nil)

// NewRecipeService creates a user service object which can be passed into the context. The service
// requires a recipe repository which it will use to hit the database when needed.
func NewRecipeService(recipeRepository domain.RecipeRepository, engagementRepository domainEngagement.EngagementRepository) domain.RecipeService {
	return &RecipeService{
		recipeRepository:     recipeRepository,
		engagementRepository: engagementRepository,
	}
}

// CreateRecipe creates a recipe in the database using the recipe repository. This function requires
// all the data to be present, though validation does not occur in this function. However, the UI
// will enforce validation, as will the database. Errors will be returned to the called when they
// occur.
//
// TODO: Implement validation in the API.
// TODO: Implement image creation.
func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
	// Ensure user is logged in
	if !domainServer.IsLoggedIn(ctx) {
		return nil, fmt.Errorf("User is not logged in.")
	}

	title := ctx.PostForm("title")
	description := ctx.PostForm("description")
	preparation := ctx.PostForm("preparation-time")
	cook := ctx.PostForm("cook-time")
	serving := ctx.PostForm("serving-size")
	category := ctx.PostForm("category")
	difficulty := ctx.PostForm("difficulty")
	ingredients := ctx.PostFormArray("ingredients")
	quantity := ctx.PostFormArray("quantity")
	instructions := ctx.PostFormArray("instructions")
	tags := strings.Split(ctx.PostForm("tags"), ",")
	userId := ctx.MustGet("userId").(int)

	// Have to get the image differently
	image, err := ctx.FormFile("image")
	if err != nil && !errors.Is(err, http.ErrMissingFile) {
		// Error getting image
	}

	// Convert to proper values
	servingInt, _ := strconv.Atoi(serving)
	difficultyInt, _ := strconv.Atoi(difficulty)
	prepInt, _ := strconv.Atoi(preparation)
	cookInt, _ := strconv.Atoi(cook)

	var ingredientSlice []domain.RecipeIngredient
	for i := range len(ingredients) {
		if strings.TrimSpace(ingredients[i]) != "" {
			ins := domain.RecipeIngredient{
				Name:     ingredients[i],
				Quantity: quantity[i],
			}

			ingredientSlice = append(ingredientSlice, ins)
		}
	}

	var instructionSlice []string
	for _, ins := range instructions {
		if ins != "" {
			instructionSlice = append(instructionSlice, ins)
		}
	}

	// Create the recipe
	recipe := domain.Recipe{
		Title:        title,
		Description:  description,
		Instructions: instructionSlice,
		Serves:       servingInt,
		Difficulty:   difficultyInt,
		Duration: domain.RecipeDuration{
			Total: prepInt + cookInt,
			Prep:  prepInt,
			Cook:  cookInt,
		},
		Category:    domain.RecipeMeal(category),
		Ingredients: ingredientSlice,
		UserId:      userId,
		Created:     time.Now(),
	}

	if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
		return &recipe, err
	}

	// TODO: Upload the image
	if image != nil {
	}

	// Create the tags
	if len(tags) > 0 {
		if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil {
			return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
		}
	}

	return &recipe, nil
}

// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
// if the recipe is nil, an error will be returned, so the caller does not need to check for a nil
// recipe (e.g., if the error is nil the recipe exists)
//
// A userId should be provided to allow the favorite status to be updated. Without a userId (nil),
// the favorite status will return false, not because its not a favorite, but because it cannot find
// out!
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
	recipe, err := s.recipeRepository.GetRecipe(id, userId)

	if recipe == nil {
		return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.")
	}

	return recipe, err
}

// SearchRecipes will search the database using the filters provided. The recipes can be passed into
// a template and displayed in the UI as the search result. A more detailed definition of the
// filters is provided below.
//
// Each input is given a bit value (e.g., 00001 for 1) and will be passed
// back to this handler as an array. The values are then added together
// and will result in a integer which represents bit values. These bits
// can then be passed to the repository and are then parsed to determine
// which filters should be applied.
// Parsing these is simple, for each filter option, use the bitwise and (&)
// operator with the value we expect for the filter. When 1, we can ensure
// the filter is provided.
// A function `isBitActive` in the recipe repository provides an example of
// testing of testing the filter parsing.
//
// The favorites parameter is used to only return filters favorited by the userId provided.
func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
	return s.recipeRepository.SearchRecipes(filters, userId, favorites)
}

// GetUserRecipes returns a list of the recipes that the user has created. The user's
// ID should be provided. Any errors will be bubbled to the caller.
func (s *RecipeService) GetUserRecipes(id int) ([]domain.Recipe, error) {
	return s.recipeRepository.GetUserRecipes(id)
}

// GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a
// favorite. The user's ID should be provided. Any errors will be bubbled to the caller.
func (s *RecipeService) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
	return s.recipeRepository.GetUserFavoriteRecipes(id)
}

// GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from
// the provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to
// the caller.
func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe, error) {
	engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementViewed)
	if err != nil {
		return nil, err
	}

	ids := make([]int, len(engagement))
	for i, eng := range engagement {
		ids[i] = eng.Entity
	}

	return s.recipeRepository.GetRecipes(ids, &userId)
}

// GetUserMadeRecipes returns a list of the most recent x (limit) recipes made by a user, from the
// provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to the
// caller.
func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, error) {
	engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementMade)
	if err != nil {
		return nil, err
	}

	ids := make([]int, len(engagement))
	for i, eng := range engagement {
		ids[i] = eng.Entity
	}

	return s.recipeRepository.GetRecipes(ids, &userId)
}

// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
// the recipe will be nil. Any errors will be bubbled to the caller.
func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
	return s.recipeRepository.GetRecipeOfTheWeek(userId)
}
