package inn.ocsf.bee.freigeld.serve

import inn.ocsf.bee.freigeld.core.model.GlobalWorld
import inn.ocsf.bee.freigeld.core.model.Person
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.DisabledException
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.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
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.crypto.password.NoOpPasswordEncoder
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.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import org.springframework.web.filter.OncePerRequestFilter
import java.io.Serializable
import java.nio.charset.StandardCharsets
import java.util.*
import javax.inject.Inject
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import kotlin.collections.HashMap


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class Security : WebSecurityConfigurerAdapter() {

    @Inject
    private lateinit var jwtUserDetailsService: JwtUserDetailsService

    @Inject
    private lateinit var jwtAuthEntryPoint: JwtAuthEntryPoint

    @Inject
    private lateinit var jwtRequestFilter: JwtRequestFilter

    @Bean
    @Throws(Exception::class)
    override fun authenticationManagerBean(): AuthenticationManager? {
        return super.authenticationManagerBean()
    }


    @Autowired
    @Throws(Exception::class)
    fun configureGlobal(auth: AuthenticationManagerBuilder) {
        auth.userDetailsService<UserDetailsService>(jwtUserDetailsService).passwordEncoder(passwordEncoder())
    }


    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return NoOpPasswordEncoder.getInstance()
    }

    @Throws(Exception::class)
    override fun configure(httpSecurity: HttpSecurity) {
        httpSecurity
                .csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/static/**").permitAll()
                .antMatchers("/api/new/person/**").permitAll()
                .antMatchers("/ws/**").permitAll()
                .anyRequest().authenticated()
                .and().exceptionHandling()
                .authenticationEntryPoint(jwtAuthEntryPoint)
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java)
    }
}

@Component
class JwtAuthEntryPoint : AuthenticationEntryPoint, Serializable {

    override fun commence(p0: HttpServletRequest?, p1: HttpServletResponse?, p2: AuthenticationException?) {
        p1?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unauthorized")
    }

}

@Component
class JwtTokenUtil : Serializable {

    companion object {
        val secret = Keys.hmacShaKeyFor("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vel fermentum eros, a molestie nisl. Nunc viverra ante nulla, non tincidunt leo rutrum vel. Donec pulvinar ornare eros, at bibendum augue efficitur eu. Nunc sit amet massa sit amet nisi fringilla commodo ac in mi. Morbi viverra enim vel rhoncus semper. Phasellus semper convallis eros, ac pretium nulla aliquet ut. Ut fringilla leo ut purus gravida, quis vestibulum mi commodo. Curabitur feugiat odio at dapibus aliquam. Phasellus eget eros quis enim vulputate fringilla a sit amet nibh. In eget metus sed dui maximus auctor. Nunc eu ornare ligula, bibendum luctus lectus.".toByteArray(StandardCharsets.UTF_8))

        val tokenLifetime = 5 * 60 * 60L
    }

    fun getUsernameFromToken(token: String?): String? {
        return getClaimFromToken(token, Claims::getSubject)
    }

    fun getExpirationDateFromToken(token: String?): Date {
        return getClaimFromToken<Date>(token, Claims::getExpiration)
    }

    fun <T> getClaimFromToken(token: String?, claimsResolver: (Claims) -> T): T {
        val claims: Claims = getAllClaimsFromToken(token)
        return claimsResolver(claims)
    }

    private fun getAllClaimsFromToken(token: String?): Claims {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).body
    }

    private fun isTokenExpired(token: String?): Boolean {
        val expiration: Date = getExpirationDateFromToken(token)
        return expiration.before(Date())
    }

    fun generateToken(userDetails: UserDetails): String {
        val claims: Map<String, Any> = HashMap()
        return doGenerateToken(claims, userDetails.username)
    }

    private fun doGenerateToken(claims: Map<String, Any>, subject: String): String {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(Date(System.currentTimeMillis()))
                .setExpiration(Date(System.currentTimeMillis() + tokenLifetime * 1000))
                .signWith(secret).compact()
    }

    fun validateToken(token: String?, userDetails: UserDetails): Boolean {
        val username = getUsernameFromToken(token)
        return username == userDetails.username && !isTokenExpired(token)
    }
}

@Component
class JwtRequestFilter : OncePerRequestFilter() {

    @Inject
    private lateinit var jwtTokenUtil: JwtTokenUtil

    @Inject
    private lateinit var jwtUserDetailsService: JwtUserDetailsService

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, next: FilterChain) {
        val requestTokenHeader = request.getHeader("Authorization")
        var username: String? = null
        var jwtToken: String? = null

        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7)
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken)
            } catch (e: IllegalArgumentException) {
                //System.out.println("Unable to get JWT Token")
            } catch (e: ExpiredJwtException) {
                //System.out.println("JWT Token has expired")
            }
        } else if (requestTokenHeader != null) {
            logger.warn("JWT Token does not begin with Bearer String")
        }
        if (username != null && SecurityContextHolder.getContext().authentication == null) {
            val userDetails = jwtUserDetailsService.loadUserByUsername(username)
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
                usernamePasswordAuthenticationToken.details = WebAuthenticationDetailsSource().buildDetails(request)
                SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
            }
        }
        next.doFilter(request, response)
    }
}

abstract class JwtAuthenticationController {

    @Autowired
    private lateinit var authenticationManager: AuthenticationManager

    @Autowired
    private lateinit var jwtTokenUtil: JwtTokenUtil

    @Autowired
    private lateinit var userDetailsService: JwtUserDetailsService

    fun createAuthenticationToken(username: String): String {
        authenticate(username)
        val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)
        return jwtTokenUtil.generateToken(userDetails)
    }

    @Throws(Exception::class)
    private fun authenticate(username: String) {
        try {
            authenticationManager.authenticate(UsernamePasswordAuthenticationToken(username, ""))
        } catch (e: DisabledException) {
            throw Exception("USER_DISABLED", e)
        } catch (e: BadCredentialsException) {
            throw Exception("INVALID_CREDENTIALS", e)
        }
    }
}

@Component
class JwtUserDetailsService : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {
        return JwtUser(username, "", mutableListOf(SimpleGrantedAuthority("PERSON")))
    }

}

@Service
class PersonService {

    @Inject
    private lateinit var world: GlobalWorld

    fun getCurrentPerson(): Person? {
        val user = SecurityContextHolder.getContext().authentication.principal as JwtUser
        return world.getPerson(UUID.fromString(user.username))
    }
}

class JwtUser(username: String, password: String, authorities: MutableCollection<out GrantedAuthority>) : User(username, password, authorities)
