package be.iminds.ilabt.jfed.log.cache;

import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

/**
 * User: twalcari
 * Date: 4/28/2014
 * Time: 17:39
 */
public abstract class ApiCallDetailsCache {
    private static final Logger LOG = LoggerFactory.getLogger(ApiCallDetailsCache.class);
    private final LoadingCache<ApiCallDetailsRef, SerializableApiCallDetails> callsCache;


    protected ApiCallDetailsCache() {
        this.callsCache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.MINUTES)
                .weakValues()
                .build(new CacheLoader<ApiCallDetailsRef, SerializableApiCallDetails>() {
                    @Override
                    public SerializableApiCallDetails load(@Nonnull ApiCallDetailsRef apiCallDetailsRef) throws Exception {
                        return getFullApiDetails(apiCallDetailsRef);
                    }
                });
    }

    protected static SerializableApiCallDetails decompressFullApiDetails(byte[] compressedDetails) {
        assert (compressedDetails != null);
        ByteArrayInputStream bais = new ByteArrayInputStream(compressedDetails);
        InflaterInputStream dflIn = new InflaterInputStream(bais);
        try {
            ApiCallDetailsProtos.PBApiCallDetails
                    pbDetails = ApiCallDetailsProtos.PBApiCallDetails.parseFrom(dflIn);

            JFedConnection.ProxyInfo proxyInfo = null;
            if (pbDetails.hasProxyInfo()) {
                ApiCallDetailsProtos.PBProxyInfo pbProxyInfo = pbDetails.getProxyInfo();
                switch (pbProxyInfo.getType()) {
                    case SSH: {
                        proxyInfo = new JFedConnection.SshProxyInfo(pbProxyInfo.getHostname(),
                                pbProxyInfo.getPort(),
                                pbProxyInfo.getExtension(ApiCallDetailsProtos.PBSshProxyInfo.username), null,
                                pbProxyInfo.getExtension(ApiCallDetailsProtos.PBSshProxyInfo.hostkey),
                                false/*limitName*/);
                        break;
                    }
                    default: LOG.warn("Unsupported proxy type: "+pbProxyInfo.getType());
                }
            }

            SerializableApiCallDetails details = new SerializableApiCallDetails(pbDetails.getId(),
                    pbDetails.getAuthorityName(),
                    pbDetails.getAuthorityUrn(),
                    pbDetails.getBaseServerUrl(),
                    pbDetails.getCallServerUrl(),
                    proxyInfo,

                    pbDetails.getConnectionSslAuthUserUrn(),
                    pbDetails.getConnectionBasicHttpAuthUsername(),
                    pbDetails.getConnectionId(),
                    pbDetails.getConnectionSslAuthUserCertificates(),

                    pbDetails.getConnectionAuthenticationMethod(),
                    pbDetails.getConnectionProtocol(),
                    pbDetails.getConnectionType(),

                    pbDetails.getHttpRequestLine(),
                    pbDetails.getHttpRequestHeaders(),
                    pbDetails.getHttpRequest(),

                    pbDetails.getHttpStatusLine(),
                    pbDetails.getHttpResponseHeaders(),
                    pbDetails.getHttpReply(),

                    pbDetails.getApiName(),
                    pbDetails.getJavaMethodName(),
                    pbDetails.getGeniMethodName(),
                    pbDetails.getMethodParameters(),

                    pbDetails.getXmlRpcRequestJsonString(),
                    pbDetails.getXmlRpcReplyJsonString(),

                    pbDetails.getXmlRpcGeniValue(),
                    pbDetails.getXmlRpcGeniOutput(),
                    pbDetails.getXmlRpcGeniCode(),


                    pbDetails.hasGeniResponseCode() ? pbDetails.getGeniResponseCode() : null,
                    pbDetails.getGeniResponseCodeDescription(),
                    pbDetails.hasGeniCodeIsSuccess() ? pbDetails.getGeniCodeIsSuccess() : null,
                    pbDetails.getGeniResponseOutput(),

                    pbDetails.getRawValue(),

                    new Date(pbDetails.getStartTime()),
                    new Date(pbDetails.getStopTime()),
                    pbDetails.getProtogeniSpewLogUrl(),
                    pbDetails.hasExceptionString() ? pbDetails.getExceptionString() : null);
            return details;

        } catch (IOException e) {
            //this should not happen for in-memory stored PBApiCallDetails. When reading from disk,
            // this section should be improved
            LOG.error("Received error while reading compressed ApiCallDetails");
            throw new RuntimeException("Received error while reading compressed ApiCallDetails", e);
        } catch (Exception | AssertionError t) {
            LOG.warn("Received throwable while reading compressed ApiCallDetails", t);
            throw t;
        }

    }

    protected static byte[] compressFullApiDetails(SerializableApiCallDetails apiCallDetails) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DeflaterOutputStream dflOut = new DeflaterOutputStream(baos);

        ApiCallDetailsProtos.PBApiCallDetails.Builder builder = ApiCallDetailsProtos.PBApiCallDetails.newBuilder();
        builder.setId(apiCallDetails.getId());

        if (apiCallDetails.getAuthorityName() != null)
            builder.setAuthorityName(apiCallDetails.getAuthorityName());
        if (apiCallDetails.getAuthorityUrn() != null)
            builder.setAuthorityUrn(apiCallDetails.getAuthorityUrn());
        if (apiCallDetails.getBaseServerUrl() != null)
            builder.setBaseServerUrl(apiCallDetails.getBaseServerUrl());
        if (apiCallDetails.getCallServerUrl() != null)
            builder.setCallServerUrl(apiCallDetails.getCallServerUrl());

        if (apiCallDetails.getConnectionAuthenticationMethod() != null)
            builder.setConnectionAuthenticationMethod(apiCallDetails.getConnectionAuthenticationMethod());
        if (apiCallDetails.getConnectionProtocol() != null)
            builder.setConnectionProtocol(apiCallDetails.getConnectionProtocol());
        if (apiCallDetails.getConnectionType() != null)
            builder.setConnectionType(apiCallDetails.getConnectionType());


        if (apiCallDetails.getProxyInfo() != null) {
            ApiCallDetailsProtos.PBProxyInfo.Builder piBuilder = ApiCallDetailsProtos.PBProxyInfo.newBuilder();

            piBuilder.setType(apiCallDetails.getProxyInfo() instanceof JFedConnection.SshProxyInfo ?
                    ApiCallDetailsProtos.PBProxyInfo.Type.SSH : ApiCallDetailsProtos.PBProxyInfo.Type.Base);
            piBuilder.setHostname(apiCallDetails.getProxyInfo().getHostname())
                    .setPort(apiCallDetails.getProxyInfo().getPort());

            if (apiCallDetails.getProxyInfo() instanceof JFedConnection.SshProxyInfo) {
                JFedConnection.SshProxyInfo sshProxyInfo =
                        (JFedConnection.SshProxyInfo) apiCallDetails.getProxyInfo();
                piBuilder.setExtension(ApiCallDetailsProtos.PBSshProxyInfo.username, sshProxyInfo.getUsername());
                piBuilder.setExtension(ApiCallDetailsProtos.PBSshProxyInfo.hostkey, sshProxyInfo.getHostKey());
            }
            builder.setProxyInfo(piBuilder);
        }

        if (apiCallDetails.getConnectionSslAuthUserUrn() != null)
            builder.setConnectionSslAuthUserUrn(apiCallDetails.getConnectionSslAuthUserUrn());
        if (apiCallDetails.getConnectionBasicHttpAuthUsername() != null)
            builder.setConnectionBasicHttpAuthUsername(apiCallDetails.getConnectionBasicHttpAuthUsername());
        if (apiCallDetails.getConnectionId() != null)
            builder.setConnectionId(apiCallDetails.getConnectionId());
        if (apiCallDetails.getConnectionSslAuthUserCertificates() != null)
            builder.setConnectionSslAuthUserCertificates(apiCallDetails.getConnectionSslAuthUserCertificates());

        if (apiCallDetails.getHttpRequestStatusLine() != null)
            builder.setHttpRequestLine(apiCallDetails.getHttpRequestStatusLine());
        if (apiCallDetails.getHttpRequestHeaders() != null)
            builder.setHttpRequestHeaders(apiCallDetails.getHttpRequestHeaders());
        if (apiCallDetails.getHttpRequest() != null)
            builder.setHttpRequest(apiCallDetails.getHttpRequest());
        if (apiCallDetails.getHttpResponseStatusLine() != null)
            builder.setHttpStatusLine(apiCallDetails.getHttpResponseStatusLine());
        if (apiCallDetails.getHttpResponseHeaders() != null)
            builder.setHttpResponseHeaders(apiCallDetails.getHttpResponseHeaders());
        if (apiCallDetails.getHttpResponse() != null)
            builder.setHttpReply(apiCallDetails.getHttpResponse());

        if (apiCallDetails.getXmlRpcRequestJsonString() != null)
            builder.setXmlRpcRequestJsonString(apiCallDetails.getXmlRpcRequestJsonString());
        if (apiCallDetails.getXmlRpcResponseJsonString() != null)
            builder.setXmlRpcReplyJsonString(apiCallDetails.getXmlRpcResponseJsonString());

        if (apiCallDetails.getApiName() != null)
            builder.setApiName(apiCallDetails.getApiName());
        if (apiCallDetails.getJavaMethodName() != null)
            builder.setJavaMethodName(apiCallDetails.getJavaMethodName());
        if (apiCallDetails.getGeniMethodName() != null)
            builder.setGeniMethodName(apiCallDetails.getGeniMethodName());
        if (apiCallDetails.getMethodParameters() != null) {
            builder.setMethodParameters(apiCallDetails.getMethodParameters());
        }

        if (apiCallDetails.getXmlRpcGeniResponseValue() != null)
            builder.setXmlRpcGeniValue(apiCallDetails.getXmlRpcGeniResponseValue());
        if (apiCallDetails.getXmlRpcGeniResponseOutput() != null)
            builder.setXmlRpcGeniOutput(apiCallDetails.getXmlRpcGeniResponseOutput());
        if (apiCallDetails.getXmlRpcGeniResponseCode() != null)
            builder.setXmlRpcGeniCode(apiCallDetails.getXmlRpcGeniResponseCode());
        if (apiCallDetails.getGeniResponseCodeIsSuccess() != null)
            builder.setGeniCodeIsSuccess(apiCallDetails.getGeniResponseCodeIsSuccess());

        if (apiCallDetails.getGeniResponseCode() != null) {
            builder.setGeniResponseCode(apiCallDetails.getGeniResponseCode());
        }

        if (apiCallDetails.getGeniResponseOutput() != null) {
            builder.setGeniResponseOutput(apiCallDetails.getGeniResponseOutput());
        }
        if (apiCallDetails.getGeniResponseCodeDescription() != null) {
            builder.setGeniResponseCodeDescription(apiCallDetails.getGeniResponseCodeDescription());
        }

        if (apiCallDetails.getRawValue() != null) {
            builder.setRawValue(apiCallDetails.getRawValue());
        }

        if (apiCallDetails.getStartTime() != null)
            builder.setStartTime(apiCallDetails.getStartTime().getTime());
        if (apiCallDetails.getStopTime() != null)
            builder.setStopTime(apiCallDetails.getStopTime().getTime());

        if (apiCallDetails.getProtogeniSpewLogUrl() != null)
            builder.setProtogeniSpewLogUrl(apiCallDetails.getProtogeniSpewLogUrl());

        if (apiCallDetails.getExceptionString() != null)
            builder.setExceptionString(apiCallDetails.getExceptionString());

        try {
            builder.build().writeTo(dflOut);
            dflOut.close();
            baos.close();
        } catch (IOException e) {
            LOG.error("Received error while serializing ApiCallDetails", e);
        }

        return baos.toByteArray();
    }

    public abstract ApiCallDetailsRef registerApiCallDetails(ApiCallDetails apiCallDetails);

    /**
     * This method should only be used when restoring SerializableApiCallDetails from another jFed instance (ex. bugreport viewer)
     *
     * This may change the ID of the SerializableApiCallDetails!
     * @param apiCallDetails
     * @return
     */
    public abstract ApiCallDetailsRef registerApiCallDetails(SerializableApiCallDetails apiCallDetails);

    public SerializableApiCallDetails getApiCallDetails(ApiCallDetailsRef apiCallDetailsRef) {
        return callsCache.getUnchecked(apiCallDetailsRef);
    }

    protected abstract SerializableApiCallDetails getFullApiDetails(ApiCallDetailsRef ref);

    public abstract void clear();
}
