package dev.fyloz.trial.colorrecipesexplorer.config

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.trial.colorrecipesexplorer.model.Employee
import dev.fyloz.trial.colorrecipesexplorer.model.EmployeeLoginRequest
import dev.fyloz.trial.colorrecipesexplorer.model.EmployeePermission
import dev.fyloz.trial.colorrecipesexplorer.service.EmployeeServiceImpl
import dev.fyloz.trial.colorrecipesexplorer.service.EmployeeUserDetailsServiceImpl
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.User
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.stereotype.Component
import org.springframework.util.Assert
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.util.WebUtils
import java.util.*
import javax.annotation.PostConstruct
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
class WebSecurityConfig(
        val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
        val securityConfigurationProperties: SecurityConfigurationProperties,
        val logger: Logger) : WebSecurityConfigurerAdapter() {
    @Autowired
    private lateinit var userDetailsService: EmployeeUserDetailsServiceImpl

    @Autowired
    private lateinit var employeeService: EmployeeServiceImpl

    override fun configure(authBuilder: AuthenticationManagerBuilder) {
        authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }

    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        return UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", CorsConfiguration().apply {
                allowedOrigins = listOf("http://localhost:4200")    // Angular development server
                allowedMethods = listOf(
                        HttpMethod.GET.name,
                        HttpMethod.POST.name,
                        HttpMethod.PUT.name,
                        HttpMethod.DELETE.name,
                        HttpMethod.OPTIONS.name,
                        HttpMethod.HEAD.name
                )
                allowCredentials = true
            }.applyPermitDefaultValues())
        }
    }

    @PostConstruct
    fun createSystemUsers() {
        fun createUser(credentials: SecurityConfigurationProperties.SystemUserCredentials?, firstName: String, lastName: String, permissions: List<EmployeePermission>) {
            Assert.notNull(credentials, "No root user has been defined.")
            credentials!!
            Assert.notNull(credentials.id, "The root user has no identifier defined.")
            Assert.notNull(credentials.password, "The root user has no password defined.")
            if (!employeeService.existsById(credentials.id!!)) {
                employeeService.save(Employee(
                        id = credentials.id!!,
                        firstName = firstName,
                        lastName = lastName,
                        password = passwordEncoder().encode(credentials.password!!),
                        isSystemUser = true,
                        permissions = permissions.toMutableSet()
                ))
            }
        }

        createUser(securityConfigurationProperties.root, "Root", "User", listOf(EmployeePermission.ADMIN))
    }

    override fun configure(http: HttpSecurity) {
        fun ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry.generateAuthorizations():
                ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry {
            ControllerAuthorizations.values().forEach { controller ->
                val antMatcher = controller.antMatcher
                controller.permissions.forEach {
                    antMatchers(it.key, antMatcher).hasAuthority(it.value.name)
                    logger.debug("Added authorization for path '$antMatcher': ${it.key.name} -> ${it.value.name}")
                }
            }
            return this
        }

        http
                .cors()
                .and()
                .headers().frameOptions().disable()
                .and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/").permitAll()
                .antMatchers("/api/login").permitAll()
                .antMatchers("/api/employee/logout").permitAll()
                .antMatchers(HttpMethod.GET, "/api/employee/current").authenticated()
                .generateAuthorizations()
                .and()
                .addFilter(JwtAuthenticationFilter(authenticationManager(), employeeService, securityConfigurationProperties))
                .addFilter(JwtAuthorizationFilter(userDetailsService, securityConfigurationProperties, authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }
}

@Component
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}

class CorsFilter : Filter {
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        response as HttpServletResponse

        response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200")
        response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS")
        response.setHeader("Access-Control-Allow-Headers", "*")
        response.setHeader("Access-Control-Allow-Credentials", true.toString())
        response.setHeader("Access-Control-Max-Age", 180.toString())

        chain.doFilter(request, response)
    }
}

const val authorizationCookieName = "Authorization"
const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>()

class JwtAuthenticationFilter(
        val authManager: AuthenticationManager,
        val employeeService: EmployeeServiceImpl,
        val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() {
    init {
        setFilterProcessesUrl("/api/login")
    }

    override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
        val loginRequest = jacksonObjectMapper().readValue(request.inputStream, EmployeeLoginRequest::class.java)
        return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
    }

    override fun successfulAuthentication(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain, authResult: Authentication) {
        val jwtSecret = securityConfigurationProperties.jwtSecret
        val jwtDuration = securityConfigurationProperties.jwtDuration
        Assert.notNull(jwtSecret, "No JWT secret has been defined.")
        Assert.notNull(jwtDuration, "No JWT duration has been defined.")
        val employeeId = (authResult.principal as User).username
        employeeService.updateLastLoginTime(employeeId.toLong())
        val expirationMs = System.currentTimeMillis() + jwtDuration!!
        val expirationDate = Date(expirationMs)
        val token = Jwts.builder()
                .setSubject(employeeId)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
                .compact()
        response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
        response.addHeader("Set-Cookie", "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict")
        response.addHeader(authorizationCookieName, "Bearer $token")
        response.addHeader("X-Authentication-Expiration", "$expirationMs")
    }
}

class JwtAuthorizationFilter(
        val userDetailsService: EmployeeUserDetailsServiceImpl,
        val securityConfigurationProperties: SecurityConfigurationProperties,
        authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
        val authorizationValue = if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName)
        if (authorizationValue != null && authorizationValue.startsWith("Bearer") && authorizationValue !in blacklistedJwtTokens) {
            val authenticationToken = getAuthentication(authorizationValue)
            SecurityContextHolder.getContext().authentication = authenticationToken
        } else {
            val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
            if (defaultGroupCookie != null) {
                val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
                SecurityContextHolder.getContext().authentication = authenticationToken
            }
        }
        chain.doFilter(request, response)
    }

    private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
        val jwtSecret = securityConfigurationProperties.jwtSecret
        Assert.notNull(jwtSecret, "No JWT secret has been defined.")
        val employeeId = Jwts.parser()
                .setSigningKey(jwtSecret!!.toByteArray())
                .parseClaimsJws(token.replace("Bearer", ""))
                .body
                .subject
        return if (employeeId != null) getAuthenticationToken(employeeId) else null
    }

    private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken {
        val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false)
        return UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
    }
}

@ConfigurationProperties("cre.security")
class SecurityConfigurationProperties {
    var jwtSecret: String? = null
    var jwtDuration: Long? = null
    var root: SystemUserCredentials? = null

    class SystemUserCredentials(var id: Long? = null, var password: String? = null)
}

private enum class ControllerAuthorizations(
        val antMatcher: String,
        val permissions: Map<HttpMethod, EmployeePermission>
) {
    SET_BROWSER_DEFAULT_GROUP("/api/employee/group/default/**", mapOf(
            HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
            HttpMethod.POST to EmployeePermission.SET_BROWSER_DEFAULT_GROUP
    )),
    EMPLOYEES_FOR_GROUP("/api/employee/group/*/employees", mapOf(
            HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE
    )),
    EMPLOYEE_GROUP("/api/employee/group/**", mapOf(
            HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
            HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE_GROUP,
            HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_GROUP,
            HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE_GROUP
    )),
    EMPLOYEE_PASSWORD("/api/employee/*/password", mapOf(
            HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_PASSWORD
    )),
    EMPLOYEE("/api/employee/**", mapOf(
            HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE,
            HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE,
            HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE,
            HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE
    ))
}
