package ru.bitel.bgbilling.modules.tv.dyn;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import ru.bitel.bgbilling.common.BGException;
import ru.bitel.common.Utils;
import ru.bitel.common.io.Base64;
import ru.bitel.common.security.EyelessHostnameVerifier;
import ru.bitel.common.security.EyelessTrustManager;

public class JsonClient
{
    private static final Logger logger = LogManager.getLogger();

    public enum Method
    {
        get("GET"), put("PUT"), post("POST"), delete("DELETE"), patch("PATCH");

        public final String method;

        Method( final String method )
        {
            this.method = method;
        }
    }

    public class JsonClientException
        extends BGException
    {
        private static final long serialVersionUID = -621775327679438693L;

        private int responseCode;
        private String data;

        public JsonClientException( String message, int responseCode, String data )
        {
            super( message );

            this.responseCode = responseCode;
            this.data = data;
        }

        public int getResponseCode()
        {
            return responseCode;
        }

        public String getData()
        {
            return data;
        }
    }

    private static TrustManager[] trustManager = new TrustManager[] { new EyelessTrustManager() };

    private SSLContext sslContext;

    protected URL url;
    protected String username;
    protected String password;
    protected boolean basicAuth = true;

    protected String contentType = null;

    public JsonClient()
        throws Exception
    {
        this( null, null, null );
    }

    public JsonClient( URL url, String username, String password )
        throws Exception
    {
        this.url = url;
        this.username = username;
        this.password = password;

        this.sslContext = SSLContext.getInstance( "SSL" );
        this.sslContext.init( null, trustManager, new java.security.SecureRandom() );
    }

    public void setUrl( URL url )
    {
        this.url = url;
    }

    public void setBasicAuth( boolean basicAuth )
    {
        this.basicAuth = basicAuth;
    }

    public void setUsername( String username )
    {
        this.username = username;
    }

    public void setPassword( String password )
    {
        this.password = password;
    }

    private HttpURLConnection connection;

    private String lastResult;

    public String getLastResult()
    {
        return lastResult;
    }

    public void setContentType( String contentType )
    {
        this.contentType = contentType;
    }

    public String requestImpl0( final URL baseUrl, Method method, Map<String, String> headers, String resource, String id, String body, boolean form )
        throws IOException, BGException, JSONException
    {
        return this.requestImpl0( baseUrl, method, headers, resource, id, null, body, form );
    }

    public String requestImpl0( final URL baseUrl, Method method, Map<String, String> headers, String resource, String id, Map<String, Object> urlParams, String body,
                                   boolean form )
        throws IOException, BGException, JSONException
    {
        lastResult = null;

        // стандартный HttpURLConnection не хочет PATCH, чтобы не сломать случайно другие протоколы
        // через HttpClient вызываем пока только PATCH
        if( method == Method.patch )
        {
            try
            {
                return requestImplHttpClient( url, method, headers, resource, id, urlParams, body, form );
            }
            catch( URISyntaxException e )
            {
                throw new BGException( e );
            }
        }

        StringBuilder spec = new StringBuilder();
        spec.append( resource );

        if( id != null )
        {
            spec.append( "/" ).append( id );
        }

        if( urlParams != null && urlParams.size() > 0 )
        {
            String urlParamsString = mapToParams( urlParams );

            spec.append( '?' ).append( urlParamsString );
        }

        URL url = new URL( baseUrl, spec.toString() );
        connection = (HttpURLConnection)url.openConnection();

        if( connection instanceof HttpsURLConnection )
        {
            try
            {
                final HttpsURLConnection httpsConnection = (HttpsURLConnection)connection;

                httpsConnection.setSSLSocketFactory( sslContext.getSocketFactory() );
                httpsConnection.setHostnameVerifier( EyelessHostnameVerifier.INSTANCE );
            }
            catch( Exception e )
            {
                logger.error( e.getMessage(), e );
            }
        }

        connection.setRequestMethod( method.method );

        logger.info( method + ": " + url );

        if( this.basicAuth && this.username != null && this.password != null )
        {
            String authHeaderValue = "Basic " + Base64.encode( (this.username + ":" + this.password), "UTF-8" );
            connection.setRequestProperty( "Authorization", authHeaderValue );
        }

        if( form )
        {
            connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
        }
        else if( contentType != null )
        {
            connection.setRequestProperty( "Content-Type", contentType );
        }

        if( headers != null && headers.size() > 0 )
        {
            logger.info( headers );

            for( Map.Entry<String, String> e : headers.entrySet() )
            {
                connection.setRequestProperty( e.getKey(), e.getValue() );
            }
        }

        connection.setDoInput( true );
        connection.setDoOutput( true );

        if( body != null && body.length() > 0 )
        {
            logger.info( ">> " + body );

            byte[] data = body.getBytes( "UTF-8" );

            connection.setFixedLengthStreamingMode( data.length );

            OutputStream os = connection.getOutputStream();
            os.write( data );
            os.flush();
        }
        else
        {
            // connection.setFixedLengthStreamingMode( 0 );
        }

        InputStream inputStream;
        try
        {
            inputStream = connection.getInputStream();
        }
        catch( IOException exception )
        {
            inputStream = connection.getErrorStream();
        }

        final int responseCode = connection.getResponseCode();

        if( responseCode / 100 != 2 )
        {
            logger.info( "Response code = " + responseCode );
        }

        byte[] data = Utils.readByBlock( inputStream );

        String dataString = new String( data, StandardCharsets.UTF_8 );

        this.lastResult = dataString;

        logger.info( "<< " + dataString );
        
        if ( responseCode / 100 == 5 )
        {
            throw new BGException( "responseCode = " + responseCode );
        }

        if( responseCode / 100 != 2 )
        {
            throw new JsonClientException( "Ошибка при получении ответа (" + responseCode + ")", responseCode, dataString );
        }

        return dataString;
    }

