package dev.fyloz.trial.colorrecipesexplorer.service

import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.model.validation.isNotNullAndNotBlank
import dev.fyloz.trial.colorrecipesexplorer.model.validation.or
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.trial.colorrecipesexplorer.service.files.FilesService
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import java.io.File
import java.nio.file.NoSuchFileException
import kotlin.contracts.ExperimentalContracts

interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository> {
    /** Checks if one or more recipes have the given [company]. */
    fun existsByCompany(company: Company): Boolean

    /** Gets all recipes with the given [company]. */
    fun getAllByCompany(company: Company): Collection<Recipe>

    /** Updates the public data of a recipe with the given [publicDateDto]. */
    fun updatePublicData(publicDataDto: RecipePublicDataDto)

    /** Adds the given [mix] to the given [recipe]. */
    fun addMix(recipe: Recipe, mix: Mix): Recipe

    /** Removes the given [mix] from its recipe. */
    fun removeMix(mix: Mix): Recipe
}

@Service
class RecipeServiceImpl(
    recipeRepository: RecipeRepository,
    val companyService: CompanyService,
    val mixService: MixService
) :
    AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(recipeRepository),
    RecipeService {
    override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
    override fun getAllByCompany(company: Company): Collection<Recipe> = repository.findAllByCompany(company)

    override fun save(entity: RecipeSaveDto): Recipe {
        // TODO checks if name is unique in the scope of the [company]
        return save(with(entity) {
            recipe(
                name = name,
                description = description,
                sample = sample,
                approbationDate = approbationDate,
                remark = remark ?: "",
                company = companyService.getById(companyId)
            )
        })
    }

    @ExperimentalContracts
    override fun update(entity: RecipeUpdateDto): Recipe {
        val persistedRecipe by lazy { getById(entity.id) }

        return update(with(entity) {
            recipe(
                id = id,
                name = name.or(persistedRecipe.name),
                description = description.or(persistedRecipe.description),
                sample = if (sample != null && sample >= 0) sample else persistedRecipe.sample,
                approbationDate = approbationDate ?: persistedRecipe.approbationDate,
                remark = remark.or(persistedRecipe.remark),
                note = persistedRecipe.note,
                company = persistedRecipe.company,
                mixes = persistedRecipe.mixes,
                steps = steps?.toMutableList() ?: persistedRecipe.steps
            )
        })
    }

    @ExperimentalContracts
    override fun updatePublicData(publicDataDto: RecipePublicDataDto) {
        val recipe = getById(publicDataDto.id)

        if (isNotNullAndNotBlank(publicDataDto.note)) {
            update(recipe.apply { note = publicDataDto.note })
        }

        with(publicDataDto.mixesLocation) {
            if (!isNullOrEmpty()) {
                // Map each mix ID to their mix in the recipe
                map { recipe.mixes.firstOrNull { mix -> mix.id == it.key } to it.value }
                    // Remove pairs with a null mix
                    .filter { it.first != null }
                    .forEach { mixService.updateLocation(it.first!!, it.second) }
            }
        }
    }

    override fun addMix(recipe: Recipe, mix: Mix) =
        update(recipe.apply { mixes.add(mix) })

    override fun removeMix(mix: Mix): Recipe =
        update(mix.recipe.apply { mixes.remove(mix) })
}

const val RECIPE_IMAGES_DIRECTORY = "images/recipe"

interface RecipeImageService {
    fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray

    /** Gets the identifier of every images associated to the recipe with the given [recipeId]. */
    fun getAllIdsForRecipe(recipeId: Long): Collection<Long>

    /** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the identifier of the saved image. */
    fun save(image: MultipartFile, recipeId: Long): Long

    /** Deletes the image with the given [recipeId] and [id]. */
    fun delete(id: Long, recipeId: Long)
}

@Service
class RecipeImageServiceImpl(val recipeService: RecipeService, val filesService: FilesService) : RecipeImageService {
    override fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray =
        try {
            filesService.readAsBytes(getPath(id, recipeId))
        } catch (ex: NoSuchFileException) {
            throw EntityNotFoundRestException("$recipeId/$id")
        }

    override fun getAllIdsForRecipe(recipeId: Long): Collection<Long> {
        val recipe = recipeService.getById(recipeId)
        val recipeDirectory = getRecipeDirectory(recipe.id!!)
        if (!recipeDirectory.exists() || !recipeDirectory.isDirectory) {
            return listOf()
        }
        return recipeDirectory.listFiles()!!   // Should never be null because we check if recipeDirectory is a directory and exists before
            .filterNotNull()
            .map { it.name.toLong() }
    }

    override fun save(image: MultipartFile, recipeId: Long): Long {
        /** Gets the next id available for a new image for the recipe with the given [recipeId]. */
        fun getNextAvailableId(): Long =
            with(getAllIdsForRecipe(recipeId)) {
                if (isEmpty())
                    0
                else
                    maxOrNull()!! + 1L  // maxOrNull() cannot return null because existingIds cannot be empty at this point
            }

        val nextAvailableId = getNextAvailableId()
        filesService.write(image, getPath(nextAvailableId, recipeId))
        return nextAvailableId
    }

    override fun delete(id: Long, recipeId: Long) =
        filesService.delete(getPath(id, recipeId))

    /** Gets the images directory of the recipe with the given [recipeId]. */
    fun getRecipeDirectory(recipeId: Long) = File(filesService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId"))

    /** Gets the file of the image with the given [recipeId] and [id]. */
    fun getPath(id: Long, recipeId: Long): String = filesService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId/$id")
}
