package dev.fyloz.trial.colorrecipesexplorer.model

import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrSize
import java.time.LocalDate
import java.util.*
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

private const val RECIPE_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val RECIPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val RECIPE_DESCRIPTION_NULL_MESSAGE = "Une description est requise"
private const val RECIPE_SAMPLE_NULL_MESSAGE = "Un numéro d'échantillon est requis"
private const val RECIPE_SAMPLE_TOO_SMALL_MESSAGE = "Le numéro d'échantillon doit être supérieur ou égal à 0"
private const val RECIPE_COMPANY_NULL_MESSAGE = "Une bannière est requise"

@Entity
data class Recipe(
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    override val id: Long?,

    /** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
    val name: String,

    val description: String,

    val sample: Int,

    val approbationDate: LocalDate?,

    /** A remark given by the creator of the recipe. */
    val remark: String,

    /** A note that can be altered by anybody with view permissions to the recipe. */
    var note: String,

    @ManyToOne
    val company: Company,

    @OneToMany(cascade = [CascadeType.ALL])
    val mixes: MutableCollection<Mix>,

    @OneToMany(cascade = [CascadeType.ALL])
    var steps: MutableCollection<RecipeStep>
) : Model {
    constructor(
        id: Long,
        name: String,
        company: Company,
        description: String,
        sample: Int,
        approbationDate: LocalDate?,
        remark: String,
        note: String
    ) : this(id, name, description, sample, approbationDate, remark, note, company, mutableListOf(), mutableListOf())

    val mixesSortedById: Collection<Mix>
        @JsonIgnore
        get() = mixes.sortedBy { it.id }

    /** The mix types contained in this recipe. */
    val mixTypes: Collection<MixType>
        @JsonIgnore
        get() = mixes.map { it.mixType }

    /** Checks if the recipe contains the given [mixType]. */
    fun containsMixType(mixType: MixType) = mixTypes.contains(mixType)

    override fun equals(other: Any?): Boolean = other is Recipe && other.name == name && other.company == company
    override fun hashCode(): Int = Objects.hash(name, company)
}

data class RecipePublicDataDto(
    @field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
    val id: Long,

    val note: String?,

    val mixesLocation: Map<Long, String>?
)

open class RecipeSaveDto(
    @field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
    val name: String,

    @field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
    val description: String,

    @field:NotNull(message = RECIPE_SAMPLE_NULL_MESSAGE)
    @field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
    val sample: Int,

    val approbationDate: LocalDate?,

    val remark: String?,

    @field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
    val companyId: Long = -1L,
) : EntityDto<Recipe> {
    override fun toEntity(): Recipe = recipe(
        name = name,
        description = description,
        sample = sample,
        approbationDate = approbationDate,
        remark = remark ?: "",
        company = company(id = companyId)
    )
}

open class RecipeUpdateDto(
    @field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
    val id: Long,

    @field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
    val name: String?,

    @field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
    val description: String?,

    @field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
    val sample: Int?,

    val approbationDate: LocalDate?,

    val remark: String?,

    val steps: List<RecipeStep>?
) : EntityDto<Recipe> {
    override fun toEntity(): Recipe = recipe(
        id,
        name = name ?: "name",
        description = description ?: "description",
        sample = sample ?: -1,
        approbationDate = approbationDate ?: LocalDate.MIN,
        remark = remark ?: "remark",
        steps = steps?.toMutableList() ?: mutableListOf()
    )
}

// ==== DSL ====
fun recipe(
    id: Long? = null,
    name: String = "name",
    description: String = "description",
    sample: Int = -1,
    approbationDate: LocalDate? = LocalDate.MIN,
    remark: String = "remark",
    note: String = "",
    company: Company = company(),
    mixes: MutableCollection<Mix> = mutableListOf(),
    steps: MutableCollection<RecipeStep> = mutableListOf(),
    op: Recipe.() -> Unit = {}
) = Recipe(id, name, description, sample, approbationDate, remark, note, company, mixes, steps).apply(op)

fun recipePublicDataDto(
    id: Long = 0L,
    note: String? = "note",
    mixesLocation: Map<Long, String>? = mapOf(),
    op: RecipePublicDataDto.() -> Unit = {}
) = RecipePublicDataDto(id, note, mixesLocation).apply(op)

fun recipeSaveDto(
    name: String = "name",
    description: String = "description",
    sample: Int = -1,
    approbationDate: LocalDate? = LocalDate.MIN,
    remark: String = "remark",
    companyId: Long = 0L,
    op: RecipeSaveDto.() -> Unit = {}
) = RecipeSaveDto(name, description, sample, approbationDate, remark, companyId).apply(op)

fun recipeUpdateDto(
    id: Long = 0L,
    name: String = "name",
    description: String = "description",
    sample: Int = -1,
    approbationDate: LocalDate? = LocalDate.MIN,
    remark: String? = "remark",
    steps: List<RecipeStep> = listOf(),
    op: RecipeUpdateDto.() -> Unit = {}
) = RecipeUpdateDto(id, name, description, sample, approbationDate, remark, steps).apply(op)