    @Deprecated
    public String requestImplHttpClient( URL url, Method method, Map<String, String> headers, String resource, String id, String request, boolean form )
        throws IOException, BGException, JSONException, URISyntaxException
    {
        return this.requestImplHttpClient( url, method, headers, resource, id, null, request, form );
    }

    public String requestImplHttpClient( final URL baseUrl, Method method, Map<String, String> headers, String resource, String id, Map<String, Object> params, String body,
                                            boolean form )
        throws IOException, BGException, JSONException, URISyntaxException
    {
        lastResult = null;

        StringBuilder spec = new StringBuilder();
        spec.append( resource );

        if( id != null )
        {
            spec.append( "/" ).append( id );
        }

        if( params != null && params.size() > 0 )
        {
            String urlParamsString = mapToParams( params );

            spec.append( '?' ).append( urlParamsString );
        }

        URL url = new URL( baseUrl, spec.toString() );

        HttpRequestBase req;
        HttpEntityEnclosingRequestBase entityReq;

        switch( method )
        {
            case get:
                req = new HttpGet( url.toURI() );
                entityReq = null;
                break;

            case post:
                req = entityReq = new HttpPost( url.toURI() );
                break;

            case put:
                req = entityReq = new HttpPut( url.toURI() );
                break;

            case patch:
                req = entityReq = new HttpPatch( url.toURI() );
                break;

            case delete:
                req = new HttpDelete( url.toURI() );
                entityReq = null;
                break;

            default:
                throw new IllegalArgumentException( "method" );
        }

        logger.info( method + ": " + url );

        if( this.basicAuth && this.username != null && this.password != null )
        {
            String authHeaderValue = "Basic " + Base64.encode( (this.username + ":" + this.password), "UTF-8" );
            req.setHeader( "Authorization", authHeaderValue );
        }

        if( form )
        {
            req.setHeader( "Content-Type", "application/x-www-form-urlencoded" );
        }
        else if( contentType != null )
        {
            req.setHeader( "Content-Type", contentType );
        }

        if( headers != null && headers.size() > 0 )
        {
            logger.info( headers );

            for( Map.Entry<String, String> e : headers.entrySet() )
            {
                req.setHeader( e.getKey(), e.getValue() );
            }
        }

        if( body != null && body.length() > 0 && entityReq != null )
        {
            logger.info( ">> " + body );
            entityReq.setEntity( new StringEntity( body, StandardCharsets.UTF_8 ) );
        }
        else
        {
            // connection.setFixedLengthStreamingMode( 0 );
        }

        HttpResponse response = HttpClientBuilder.create().build().execute( req );

        final int responseCode = response.getStatusLine().getStatusCode();
        if( responseCode / 100 != 2 )
        {
            logger.info( "Response code = " + responseCode );
        }

        String dataString;
        // считываем json-ответ
        try( BufferedReader bufferedReader = new BufferedReader( new InputStreamReader( response.getEntity().getContent() ) ) )
        {
            StringBuilder result = new StringBuilder();
            for( String line; (line = bufferedReader.readLine()) != null; )
            {
                result.append( line );
            }
            dataString = result.toString();
        }

        this.lastResult = dataString;

        logger.info( "<< " + dataString );

        if( responseCode / 100 != 2 )
        {
            throw new JsonClientException( "Ошибка при получении ответа (" + responseCode + ")", responseCode, dataString );
        }

        return dataString;
    }

