package com.jscape.inet.proxy;

import com.jscape.inet.http.protocol.HttpClient;
import com.jscape.inet.http.protocol.Response;
import com.jscape.inet.http.protocol.marshaling.ClientCodec;
import com.jscape.inet.http.protocol.messages.*;
import com.jscape.util.Base64Format;
import com.jscape.util.StringTools;
import com.jscape.util.io.DataCodec;
import com.jscape.util.io.NullInputStream;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.Connection;
import com.jscape.util.net.connection.SyncMessageConnection;
import com.jscape.util.net.connection.SyncMessageConnectionSyncRawBase;
import com.jscape.util.net.connection.SyncRawConnection;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * @author Alex Usun
 */
public class HttpProxy
        implements Proxy {

    private static final DataCodec<Message> MESSAGE_CODEC = ClientCodec.defaultCodec();

    private final Logger logger;

    public HttpProxy(Logger logger) {
        assertNotNull(logger);
        this.logger = logger;
    }

    @Override
    public SyncRawConnection openConnection(SyncRawConnection connection, TransportAddress address, String username, String password)
            throws IOException {
        SyncMessageConnection<Message> messageConnection = new SyncMessageConnectionSyncRawBase<>(connection, MESSAGE_CODEC);
    
        String uri = String.format("%s:%s", address.getHost(), address.getPort());
        Header[] headers = StringTools.isValidString(username) ? new Header[] {authenticationHeaderFor(username, password)} : new Header[0];
        ConnectRequest requestMessage = new ConnectRequest(uri, "HTTP/1.1", headers, new NullInputStream());
        exchange(requestMessage, messageConnection);
        
        return connection;
    }

    protected Header authenticationHeaderFor(String username, String password) {
        String credentials = String.format("%s:%s", username, password != null ? password : "");
        return new Header("Proxy-Authorization", String.format("Basic %s", Base64Format.formatData(credentials.getBytes(StandardCharsets.UTF_8))));
    }

    protected Response exchange(RequestMessage request, SyncMessageConnection<Message> connection)
            throws IOException {
        send(request, connection);
        return receive(connection);
    }
    
    protected void send(RequestMessage message, SyncMessageConnection<Message> connection)
            throws IOException {
        logOutgoingMessage(connection, message);
        connection.write(message);
    }
    
    protected Response receive(SyncMessageConnection<Message> connection)
            throws IOException {
        ResponseMessage message = (ResponseMessage) connection.read();
        logIncomingMessage(connection, message);
        
        Response response = responseFor(message);
        assertNoErrors(response);
        
        return response;
    }
    
    protected Response responseFor(ResponseMessage message)
            throws IOException {
        return new Response(message.protocolVersion, message.statusCode, message.reasonPhrase, new Headers(message.headers), new NullInputStream(), new Headers());
    }
    
    protected void assertNoErrors(Response response)
            throws IOException {
        if (response.error()) {
            throw new HttpClient.ServerException(response);
        }
    }

    private void logOutgoingMessage(Connection connection, Message message) {
        if (this.logger.isLoggable(Level.FINER)) {
            this.logger.log(Level.FINER, String.format("Outgoing HTTP proxy message at [%s <-> %s]: %s.",
                                                       connection.localAddress(),
                                                       connection.remoteAddress(),
                                                       message));
        }
    }

    private void logIncomingMessage(Connection connection, Message message) {
        if (this.logger.isLoggable(Level.FINER)) {
            this.logger.log(Level.FINER,
                            String.format("Incoming HTT proxy message at [%s <-> %s]: %s.",
                                          connection.localAddress(),
                                          connection.remoteAddress(),
                                          message));
        }
    }
}
