package dev.fyloz.trial.colorrecipesexplorer.service

import dev.fyloz.trial.colorrecipesexplorer.config.SecurityConfigurationProperties
import dev.fyloz.trial.colorrecipesexplorer.config.blacklistedJwtTokens
import dev.fyloz.trial.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.trial.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.trial.colorrecipesexplorer.repository.EmployeeRepository
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.*
import io.jsonwebtoken.lang.Assert
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.web.util.WebUtils
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional

interface EmployeeService : ModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeRepository> {
    /** Check if an [Employee] with the given [firstName] and [lastName] exists. */
    fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean

    /** Gets the employee with the given [id]. */
    fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee

    /** Gets all employees which have the given [group]. */
    fun getByGroup(group: EmployeeGroup): Collection<Employee>

    /** Gets the default user of the given [group]. */
    fun getDefaultGroupEmployee(group: EmployeeGroup): Employee

    /** Save a default group employee for the given [group]. */
    fun saveDefaultGroupEmployee(group: EmployeeGroup)

    /** Updates de given [entity]. **/
    fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee

    /** Updates the last login time of the employee with the given [employeeId]. */
    fun updateLastLoginTime(employeeId: Long, time: LocalDateTime = LocalDateTime.now()): Employee

    /** Updates the password of the employee with the given [id]. */
    fun updatePassword(id: Long, password: String): Employee

    /** Adds the given [permission] to the employee with the given [employeeId]. */
    fun addPermission(employeeId: Long, permission: EmployeePermission): Employee

    /** Removes the given [permission] from the employee with the given [employeeId]. */
    fun removePermission(employeeId: Long, permission: EmployeePermission): Employee

    /** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */
    fun logout(request: HttpServletRequest)
}

interface EmployeeGroupService : ModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository> {
    /** Checks if a group with the given [name] exists. */
    fun existsByName(name: String): Boolean

    /** Gets all the employees of the group with the given [id]. */
    fun getEmployeesForGroup(id: Long): Collection<Employee>

    /** Gets the default group from a cookie in the given HTTP [request]. */
    fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup

    /** Sets the default group cookie for the given HTTP [response]. */
    fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)

    /** Adds the employee with the given [employeeId] to the group with the given [groupId]. */
    fun addEmployeeToGroup(groupId: Long, employeeId: Long)

    /**
     * Adds a given [employee] to a given [group].
     *
     * If the [employee] is already in the [group], nothing will be done.
     * If the [employee] is already in a group, it will be removed from it.
     */
    fun addEmployeeToGroup(group: EmployeeGroup, employee: Employee)

    /** Removes the employee with the given [employeeId] from the group with the given [groupId]. */
    fun removeEmployeeFromGroup(groupId: Long, employeeId: Long)

    /** Removes a given [employee] from the given [group]. */
    fun removeEmployeeFromGroup(group: EmployeeGroup, employee: Employee)
}

interface EmployeeUserDetailsService : UserDetailsService {
    /** Loads an [User] for the given [employeeId]. */
    fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
}

@Service
class EmployeeServiceImpl(val employeeRepository: EmployeeRepository, val passwordEncoder: PasswordEncoder) :
        AbstractModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeRepository>(employeeRepository),
        EmployeeService {
    @Autowired
    lateinit var groupService: EmployeeGroupServiceImpl

    override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
            repository.existsByFirstNameAndLastName(firstName, lastName)

    override fun getAll(): Collection<Employee> =
            super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }

    override fun getById(id: Long): Employee =
            getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)

    override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee =
            super.getById(id).apply {
                if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) throw EntityNotFoundRestException(id)
            }

    override fun getByGroup(group: EmployeeGroup): Collection<Employee> =
            repository.findAllByGroup(group).filter {
                !it.isSystemUser && !it.isDefaultGroupUser
            }

    override fun getDefaultGroupEmployee(group: EmployeeGroup): Employee =
            repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)

    override fun save(entity: EmployeeSaveDto): Employee =
            save(with(entity) {
                Employee(id, firstName, lastName, passwordEncoder.encode(password), isDefaultGroupUser = false, isSystemUser = false, group = if (groupId != null) groupService.getById(groupId!!) else null, permissions = permissions)
            })

    override fun save(entity: Employee): Employee {
        if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
            throw EntityAlreadyExistsRestException("${entity.firstName} ${entity.lastName}")
        return super.save(entity)
    }

    override fun saveDefaultGroupEmployee(group: EmployeeGroup) {
        save(Employee(
                id = 1000000L + group.id!!,
                firstName = group.name,
                lastName = "EmployeeModel",
                password = passwordEncoder.encode(group.name),
                group = group,
                isDefaultGroupUser = true
        ))
    }

    override fun updateLastLoginTime(employeeId: Long, time: LocalDateTime): Employee =
            update(Employee(id = employeeId, lastLoginTime = time), ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)

    override fun update(entity: Employee): Employee =
            update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)

    override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
        val persistedEmployee = getById(entity.id, ignoreDefaultGroupUsers, ignoreSystemUsers)
        with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
            if (this != null && id != entity.id)
                throw EntityAlreadyExistsRestException("${entity.firstName} ${entity.lastName}")
        }

        return super.update(with(entity) {
            Employee(
                    id,
                    if (firstName.isNotBlank()) firstName else persistedEmployee.firstName,
                    if (lastName.isNotBlank()) lastName else persistedEmployee.lastName,
                    persistedEmployee.password,
                    if (ignoreDefaultGroupUsers) false else persistedEmployee.isDefaultGroupUser,
                    if (ignoreSystemUsers) false else persistedEmployee.isSystemUser,
                    persistedEmployee.group,
                    if (permissions.isNotEmpty()) permissions else persistedEmployee.permissions,
                    lastLoginTime ?: persistedEmployee.lastLoginTime
            )
        })
    }

    override fun updatePassword(id: Long, password: String): Employee {
        val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
        return super.update(with(persistedEmployee) {
            Employee(
                    id,
                    firstName,
                    lastName,
                    passwordEncoder.encode(password),
                    isDefaultGroupUser,
                    isSystemUser,
                    group,
                    permissions,
                    lastLoginTime
            )
        })
    }

    override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee =
            super.update(getById(employeeId).apply { permissions += permission })

    override fun removePermission(employeeId: Long, permission: EmployeePermission): Employee =
            super.update(getById(employeeId).apply { permissions -= permission })

    override fun logout(request: HttpServletRequest) {
        val authorizationCookie = WebUtils.getCookie(request, "Authorization")
        if (authorizationCookie != null) {
            val authorizationToken = authorizationCookie.value
            if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
                blacklistedJwtTokens.add(authorizationToken)
            }
        }
    }
}

