package alien4cloud.audit;

import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.annotation.Resource;

import alien4cloud.dao.QueryUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;

import alien4cloud.audit.annotation.Audit;
import alien4cloud.audit.model.AuditConfiguration;
import alien4cloud.audit.model.AuditTrace;
import alien4cloud.audit.model.Method;
import alien4cloud.dao.IGenericSearchDAO;
import alien4cloud.dao.model.FacetedSearchResult;
import alien4cloud.exception.NotFoundException;
import alien4cloud.security.AuthorizationUtil;
import lombok.extern.slf4j.Slf4j;

import static org.apache.commons.lang3.StringUtils.defaultString;

@Component
@Slf4j
public class AuditService {

    public static final String CONTROLLER_SUFFIX = "Controller";

    @Resource(name = "alien-audit-dao")
    private IGenericSearchDAO alienDAO;

    /**
     * Cache the instance of audit configuration bad idea ?
     */
    private AuditConfiguration auditConfiguration;

    /**
     * Logger for audit events
     */
    private final Logger audit_logger = LoggerFactory.getLogger("AUDIT_LOGS_LOGGER");

    @Value("${logs_audit_appender.enable:false}")
    private boolean isLoggerEnabled;

    private final static String OUTPUT_FORMAT = "%s %s.%s [method=%s][call=%s][response_code=%d][Ipsource=%s][Alien4cloud UI=%s] | %s";

    /**
     * Get the audit configuration, this method will cache the configuration in memory
     * 
     * @return the audit configuration
     */
    public synchronized AuditConfiguration getAuditConfiguration() {
        if (this.auditConfiguration == null) {
            this.auditConfiguration = alienDAO.findById(AuditConfiguration.class, AuditConfiguration.ID);
        }
        return this.auditConfiguration;
    }

    public synchronized void saveAuditConfiguration(AuditConfiguration auditConfiguration) {
        alienDAO.save(auditConfiguration);
        this.auditConfiguration = auditConfiguration;
    }

    public void saveAuditTrace(AuditTrace auditTrace) {
        try {
            if (isLoggerEnabled) {
                audit_logger.info("{}",toLogFormat(auditTrace));
            }
        } finally {
            alienDAO.save(auditTrace);
        }
    }

    public AuditConfiguration getMandatoryAuditConfiguration() {
        AuditConfiguration auditConfiguration = getAuditConfiguration();
        if (auditConfiguration == null) {
            throw new NotFoundException("Audit configuration not found");
        }
        return auditConfiguration;
    }

    public FacetedSearchResult searchAuditTrace(String query, Map<String, String[]> filters, int from, int size, Date fromDate, Date toDate) {
        QueryBuilder authorizationFilter = AuthorizationUtil.getResourceAuthorizationFilters();
        QueryBuilder dateFilter = QueryUtils.buildTimestampRangeQuery(fromDate,toDate);

        QueryBuilder customFilters = QueryUtils.allOf(authorizationFilter,dateFilter);

        return alienDAO.facetedSearch(AuditTrace.class, query, filters, customFilters, null, from, size, "timestamp", "long", true);
    }

    private String getRequestMappingMethod(RequestMapping requestMapping) {
        RequestMethod[] methods = requestMapping.method();
        if (methods.length == 0) {
            return null;
        }
        if (methods.length > 1) {
            log.error("Audit does not support more than one http method on the same Spring Controller method : "
                    + Arrays.stream(methods).map(Enum::toString).collect(Collectors.joining(", ")) + " on "
                    + Arrays.stream(requestMapping.path()).collect(Collectors.joining(", ")));
            return null;
        }
        return methods[0].toString();
    }

    public Method getAuditedMethod(HandlerMethod controllerMethod) {
        RequestMapping methodMapping = AnnotationUtils.findAnnotation(controllerMethod.getMethod(), RequestMapping.class);
        RequestMapping controllerMapping = AnnotationUtils.findAnnotation(controllerMethod.getMethod().getDeclaringClass(), RequestMapping.class);
        String httpMethod = null;
        if (controllerMapping != null) {
            httpMethod = getRequestMappingMethod(controllerMapping);
            if (methodMapping != null) {
                String methodHttpMethod = getRequestMappingMethod(methodMapping);
                if (httpMethod == null) {
                    // Controller http method override method http method
                    httpMethod = methodHttpMethod;
                }
            }
        } else if (methodMapping != null) {
            httpMethod = getRequestMappingMethod(methodMapping);
        }
        if (httpMethod == null) {
            return null;
        }
        Audit audit = getAuditAnnotation(controllerMethod);
        return new Method(controllerMethod.getMethod().toGenericString(), httpMethod, getAuditCategoryName(controllerMethod, audit),
                getAuditActionName(controllerMethod, audit), getAuditHiddenFields(audit));
    }

    public boolean isMethodAudited(AuditConfiguration auditConfiguration, HandlerMethod controllerMethod) {
        Method method = getAuditedMethod(controllerMethod);
        return method != null && Boolean.TRUE.equals(auditConfiguration.getAuditedMethodsMap().get(method));
    }

    public String getAuditCategoryName(HandlerMethod method, Audit audit) {
        if (audit != null && StringUtils.isNotBlank(audit.category())) {
            return audit.category();
        }
        String auditCategory = method.getMethod().getDeclaringClass().getSimpleName();
        if (auditCategory.endsWith(CONTROLLER_SUFFIX) && auditCategory.length() > CONTROLLER_SUFFIX.length()) {
            auditCategory = auditCategory.substring(0, auditCategory.length() - CONTROLLER_SUFFIX.length());
        }
        return auditCategory;
    }

    public String getAuditActionName(HandlerMethod method, Audit audit) {
        if (audit != null && StringUtils.isNotBlank(audit.action())) {
            return audit.action();
        }
        return method.getMethod().getName();
    }

    public Audit getAuditAnnotation(HandlerMethod method) {
        Audit audit = method.getMethodAnnotation(Audit.class);
        if (audit == null) {
            audit = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), Audit.class);
        }
        return audit;
    }

    public String[] getAuditHiddenFields(final Audit audit) {
        if (audit == null) {
            return new String[0];
        }
        return audit.bodyHiddenFields();
    }

    private String toLogFormat(AuditTrace auditTrace) {
        return String.format(OUTPUT_FORMAT,
                defaultString(auditTrace.getUserName()),
                defaultString(auditTrace.getCategory()),
                defaultString(auditTrace.getAction()),
                defaultString(auditTrace.getMethod()),
                defaultString(auditTrace.getPath()),
                auditTrace.getResponseStatus(),
                defaultString(auditTrace.getSourceIp()),
                auditTrace.getAlien4CloudUI().toString(),
                defaultString(auditTrace.getActionDescription())
                );
    }
}
