package dev.fyloz.trial.colorrecipesexplorer.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.EntityDto
import dev.fyloz.trial.colorrecipesexplorer.model.Model
import dev.fyloz.trial.colorrecipesexplorer.model.NamedModel
import dev.fyloz.trial.colorrecipesexplorer.repository.NamedJpaRepository
import io.jsonwebtoken.lang.Assert
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull

/**
 * A service implementing the basics CRUD operations.
 *
 * @param E The entity type
 * @param S The entity save type
 * @param U The entity update type
 */
interface Service<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> {
    val repository: R

    /** Gets all entities. */
    fun getAll(): Collection<E>

    /** Saves a given [entity]. */
    fun save(entity: E): E

    /** Saves a given [entity]. */
    fun save(entity: S): E

    /** Updates a given [entity]. */
    fun update(entity: E): E

    /** Updates a given [entity]. */
    fun update(entity: U): E

    /** Deletes a given [entity]. */
    fun delete(entity: E)
}

/** A service for entities implementing the [Model] interface. This service add supports for numeric identifiers. */
interface ModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> : Service<E, S, U, R> {
    /** Checks if an entity with the given [id] exists. */
    fun existsById(id: Long): Boolean

    /** Gets the entity with the given [id]. */
    fun getById(id: Long): E

    /** Deletes the entity with the given [id]. */
    fun deleteById(id: Long)
}

/** A service for entities implementing the [NamedModel] interface. This service add supports for name identifiers. */
interface NamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> : ModelService<E, S, U, R> {
    /** Checks if an entity with the given [name] exists. */
    fun existsByName(name: String): Boolean

    /** Gets the entity with the given [name]. */
    fun getByName(name: String): E

    /** Deletes the entity with the given [name]. */
    fun deleteByName(name: String)
}


abstract class AbstractService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>>(override val repository: R) : Service<E, S, U, R> {
    override fun getAll(): Collection<E> = repository.findAll()
    override fun save(entity: E): E = repository.save(entity)
    override fun save(entity: S): E = save(entity.toEntity())
    override fun update(entity: E): E = repository.save(entity)
    override fun update(entity: U): E = update(entity.toEntity())
    override fun delete(entity: E) = repository.delete(entity)
}

abstract class AbstractModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, Long>>(repository: R) :
        AbstractService<E, S, U, R>(repository), ModelService<E, S, U, R> {
    override fun existsById(id: Long): Boolean = repository.existsById(id)
    override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw EntityNotFoundRestException(id)

    override fun save(entity: E): E {
        if (entity.id != null && existsById(entity.id!!))
            throw EntityAlreadyExistsRestException(entity.id!!)
        return super.save(entity)
    }

    override fun update(entity: E): E {
        Assert.notNull(entity.id, "AbstractModelService.update() was called with a null identifier")
        if (!existsById(entity.id!!))
            throw EntityNotFoundRestException(entity.id!!)
        return super.update(entity)
    }

    override fun deleteById(id: Long) = repository.deleteById(id)
}

abstract class AbstractNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(repository: R) :
        AbstractModelService<E, S, U, R>(repository), NamedModelService<E, S, U, R> {
    override fun existsByName(name: String): Boolean = repository.existsByName(name)
    override fun getByName(name: String): E = repository.findByName(name) ?: throw EntityNotFoundRestException(name)
    override fun deleteByName(name: String) = repository.deleteByName(name)

    override fun save(entity: E): E {
        if (existsByName(entity.name))
            throw EntityAlreadyExistsRestException(entity.name)
        return super.save(entity)
    }

    override fun update(entity: E): E {
        Assert.notNull(entity.id, "AbstractNamedModelService.update() was called with a null identifier")
        with(repository.findByName(entity.name)) {
            if (this != null && id != entity.id)
                throw EntityAlreadyExistsRestException(entity.name)
        }
        return super.update(entity)
    }
}

/** Transforms the given object to JSON. **/
fun Any.asJson(): String {
    return jacksonObjectMapper().writeValueAsString(this)
}
