package dev.fyloz.trial.colorrecipesexplorer.model

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import java.util.*
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size


private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom  est requis"
private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"

@Entity
data class Employee(
        @Id
        override val id: Long,

        val firstName: String = "",

        val lastName: String = "",

        @JsonIgnore
        val password: String = "",

        @JsonIgnore
        val isDefaultGroupUser: Boolean = false,

        @JsonIgnore
        val isSystemUser: Boolean = false,

        @field:ManyToOne
        @Fetch(FetchMode.SELECT)
        var group: EmployeeGroup? = null,

        @Enumerated(EnumType.STRING)
        @ElementCollection(fetch = FetchType.EAGER)
        @Fetch(FetchMode.SUBSELECT)
        @get:JsonIgnore
        val permissions: MutableSet<EmployeePermission> = mutableSetOf(),

        val lastLoginTime: LocalDateTime? = null
) : Model {
    @JsonProperty("permissions")
    fun getFlattenedPermissions(): Iterable<EmployeePermission> = getPermissions()

    override fun equals(other: Any?): Boolean = other is Employee && id == other.id && firstName == other.firstName && lastName == other.lastName
    override fun hashCode(): Int = Objects.hash(id, firstName, lastName)
}

/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
        @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
        val id: Long,

        @field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
        val firstName: String,

        @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
        val lastName: String,

        @field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
        @field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
        val password: String,

        @field:ManyToOne
        @Fetch(FetchMode.SELECT)
        var groupId: Long? = null,

        @Enumerated(EnumType.STRING)
        val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee> {
    override fun toEntity(): Employee =
            Employee(id, firstName, lastName, "", isDefaultGroupUser = false, isSystemUser = false, group = null, permissions = permissions)
}

open class EmployeeUpdateDto(
        @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
        val id: Long,

        @field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
        val firstName: String = "",

        @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
        val lastName: String = "",

        @Enumerated(EnumType.STRING)
        val permissions: Set<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee> {
    override fun toEntity(): Employee =
            Employee(id, firstName, lastName, permissions = permissions.toMutableSet())
}


private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"

@Entity
data class EmployeeGroup(
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        override var id: Long? = null,

        @Column(unique = true)
        override val name: String = "",

        @Enumerated(EnumType.STRING)
        @ElementCollection(fetch = FetchType.EAGER)
        val permissions: MutableSet<EmployeePermission> = mutableSetOf(),

        @OneToMany
        @JsonIgnore
        val employees: MutableSet<Employee> = mutableSetOf()
) : NamedModel {
    fun getEmployeeCount() = employees.size

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

open class EmployeeGroupSaveDto(
        @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
        @field:Size(min = 3)
        val name: String,

        @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
        val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
    override fun toEntity(): EmployeeGroup =
            EmployeeGroup(null, name, permissions)
}

open class EmployeeGroupUpdateDto(
        @field:NotNull(message = GROUP_ID_NULL_MESSAGE)
        val id: Long,

        @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
        @field:Size(min = 3)
        val name: String,

        @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
        val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
    override fun toEntity(): EmployeeGroup =
            EmployeeGroup(id, name, permissions)
}


data class EmployeeLoginRequest(val id: Long, val password: String)


enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> = listOf()) {
    // View
    VIEW_EMPLOYEE,
    VIEW_EMPLOYEE_GROUP,
    VIEW(listOf(

    )),

    // Edit
    EDIT_EMPLOYEE(listOf(VIEW_EMPLOYEE)),
    EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_EMPLOYEE)),
    EDIT_EMPLOYEE_GROUP(listOf(VIEW_EMPLOYEE_GROUP)),
    EDIT(listOf(
            VIEW
    )),

    // Remove
    REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)),
    REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)),
    REMOVE(listOf(
            EDIT
    )),

    // Others
    SET_BROWSER_DEFAULT_GROUP(listOf(
            VIEW_EMPLOYEE_GROUP
    )),

    ADMIN(listOf(
            REMOVE,
            SET_BROWSER_DEFAULT_GROUP,

            // Admin only permissions
            REMOVE_EMPLOYEE,
            EDIT_EMPLOYEE_PASSWORD,
            REMOVE_EMPLOYEE_GROUP,
    ));

    operator fun contains(permission: EmployeePermission): Boolean {
        return permission == this || impliedPermissions.any { permission in it }
    }
}

/** Gets [GrantedAuthority]s of the given [Employee]. */
fun Employee.getAuthorities(): MutableCollection<GrantedAuthority> {
    return getPermissions().map { it.toAuthority() }.toMutableSet()
}

/** Gets [EmployeePermission]s of the given [Employee]. */
fun Employee.getPermissions(): Iterable<EmployeePermission> {
    val grantedPermissions: MutableSet<EmployeePermission> = mutableSetOf()
    if (group != null) grantedPermissions.addAll(group!!.permissions.flatMap { it.flat() })
    grantedPermissions.addAll(permissions.flatMap { it.flat() })
    return grantedPermissions
}

private fun EmployeePermission.flat(): Iterable<EmployeePermission> {
    return mutableSetOf(this).apply {
        impliedPermissions.forEach {
            addAll(it.flat())
        }
    }
}

/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */
private fun EmployeePermission.toAuthority(): GrantedAuthority {
    return SimpleGrantedAuthority(name)
}


// ==== DSL ====
fun employee(
        passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
        id: Long = 0L,
        firstName: String = "firstName",
        lastName: String = "lastName",
        password: String = passwordEncoder.encode("password"),
        isDefaultGroupUser: Boolean = false,
        isSystemUser: Boolean = false,
        group: EmployeeGroup? = null,
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        lastLoginTime: LocalDateTime? = null,
        op: Employee.() -> Unit = {}
) = Employee(id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime).apply(op)

fun employee(
        employee: Employee,
        newId: Long? = null
) = with(employee) {
    Employee(newId
            ?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime)
}

fun employeeSaveDto(
        passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
        id: Long = 0L,
        firstName: String = "firstName",
        lastName: String = "lastName",
        password: String = passwordEncoder.encode("password"),
        groupId: Long? = null,
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)

fun employeeUpdateDto(
        id: Long = 0L,
        firstName: String = "firstName",
        lastName: String = "lastName",
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, permissions).apply(op)

fun employeeGroup(
        id: Long? = null,
        name: String = "name",
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        employees: MutableSet<Employee> = mutableSetOf(),
        op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions, employees).apply(op)

fun employeeGroup(
        employeeGroup: EmployeeGroup,
        newId: Long? = null,
        newName: String? = null
) = with(employeeGroup) { EmployeeGroup(newId ?: id, newName ?: name, permissions, employees) }

fun employeeGroupSaveDto(
        name: String = "name",
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)

fun employeeGroupUpdateDto(
        id: Long = 0L,
        name: String = "name",
        permissions: MutableSet<EmployeePermission> = mutableSetOf(),
        op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)

