package com.avg.zaap.crashlibrary.publish.cap.http;

import android.support.annotation.NonNull;

import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.publish.cap.http.boundary.IBoundaryGenerator;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;

/**
 * This class represents a single HTTP CAP request.
 * <p>
 * Created by marek.osvald on 7.6.2016.
 */
public class CapRequest {

    private static final String TAG = CapRequest.class.getSimpleName();
    private static final String CRLF = "\r\n";
    private static final int BUFFER_SIZE = 4096; // bytes
    private static final char SPACE = ' ';
    private static final String SEPARATOR = "--";

    private static final String CONTENT_TYPE = "Content-Type";
    private static final String MULTIPART_FORM_DATA_WITH_BOUNDARY = "multipart/form-data; boundary=";

    private IBoundaryGenerator mBoundaryGenerator;
    private String mBoundary;
    private HttpURLConnection mConnection;

    private OutputStream mOutputStream;
    private Writer mWriter;

    private ICapRequestFactory mFactory;

    /**
     * Default constructor. Uses default dependency factory.
     */
    public CapRequest() {
        this(new CapRequestFactory());
    }

    /**
     * Parametric constructor. Sets dependency factory to be used.
     *
     * @param factory dependency factory to be used
     */
    public CapRequest(ICapRequestFactory factory) {
        mBoundaryGenerator = factory.getBoundaryGenerator();
        mFactory = factory;
    }

