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 dev.fyloz.trial.colorrecipesexplorer.rest.RestApiController
import io.jsonwebtoken.lang.Assert
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull
import kotlin.contracts.ExperimentalContracts

/**
 * A service implementing the basics CRUD operations for the given entities.
 *
 * @param E The entity type
 * @param R The entity repository type
 */
interface Service<E, R : JpaRepository<E, *>> {
    val repository: R

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

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

    /** Updates a given [entity]. */
    fun update(entity: E): 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, R : JpaRepository<E, *>> : Service<E, 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, R : JpaRepository<E, *>> : ModelService<E, 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, R : JpaRepository<E, *>>(override val repository: R) : Service<E, R> {
    override fun getAll(): Collection<E> = repository.findAll()
    override fun save(entity: E): E = repository.save(entity)
    override fun update(entity: E): E = repository.save(entity)
    override fun delete(entity: E) = repository.delete(entity)
}

abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repository: R) :
    AbstractService<E, R>(repository), ModelService<E, 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 {
        assertId(entity.id)
        if (!existsById(entity.id!!))
            throw EntityNotFoundRestException(entity.id!!)
        return super.update(entity)
    }

    override fun deleteById(id: Long) =
        delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing

    protected fun assertId(id: Long?) {
        Assert.notNull(id, "${javaClass.simpleName}.update() was called with a null identifier")
    }
}

abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<E>>(repository: R) :
    AbstractModelService<E, R>(repository), NamedModelService<E, 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 {
        assertId(entity.id)
        assertName(entity.name)
        with(repository.findByName(entity.name)) {
            if (this != null && id != entity.id)
                throw EntityAlreadyExistsRestException(entity.name)
        }
        return super.update(entity)
    }

    protected fun assertName(name: String) {
        Assert.notNull(name, "${javaClass.simpleName}.update() was called with a null name")
    }
}

/**
 * A service that will receive *external* interactions, from a [RestApiController] for example.
 *
 * @param E The entity type
 * @param S The entity save DTO type
 * @param U The entity update DTO type
 */
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> : Service<E, R> {
    /** Saves a given [entity]. */
    fun save(entity: S): E = save(entity.toEntity())

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

/** An [ExternalService] for entities implementing the [Model] interface. */
interface ExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> :
    ModelService<E, R>, ExternalService<E, S, U, R>

/** An [ExternalService] for entities implementing the [NamedModel] interface. */
interface ExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> :
    NamedModelService<E, R>, ExternalModelService<E, S, U, R>

/** An [AbstractService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>>(repository: R) :
    AbstractService<E, R>(repository), ExternalService<E, S, U, R>

/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, Long>>(
    repository: R
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, R>

/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(
    repository: R
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, R>

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