/*
 * This file is part of ComputerCraft - http://www.computercraft.info
 * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
 * Send enquiries to dratcliffe@gmail.com
 */
package dan200.computercraft.core.apis.http;

import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.shared.util.ThreadUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.timeout.ReadTimeoutException;

import javax.annotation.Nonnull;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.KeyStore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Just a shared object for executing simple HTTP related tasks.
 */
public final class NetworkUtils
{
    public static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
        4, Integer.MAX_VALUE,
        60L, TimeUnit.SECONDS,
        new SynchronousQueue<>(),
        ThreadUtils.builder( "Network" )
            .setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
            .build()
    );

    public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, ThreadUtils.builder( "Netty" )
        .setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
        .build()
    );

    private NetworkUtils()
    {
    }

    private static final Object sslLock = new Object();
    private static TrustManagerFactory trustManager;
    private static SslContext sslContext;
    private static boolean triedSslContext = false;

    private static TrustManagerFactory getTrustManager()
    {
        if( trustManager != null ) return trustManager;
        synchronized( sslLock )
        {
            if( trustManager != null ) return trustManager;

            TrustManagerFactory tmf = null;
            try
            {
                tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() );
                tmf.init( (KeyStore) null );
            }
            catch( Exception e )
            {
                ComputerCraft.log.error( "Cannot setup trust manager", e );
            }

            return trustManager = tmf;
        }
    }

    public static SslContext getSslContext() throws HTTPRequestException
    {
        if( sslContext != null || triedSslContext ) return sslContext;
        synchronized( sslLock )
        {
            if( sslContext != null || triedSslContext ) return sslContext;
            try
            {
                return sslContext = SslContextBuilder
                    .forClient()
                    .trustManager( getTrustManager() )
                    .build();
            }
            catch( SSLException e )
            {
                ComputerCraft.log.error( "Cannot construct SSL context", e );
                triedSslContext = true;
                sslContext = null;

                throw new HTTPRequestException( "Cannot create a secure connection" );
            }
        }
    }

    /**
     * Create a {@link InetSocketAddress} from a {@link java.net.URI}.
     *
     * Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
     *
     * @param uri The URI to fetch.
     * @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified.
     * @return The resolved address.
     * @throws HTTPRequestException If the host is not malformed.
     */
    public static InetSocketAddress getAddress( URI uri, boolean ssl ) throws HTTPRequestException
    {
        return getAddress( uri.getHost(), uri.getPort(), ssl );
    }

    /**
     * Create a {@link InetSocketAddress} from the resolved {@code host} and port.
     *
     * Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
     *
     * @param host The host to resolve.
     * @param port The port, or -1 if not defined.
     * @param ssl  Whether to connect with SSL. This is used to find the default port if not otherwise specified.
     * @return The resolved address.
     * @throws HTTPRequestException If the host is not malformed.
     */
    public static InetSocketAddress getAddress( String host, int port, boolean ssl ) throws HTTPRequestException
    {
        if( port < 0 ) port = ssl ? 443 : 80;
        InetSocketAddress socketAddress = new InetSocketAddress( host, port );
        if( socketAddress.isUnresolved() ) throw new HTTPRequestException( "Unknown host" );
        return socketAddress;
    }

    /**
     * Get options for a specific domain.
     *
     * @param host    The host to resolve.
     * @param address The address, resolved by {@link #getAddress(String, int, boolean)}.
     * @return The options for this host.
     * @throws HTTPRequestException If the host is not permitted
     */
    public static Options getOptions( String host, InetSocketAddress address ) throws HTTPRequestException
    {
        Options options = AddressRule.apply( ComputerCraft.httpRules, host, address );
        if( options.action == Action.DENY ) throw new HTTPRequestException( "Domain not permitted" );
        return options;
    }

    /**
     * Read a {@link ByteBuf} into a byte array.
     *
     * @param buffer The buffer to read.
     * @return The resulting bytes.
     */
    public static byte[] toBytes( ByteBuf buffer )
    {
        byte[] bytes = new byte[buffer.readableBytes()];
        buffer.readBytes( bytes );
        return bytes;
    }

    @Nonnull
    public static String toFriendlyError( @Nonnull Throwable cause )
    {
        if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException )
        {
            return cause.getMessage();
        }
        else if( cause instanceof TooLongFrameException )
        {
            return "Message is too large";
        }
        else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
        {
            return "Timed out";
        }
        else if( cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException) )
        {
            return "Could not create a secure connection";
        }
        else
        {
            return "Could not connect";
        }
    }
}