    /**
     * Initializes the CAP request by specifying the URL.
     *
     * @param url URL to open
     * @return this instance
     * @throws CapRequestException provided that connection fails upon MalformedURLException or
     *                             IO exception
     */
    @SuppressWarnings("UnusedReturnValue")
    @NonNull
    public CapRequest openURL(String url) throws CapRequestException {
        CrashLogger.d(TAG, String.format("openURL() called, url: %s", url));

        mBoundary = mBoundaryGenerator.generateBoundary();

        try {
            URL connectionURL = mFactory.openURL(url);
            mConnection = (HttpURLConnection) connectionURL.openConnection();
            mConnection.setRequestProperty(CONTENT_TYPE, MULTIPART_FORM_DATA_WITH_BOUNDARY + mBoundary);

            mConnection.setUseCaches(false);
            mConnection.setDoOutput(true);
            mConnection.setDoInput(true);

            mOutputStream = mConnection.getOutputStream();
            mWriter = mFactory.getPrintWriter(mOutputStream);

        } catch (MalformedURLException ex) {
            CrashLogger.e(TAG, "Malformed URL, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
            throw new CapRequestException("Malformed URL", ex);
        } catch (IOException ex) {
            CrashLogger.e(TAG, "IO error, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
            throw new CapRequestException("IO exception", ex);
        }

        return this;
    }

    /**
     * Adds basic text form value.
     *
     * @param key   key to add
     * @param value value to add
     * @return this instance
     * @throws CapRequestException provided that connection fails on IOException
     */
    @SuppressWarnings("UnusedReturnValue")
    @NonNull
    public CapRequest addFormValue(String key, String value) throws CapRequestException {
        CrashLogger.d(TAG, "addFormValue() called, key: %s, value: %s", key, value);

        if (mWriter != null) {
            try {
                mWriter.append(SEPARATOR).append(mBoundary).append(CRLF)
                        .append("Content-Disposition: form-data; name=\"").append(key).append('\"').append(CRLF)
                        .append("Content-Type: text").append(CRLF)
                        .append(CRLF)
                        .append(value).append(CRLF)
                        .flush();

            } catch (IOException ex) {
                CrashLogger.e(TAG, "IO error occurred, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
                throw new CapRequestException("IO Exception", ex);
            }

            return this;
        } else {
            throw new CapRequestException("URL not open!");
        }
    }

    /**
     * Adds CAP exception header.
     *
     * @param exceptionHeader exception header to add
     * @return this instance
     * @throws CapRequestException provided that connection fails on IOException
     */
    @SuppressWarnings("UnusedReturnValue")
    @NonNull
    public CapRequest addExceptionHeader(Map<String, String> exceptionHeader) throws CapRequestException {
        CrashLogger.d(TAG, "addExceptionHeader() called");

        if (mWriter != null) {
            try {
                //noinspection SpellCheckingInspection
                mWriter.append(SEPARATOR).append(mBoundary).append(CRLF)
                        .append("Content-Disposition: form-data; name=\"excpre\"; filename=\"header.exh\"")
                        .append(CRLF)
                        .append("Content-Type: multipart/form-data").append(CRLF)
                        .append(CRLF)
                        .flush();

                for (Map.Entry<String, String> entry : exceptionHeader.entrySet()) {
                    mWriter.append(entry.getKey())
                            .append(SPACE)
                            .append(entry.getValue())
                            .append(CRLF);
                }

                mWriter.flush();

            } catch (IOException ex) {
                CrashLogger.e(TAG, "IO error occurred, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
                throw new CapRequestException("IO Exception", ex);
            }

            return this;
        } else {
            throw new CapRequestException("URL not open!");
        }
    }

    /**
     * Adds a file in a binary form.
     *
     * @param fileName file name to add
     * @return this instance
     * @throws CapRequestException provided that connection fail on an IO exception
     */
    @SuppressWarnings("UnusedReturnValue")
    @NonNull
    public CapRequest addFile(String fileName) throws CapRequestException {
        CrashLogger.d(TAG, "addFile() called, fileName: %s", fileName);

        if (mWriter != null) {
            FileInputStream inputStream = null;

            try {
                File file = mFactory.openFile(fileName);

                //noinspection SpellCheckingInspection
                mWriter.append(SEPARATOR).append(mBoundary).append(CRLF)
                        .append("Content-Disposition: form-data; name=\"upfiles[]\"; filename=\"").append(file.getName()).append('"').append(CRLF)
                        .append("Content-Type: multipart/form-data").append(CRLF)
                        .append("Content-Transfer-Encoding: binary").append(CRLF)
                        .append(CRLF)
                        .flush();

                inputStream = mFactory.getFileInputStream(file);
                byte[] buffer = new byte[BUFFER_SIZE];
                int bytesRead;

                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    mOutputStream.write(buffer, 0, bytesRead);
                }

                mOutputStream.flush();
                mWriter.append(CRLF);
                mWriter.flush();

            } catch (IOException ex) {
                CrashLogger.e(TAG, "IO error occurred, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
                throw new CapRequestException("IO Exception", ex);
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException ex) { /* ignored */ }
                }
            }

            return this;

        } else {
            throw new CapRequestException("URL not open!");
        }
    }

    /**
     * Sends the prepared request.
     *
     * @return an instance of {@link CapResponse} structure
     * @throws CapRequestException provided that request fails with an IO exception
     */
    @NonNull
    public CapResponse send() throws CapRequestException {
        CrashLogger.d(TAG, "send() called");

        try {
            finish();

            int responseCode = mConnection.getResponseCode();
            String responseMessage = mConnection.getResponseMessage();

            char[] buffer = new char[BUFFER_SIZE];
            final InputStream inputStream = mConnection.getInputStream();
            final InputStreamReader inputStreamReader = mFactory.getInputStreamReader(inputStream);

            int read;

            Writer stringWriter = mFactory.getStringWriter();

            while ((read = inputStreamReader.read(buffer)) != -1) {
                stringWriter.write(buffer, 0, read);
                CrashLogger.d(TAG, "Read %s bytes", read);
            }

            CrashLogger.d(TAG, "Input stream end");
            String responseBody = stringWriter.toString();
            int splitIndex = responseBody.indexOf(SPACE);

            int capCode;
            String capMessage = "";

            try {
                if (splitIndex != -1) {
                    capCode = Integer.valueOf(responseBody.substring(0, splitIndex));
                    capMessage = responseBody.substring(splitIndex + 1);
                } else {
                    capCode = Integer.valueOf(responseBody);
                }
            } catch (NumberFormatException ex) {
                CrashLogger.e(TAG, "Response body does not contain a number, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
                capCode = CapResponseCode.FATAL;
            }

            return new CapResponse(responseCode, responseMessage, capCode, capMessage);

        } catch (IOException ex) {
            CrashLogger.d(TAG, "IO error, original exception %s: %s", ex.getClass().getSimpleName(), ex.getMessage());
            throw new CapRequestException("IO exception", ex);
        } finally {
            try {
                if (mWriter != null) {
                    mWriter.close();
                }
            } catch (IOException e) {
                CrashLogger.e(TAG, "Unable to close the writer.");
            }
        }
    }

    private void finish() throws IOException, CapRequestException {
        if (mWriter != null) {
            mWriter.append(SEPARATOR)
                    .append(mBoundary)
                    .append(SEPARATOR)
                    .append(CRLF)
                    .flush();
        } else {
            throw new CapRequestException("URL not open!");
        }
    }

}
