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

import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

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.bgbilling.kernel.network.http.BGHttpClient;
import ru.bitel.common.io.Base64;

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;
        }
    }

    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;
    }

    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 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;

        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() );

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

        try
        {
            HttpRequest.Builder requestBuilder = HttpRequest.newBuilder( url.toURI() );

            if ( this.basicAuth && this.username != null && this.password != null )
            {
                String authHeaderValue = "Basic " + Base64.encodeUTF8( this.username + ":" + this.password );
                requestBuilder.header( "Authorization", authHeaderValue );
            }

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

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

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

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

            HttpRequest httpRequest = requestBuilder
                .method( method.method, body != null && body.length() > 0 ? BodyPublishers.ofString( body ) : BodyPublishers.noBody() )
                .build();

            HttpResponse<String> response = BGHttpClient
                .getTrustAllWithConnectTimeout3()
                .send( httpRequest, BodyHandlers.ofString() );

            final int responseCode = response.statusCode();

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

            String dataString = response.body();

            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;
        }
        catch( URISyntaxException ex )
        {
            throw new BGException( ex );
        }
        catch( InterruptedException ex )
        {
            Thread.currentThread().interrupt();

            throw new BGException( ex );
        }
    }

    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 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();
        }
    }
}
