import { RecipeDto } from "../api/dtos/RecipeDto.js";
import { RecipeDtoEntityMapper } from "../mappers/RecipeDtoEntityMapper.js";
import { RecipeRepository } from "../repositories/RecipeRepository.js";
import { NotFoundError, ValidationError } from "../api/errors/httpErrors.js";
import { RecipeInstructionStepDto } from "../api/dtos/RecipeInstructionStepDto.js";
import { RecipeIngredientGroupDto } from "../api/dtos/RecipeIngredientGroupDto.js";
import { RecipeIngredientDto } from "../api/dtos/RecipeIngredientDto.js";
import { RecipeEntity } from "../entities/RecipeEntity.js";

/**
 * Controls all recipe specific actions
 */
export class RecipeHandler {
    constructor(
        private recipeRepository: RecipeRepository,
        private mapper: RecipeDtoEntityMapper
    ) { }

    /**
     * Load a specific recipe
     * @param id recipe id
     * @returns RecipeDto for requested recipe
     */
    async getRecipeById(id: string){
        const recipeEntity = await this.recipeRepository.findById(id);
        if(recipeEntity === null){
            throw new NotFoundError("recipe with id " + id + " not found!")
        }
        const recipeDto = this.mapper.toDto(recipeEntity);
        return recipeDto;
    }

    /**
     * Save or update recipe depending on whether the DTO contains an ID
     * @param dto recipe data
     * @returns Recipe as saved in the database
     */
    async createOrUpdateRecipe(dto: RecipeDto){
        if (!this.isRecipeDtoValid(dto)) {
            throw new ValidationError("recipe data is not valid!")
        }
        var savedEntity: RecipeEntity;
        const recipeId = dto.id
        if(recipeId === undefined || recipeId.length === 0){
            // create new recipe
            const recipeEntity = this.mapper.toEntity(dto)
            delete (recipeEntity as any).id;
            savedEntity = await this.recipeRepository.create(recipeEntity);
        } else {
            // save existing Recipe
            // First: Load current version of recipe from database
            const recipeEntity = await this.recipeRepository.findById(recipeId);
            if(!recipeEntity){
                throw new ValidationError("No recipe with ID " + recipeId + " found in database!")
            }
            // merge changes into entity
            this.mapper.mergeDtoIntoEntity(dto, recipeEntity);
            // persist changes
            savedEntity = await this.recipeRepository.update(recipeEntity);
        }
        return this.mapper.toDto(savedEntity);
    }
    /**
     * Update recipe data
     * @param dto containing the entire updated recipe
     * @returns Up-to-date RecipeDto as saved in the database
     */
    async updateRecipe(dto: RecipeDto){
        if (!this.isRecipeDtoValid(dto)) {
            throw new ValidationError("recipe data is not valid!")
        }
        const recipeId = dto.id
        if(recipeId === undefined){
            throw new ValidationError("Trying to update recipe without ID!")
        }
        // Load current version of recipe from database
        const recipeEntity = await this.recipeRepository.findById(recipeId);
        if(!recipeEntity){
            throw new ValidationError("No recipe with ID " + recipeId + " found in database!")
        }
        // merge changes into entity
        this.mapper.mergeDtoIntoEntity(dto, recipeEntity);
        // persist changes
        const savedEntity = await this.recipeRepository.update(recipeEntity);
        return this.mapper.toDto(savedEntity);
    }
    /**
     * Create a new recipe
     * @param dto RecipeDto containing the new recipe
     */
    async createRecipe(dto: RecipeDto) {
        if (!this.isRecipeDtoValid(dto)) {
            throw new ValidationError("recipe data is not valid!")
        }
        const recipeEntity = this.mapper.toEntity(dto)
        const savedEntity = await this.recipeRepository.create(recipeEntity);
        return this.mapper.toDto(savedEntity);
    }

    /**
     * Validates a recipe.
     * A recipe must have a non-empty title, servings info and valid instructions and ingredients
     * @param dto RecipeDTO
     * @returns true if the recipe is valid
     */
    private isRecipeDtoValid(dto: RecipeDto): boolean {
        
        return dto.title !==  undefined
            && dto.title.length !== 0
            && dto.amount !== undefined
            && dto.amountDescription !== undefined
            && dto.amountDescription.length !== 0
            && this.isInstructionsValid(dto.instructions)
            && this.isIngredientGroupsValid(dto.ingredientGroups);
    }

    /**
     * Validates the ingredient groups if a recipe - each group must contain a valid ingredient list
     * @param ingredientGroups Array of ingredient groups
     * @returns true if all ingredient groups are valid
     */
    private isIngredientGroupsValid(ingredientGroups: RecipeIngredientGroupDto[] | undefined): boolean {
        return ingredientGroups !== undefined
            && ingredientGroups.length !== 0
            && ingredientGroups.every(group => {
            return (
                group.sortOrder !== undefined
                && this.isIngredientsValid(group.ingredients)
            );
        });
    }
    /**
     * Validates an ingredient list ensuring that it is present, contains at least one ingredient and
     * that all ingredients in the list are valid
     * @param ingredients Array if ingredients
     * @returns true if the ingredient list is valid
     */
    private isIngredientsValid(ingredients: RecipeIngredientDto[] | undefined): boolean {
        return ingredients !== undefined
            && ingredients.length !== 0
            && ingredients.every(ingredient => {
            return this.isIngredientValid(ingredient);
        });
    }
    /**
     * Validates an ingredient - An ingredient must have a name and a sortOrder
     * @param ingredient RecipeIngredientDto
     * @returns true if the ingredient is valid
     */
    private isIngredientValid(ingredient: RecipeIngredientDto): boolean {
        return ingredient !== null 
            && ingredient.sortOrder !== undefined
            && ingredient.name !== undefined
            && ingredient.name.length !== 0
    }
    // @todo create instruction handler/controller for validation?
    /**
     * Validates instructions
     * The list must be present and must contain at least one value with a non-empty
     * text field and sort order
     * @param instructions Array if instruction step DTOs
     * @returns Boolean indicating whether the instruction steps are valid
     */
    private isInstructionsValid(instructions: RecipeInstructionStepDto[] | undefined): boolean {
        return instructions !== undefined
            && instructions.length !== 0
            && instructions.every(step => {
            return this.isInstructionStepValid(step);
        });
    }

    /**
     * Validates a single instruction step. Each step must have a well-defined non-empty text
     * and a sort order
     * @param step InstructionStepDto describing a single step of the recipe
     * @returns true if the step is valid
     */
    private isInstructionStepValid(step: RecipeInstructionStepDto): boolean {
        return step !== null
            && step.text !== undefined
            && step.text.length !== 0
            && step.sortOrder !== undefined;
    }
}