    public JSONObject requestImpl( URL url, Method method, Map<String, String> headers, String resource, String id, String body, boolean form )
        throws IOException, BGException, JSONException
    {
        return this.requestImpl( url, method, headers, resource, id, null, body, form );
    }

    public JSONObject requestImpl( URL url, Method method, Map<String, String> headers, String resource, String id, Map<String, Object> urlParams, String body, boolean form )
        throws IOException, BGException, JSONException
    {
        String dataString = requestImpl0( url, method, headers, resource, id, urlParams, body, form );

        try
        {
            return new JSONObject( dataString );
        }
        catch( JSONException ex )
        {
            logger.error( "Error parse as JSON: " + dataString );

            throw ex;
        }
    }

    public JSONObject request( URL url, Method method, Map<String, String> headers, String resource, String id, Map<String, Object> formParams )
        throws IOException, BGException, JSONException
    {
        return request( url, method, headers, resource, id, null, formParams );
    }

    public JSONObject request( URL url, Method method, Map<String, String> headers, String resource, String id, Map<String, Object> urlParams, Map<String, Object> formParams )
        throws IOException, BGException, JSONException
    {
        logger.info( urlParams );
        logger.info( formParams );

        String formParamsString = mapToParams( formParams );

        return requestImpl( url, method, headers, resource, id, urlParams, formParamsString, true );
    }

    private String mapToParams( Map<String, Object> formParams )
        throws UnsupportedEncodingException
    {
        if( formParams == null || formParams.size() == 0 )
        {
            return "";
        }

        StringBuilder sb = new StringBuilder();

        for( Map.Entry<String, Object> e : formParams.entrySet() )
        {
            final String key = e.getKey();
            final Object value = e.getValue();

            if( value instanceof Collection<?> )
            {
                for( Object o : (Collection<?>)value )
                {
                    sb.append( key );
                    sb.append( '=' );
                    if( o != null )
                    {
                        sb.append( URLEncoder.encode( o.toString(), "UTF-8" ) );
                    }
                    sb.append( '&' );
                }
            }
            else
            {
                sb.append( key );
                sb.append( '=' );
                if( value != null )
                {
                    sb.append( URLEncoder.encode( value.toString(), "UTF-8" ) );
                }
                sb.append( '&' );
            }
        }

        if( sb.length() > 0 )
        {
            sb.setLength( sb.length() - 1 );
        }

        return sb.toString();
    }

    public JSONObject request( Method method, Map<String, String> headers, String resource, String id, Map<String, Object> formParams )
        throws IOException, BGException, JSONException
    {
        return this.request( this.url, method, headers, resource, id, formParams );
    }

    public JSONObject request( Method method, Map<String, String> headers, String resource, String id, Map<String, Object> urlParams, Map<String, Object> formParams )
        throws IOException, BGException, JSONException
    {
        return this.request( this.url, method, headers, resource, id, urlParams, formParams );
    }

    public JSONObject invoke( URL url, Method method, Map<String, String> headers, String resource, String id, Object obj )
        throws IOException, BGException, JSONException
    {
        return requestImpl( url, method, headers, resource, id, objToString( obj ), false );
    }

    public JSONArray invokeAndGetArray( URL url, Method method, Map<String, String> headers, String resource, String id, Object obj )
        throws IOException, BGException, JSONException
    {
        String dataString = requestImpl0( url, method, headers, resource, id, objToString( obj ), false );

        try
        {
            return new JSONArray( dataString );
        }
        catch( JSONException ex )
        {
            logger.error( "Error parse as JSON: " + dataString );

            throw ex;
        }
    }

    public Object invokeAndGetObject( URL url, Method method, Map<String, String> headers, String resource, String id, JSONObject obj )
        throws IOException, BGException, JSONException
    {
        final String dataString = requestImpl0( url, method, headers, resource, id, objToString( obj ), false );
        try
        {
            JSONTokener tokener = new JSONTokener( dataString );
            return tokener.nextValue();
        }
        catch( JSONException ex )
        {
            logger.error( "Error parse as JSON: " + dataString );

            throw ex;
        }
    }
    
    private String objToString( Object obj )
        throws IllegalArgumentException
    {
        String str = "";
        if( obj != null )
        {
            if( obj instanceof JSONObject || obj instanceof JSONArray )
                str = obj.toString();
            else
                throw new IllegalArgumentException( "obj is not JSONObject AND not JSONArray" );
        }

        return str;
    }

