package com.android.hotspot2.osu;

import android.util.Log;

import com.android.hotspot2.utils.HTTPMessage;
import com.android.hotspot2.utils.HTTPRequest;
import com.android.hotspot2.utils.HTTPResponse;

import com.android.org.conscrypt.OpenSSLSocketImpl;

import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.xml.parsers.ParserConfigurationException;

public class HTTPHandler implements AutoCloseable {
    private final Charset mCharset;
    private final OSUSocketFactory mSocketFactory;
    private Socket mSocket;
    private BufferedOutputStream mOut;
    private BufferedInputStream mIn;
    private final String mUser;
    private final byte[] mPassword;
    private boolean mHTTPAuthPerformed;
    private static final AtomicInteger sSequence = new AtomicInteger();

    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory) throws IOException {
        this(charset, socketFactory, null, null);
    }

    public HTTPHandler(Charset charset, OSUSocketFactory socketFactory,
                       String user, byte[] password) throws IOException {
        mCharset = charset;
        mSocketFactory = socketFactory;
        mSocket = mSocketFactory.createSocket();
        mOut = new BufferedOutputStream(mSocket.getOutputStream());
        mIn = new BufferedInputStream(mSocket.getInputStream());
        mUser = user;
        mPassword = password;
    }

    public boolean isHTTPAuthPerformed() {
        return mHTTPAuthPerformed;
    }

    public X509Certificate getOSUCertificate(URL osu) throws GeneralSecurityException {
        return mSocketFactory.getOSUCertificate(osu);
    }

    public void renegotiate(Map<OSUCertType, List<X509Certificate>> certs, PrivateKey key)
            throws IOException {
        if (!(mSocket instanceof SSLSocket)) {
            throw new IOException("Not a TLS connection");
        }
        if (certs != null) {
            mSocketFactory.reloadKeys(certs, key);
        }
        ((SSLSocket) mSocket).startHandshake();
    }

    public byte[] getTLSUnique() throws SSLException {
        if (mSocket instanceof OpenSSLSocketImpl) {
            return ((OpenSSLSocketImpl) mSocket).getChannelId();
        }
        return null;
    }

    public OSUResponse exchangeSOAP(URL url, String message) throws IOException {
        HTTPResponse response = exchangeWithRetry(url, message, HTTPMessage.Method.POST,
                HTTPMessage.ContentTypeSOAP);
        if (response.getStatusCode() >= 300) {
            throw new IOException("Bad HTTP status code " + response.getStatusCode());
        }
        try {
            SOAPParser parser = new SOAPParser(response.getPayloadStream());
            return parser.getResponse();
        } catch (ParserConfigurationException | SAXException e) {
            ByteBuffer x = response.getPayload();
            byte[] b = new byte[x.remaining()];
            x.get(b);
            Log.w("XML", "Bad: '" + new String(b, StandardCharsets.ISO_8859_1));
            throw new IOException(e);
        }
    }

    public ByteBuffer exchangeBinary(URL url, String message, String contentType)
            throws IOException {
        HTTPResponse response =
                exchangeWithRetry(url, message, HTTPMessage.Method.POST, contentType);
        return response.getBinaryPayload();
    }

    public InputStream doGet(URL url) throws IOException {
        HTTPResponse response = exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
        return response.getPayloadStream();
    }

    public HTTPResponse doGetHTTP(URL url) throws IOException {
        return exchangeWithRetry(url, null, HTTPMessage.Method.GET, null);
    }

    private HTTPResponse exchangeWithRetry(URL url, String message, HTTPMessage.Method method,
                                           String contentType) throws IOException {
        HTTPResponse response = null;
        int retry = 0;
        for (; ; ) {
            try {
                response = httpExchange(url, message, method, contentType);
                break;
            } catch (IOException ioe) {
                close();
                retry++;
                if (retry > 3) {
                    break;
                }
                Log.d(OSUManager.TAG, "Failed HTTP exchange, retry " + retry);
                mSocket = mSocketFactory.createSocket();
                mOut = new BufferedOutputStream(mSocket.getOutputStream());
                mIn = new BufferedInputStream(mSocket.getInputStream());
            }
        }
        if (response == null) {
            throw new IOException("Failed to establish connection to peer");
        }
        return response;
    }

    private HTTPResponse httpExchange(URL url, String message, HTTPMessage.Method method,
                                      String contentType)
            throws IOException {
        HTTPRequest request = new HTTPRequest(message, mCharset, method, url, contentType, false);
        request.send(mOut);
        HTTPResponse response = new HTTPResponse(mIn);
        Log.d(OSUManager.TAG, "HTTP code " + response.getStatusCode() + ", user " + mUser +
                ", pw " + (mPassword != null ? '\'' + new String(mPassword) + '\'' : "-"));
        if (response.getStatusCode() == 401) {
            if (mUser == null) {
                throw new IOException("Missing user name for HTTP authentication");
            }
            try {
                request = new HTTPRequest(message, StandardCharsets.ISO_8859_1, method, url,
                        contentType, true);
                request.doAuthenticate(response, mUser, mPassword, url,
                        sSequence.incrementAndGet());
                request.send(mOut);
                mHTTPAuthPerformed = true;
            } catch (GeneralSecurityException gse) {
                throw new IOException(gse);
            }

            response = new HTTPResponse(mIn);
        }
        return response;
    }

    public void close() throws IOException {
        mIn.close();
        mOut.close();
        mSocket.close();
    }
}
