package dev.alangomes.springspigot.security;

import dev.alangomes.springspigot.context.Context;
import dev.alangomes.springspigot.context.SessionService;
import dev.alangomes.springspigot.exception.PermissionDeniedException;
import dev.alangomes.springspigot.exception.PlayerNotFoundException;
import dev.alangomes.springspigot.util.AopAnnotationUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.bukkit.ChatColor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.apache.commons.lang3.BooleanUtils.toBoolean;
import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_SINGLETON;

@Slf4j
@Aspect
@Component
@Scope(SCOPE_SINGLETON)
class SecurityAspect {

    @Autowired
    private Context context;

    @Autowired
    private SessionService sessionService;

    @Autowired(required = false)
    private GuardService guardService;

    private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();

    private final ExpressionParser parser = new SpelExpressionParser();

    @Order(0)
    @Around("within(@(@dev.alangomes.springspigot.security.Authorize *) *) " +
            "|| execution(@(@dev.alangomes.springspigot.security.Authorize *) * *(..)) " +
            "|| @within(dev.alangomes.springspigot.security.Authorize)" +
            "|| execution(@dev.alangomes.springspigot.security.Authorize * *(..))")
    public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
        val sender = context.getSender();
        if (sender == null) {
            throw new PlayerNotFoundException();
        }
        val method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        val senderContext = new StandardEvaluationContext(sender);
        val parameters = method.getParameters();
        IntStream.range(0, parameters.length)
                .forEach(i -> senderContext.setVariable(parameters[i].getName(), joinPoint.getArgs()[i]));
        senderContext.setVariable("session", sessionService.current());
        senderContext.setVariable("guard", guardService);

        AopAnnotationUtils.getAppliableAnnotations(method, Authorize.class).forEach(authorize -> {
            val expressionSource = authorize.value();
            val expression = expressionCache.computeIfAbsent(expressionSource, parser::parseExpression);
            senderContext.setVariable("params", authorize.params());
            if (!toBoolean(expression.getValue(senderContext, Boolean.class))) {
                val message = StringUtils.trimToNull(ChatColor.translateAlternateColorCodes('&', authorize.message()));
                throw new PermissionDeniedException(expressionSource, message);
            }
        });
        return joinPoint.proceed();
    }

    @Order(1)
    @Before("within(@(@dev.alangomes.springspigot.security.Audit *) *) " +
            "|| execution(@(@dev.alangomes.springspigot.security.Audit *) * *(..)) " +
            "|| @within(dev.alangomes.springspigot.security.Audit)" +
            "|| execution(@dev.alangomes.springspigot.security.Audit * *(..))")
    public void auditCall(JoinPoint joinPoint) {
        val sender = context.getSender();
        val method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        val signature = ClassUtils.getUserClass(method.getDeclaringClass()).getName() + "." + method.getName();
        val arguments = Arrays.stream(joinPoint.getArgs()).map(String::valueOf).collect(Collectors.joining(", "));

        AopAnnotationUtils.getAppliableAnnotations(method, Audit.class).stream()
                .filter(audit -> sender != null || !audit.senderOnly())
                .limit(1)
                .forEach(audit -> {
                    if (sender != null) {
                        log.info(String.format("Player %s invoked %s(%s)", sender.getName(), signature, arguments));
                    } else {
                        log.info(String.format("Server invoked %s(%s)", signature, arguments));
                    }
                });
    }

}