    public Object invokeAndGetObject( Method method, Map<String, String> headers, String resource, String id, JSONObject obj )
        throws IOException, BGException, JSONException
    {
        return invokeAndGetObject( this.url, method, headers, resource, id, obj );
    }

    public JSONObject invoke( Method method, Map<String, String> headers, String resource, String id, Object obj )
        throws IOException, BGException, JSONException
    {
        return this.invoke( this.url, method, headers, resource, id, obj );
    }

    public JSONArray invokeAndGetArray( Method method, Map<String, String> headers, String resource, String id, Object obj )
        throws IOException, BGException, JSONException
    {
        return this.invokeAndGetArray( this.url, method, headers, resource, id, obj );
    }

    public void disconnect()
    {
        if( connection != null )
        {
            connection.disconnect();
            connection = null;
        }
    }

    public Request newRequest()
    {
        return new Request().setBaseUrl( this.url );
    }

    public class Request
    {
        URL baseUrl;
        Method method;
        Map<String, String> headers;
        String resource;
        String id;
        Map<String, Object> params;
        Map<String, Object> formParams;
        Object body;
        boolean form;

        public Request()
        {
        }

        public Request setBaseUrl( URL baseUrl )
        {
            this.baseUrl = baseUrl;
            return this;
        }

        public Request setMethod( Method method )
        {
            this.method = method;
            return this;
        }

        public Request setHeader( String name, String value )
        {
            if( this.headers == null )
            {
                this.headers = new HashMap<>();
            }

            this.headers.put( name, value );
            return this;
        }

        public Request setResource( String resource )
        {
            this.resource = resource;
            return this;
        }

        public Request setId( Object id )
        {
            this.id = String.valueOf( id );
            return this;
        }

        public Request setParam( String name, Object value )
        {
            if( this.params == null )
            {
                this.params = new HashMap<>();
            }

            this.params.put( name, value );
            return this;
        }

        public Request setFormParam( String name, Object value )
        {
            if( this.body != null )
            {
                throw new IllegalStateException( "You cannot set the form parameter if you have already set the body." );
            }

            if( this.formParams == null )
            {
                this.formParams = new HashMap<>();
            }

            this.form = true;
            this.formParams.put( name, value );
            return this;
        }

        public Request setBody( Object body )
        {
            if( this.formParams != null )
            {
                throw new IllegalStateException( "You cannot set the body if you have already set the form parameter." );
            }

            this.body = body;
            return this;
        }

        public Request setForm( boolean form )
        {
            this.form = form;
            return this;
        }

        public Response execute()
            throws JSONException, IOException, BGException
        {
            String body;
            if( formParams != null )
            {
                body = mapToParams( formParams );
            }
            else if( this.body != null )
            {
                if( this.body instanceof String )
                {
                    body = (String)this.body;
                }
                else
                {
                    StringWriter sb = new StringWriter();

                    if( this.body instanceof JSONObject )
                    {
                        ((JSONObject)this.body).write( sb );
                    }
                    else if( this.body instanceof JSONArray )
                    {
                        ((JSONArray)this.body).write( sb );
                    }
                    else
                    {
                        throw new IllegalArgumentException( "body" );
                    }

                    body = sb.toString();
                }
            }
            else
            {
                body = null;
            }
            
            String resultString = null;
            JsonClientException resultException = null;

            try
            {
                resultString = requestImpl0( baseUrl, method, headers, resource, id, params, body, form );
            }
            catch( JsonClientException ex )
            {
                resultException = ex;
            }

            return new Response( resultString, resultException );
        }
    }

    public class Response
    {
        private final String dataString;
        private final JsonClientException exception;

        private Response( String dataString, JsonClientException exception )
        {
            this.dataString = dataString;
            this.exception = exception;
        }

        private void checkError()
            throws JsonClientException
        {
            if( exception != null )
            {
                throw exception;
            }
        }

        public boolean hasError()
        {
            return exception != null;
        }

        public JsonClientException getException()
        {
            return exception;
        }

        public String getString()
        {
            return dataString;
        }

        public Object getObject()
            throws JsonClientException
        {
            checkError();

            try
            {
                JSONTokener tokener = new JSONTokener( dataString );
                return tokener.nextValue();
            }
            catch( JSONException ex )
            {
                logger.error( "Error parse as JSON: " + dataString );

                throw ex;
            }
        }

        public JSONObject getJsonObject()
            throws JsonClientException
        {
            return (JSONObject)getObject();
        }

        public JSONArray getJsonArray()
            throws JsonClientException
        {
            return (JSONArray)getObject();
        }
    }
}