const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans

@Service
class EmployeeGroupServiceImpl(val employeeGroupRepository: EmployeeGroupRepository, val employeeService: EmployeeService) :
        AbstractModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository>(employeeGroupRepository),
        EmployeeGroupService {
    override fun existsByName(name: String): Boolean = repository.existsByName(name)
    override fun getEmployeesForGroup(id: Long): Collection<Employee> =
            getById(id).employees

    @Transactional
    override fun save(entity: EmployeeGroup): EmployeeGroup {
        return super.save(entity).apply {
            Assert.notNull(id, "Saved employee group has a null identifier")
            employeeService.saveDefaultGroupEmployee(this)
        }
    }

    override fun update(entity: EmployeeGroup): EmployeeGroup {
        Assert.notNull("Updated employee group has a null identifier")
        val persistedGroup = getById(entity.id!!)

        return super.update(with(entity) {
            EmployeeGroup(
                    entity.id,
                    if (name.isNotBlank()) entity.name else persistedGroup.name,
                    if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions,
                    persistedGroup.employees
            )
        })
    }

    @Transactional
    override fun delete(entity: EmployeeGroup) {
        employeeService.delete(employeeService.getDefaultGroupEmployee(entity))
        super.delete(entity)
    }

    override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup {
        val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
                ?: throw EntityNotFoundRestException("defaultGroup")
        val defaultGroupUser = employeeService.getById(defaultGroupCookie.value.toLong(), ignoreDefaultGroupUsers = false, ignoreSystemUsers = true)
        return defaultGroupUser.group!!
    }

    override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
        val group = getById(groupId)
        val defaultGroupUser = employeeService.getDefaultGroupEmployee(group)
        response.addHeader("Set-Cookie", "$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict")
    }

    override fun addEmployeeToGroup(groupId: Long, employeeId: Long) {
        addEmployeeToGroup(getById(groupId), employeeService.getById(employeeId))
    }

    @Transactional
    override fun addEmployeeToGroup(group: EmployeeGroup, employee: Employee) {
        if (employee.group == group) return
        if (employee.group != null) removeEmployeeFromGroup(employee.group!!, employee)

        group.employees.add(employee)
        employee.group = group
        update(group)
        employeeService.update(employee)
    }

    override fun removeEmployeeFromGroup(groupId: Long, employeeId: Long) =
            removeEmployeeFromGroup(getById(groupId), employeeService.getById(employeeId))

    @Transactional
    override fun removeEmployeeFromGroup(group: EmployeeGroup, employee: Employee) {
        if (employee.group == null || employee.group != group) return

        group.employees.remove(employee)
        employee.group = null
        update(group)
        employeeService.update(employee)
    }
}

@Service
class EmployeeUserDetailsServiceImpl(val employeeService: EmployeeService, val securityConfigurationProperties: SecurityConfigurationProperties) :
        EmployeeUserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        try {
            return loadUserByEmployeeId(username.toLong(), true)
        } catch (ex: EntityNotFoundException) {
            throw UsernameNotFoundException(username)
        } catch (ex: EntityNotFoundRestException) {
            throw UsernameNotFoundException(username)
        }
    }

    override fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
        val employee = employeeService.getById(employeeId, ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, ignoreSystemUsers = false)
        return User(employee.id.toString(), employee.password, employee.getAuthorities())
    }
}
