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.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collection;
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.HttpClient;
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.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 = Logger.getLogger( JsonClient.class );

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

	protected String requestImpl0( URL url, Method method, Map<String, String> requestProperties, String resource, String id, String request, boolean form )
	    throws IOException, BGException, JSONException
	{
		lastResult = null;

		// стандартный HttpURLConnection не хочет PATCH, чтобы не сломать случайно другие протоколы
		// через HttpClient вызываем пока только PATCH
		if( method == Method.patch )
		{
			try
			{
				return requestImplHttpClient( url, method, requestProperties, resource, id, request, form );
			}
			catch( URISyntaxException e )
			{
				throw new BGException( e );
			}
		}
		
		url = new URL( url, resource + (id != null ? ("/" + id) : "") );
		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( requestProperties != null && requestProperties.size() > 0 )
		{
			logger.info( requestProperties );

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

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

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

			byte[] data = request.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, "UTF-8" );
		
		this.lastResult = dataString;

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

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

		return dataString;
	}

	protected String requestImplHttpClient( URL url, Method method, Map<String, String> requestProperties, String resource, String id, String request, boolean form )
		throws IOException, BGException, JSONException, URISyntaxException
	{
		lastResult = null;

		url = new URL( url, resource + (id != null ? ("/" + id) : "") );

		HttpClient httpClient = HttpClientBuilder.create().build();

		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( requestProperties != null && requestProperties.size() > 0 )
		{
			logger.info( requestProperties );

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

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

			StringEntity input = new StringEntity( request );
			entityReq.setEntity( input );
		}
		else
		{
			// connection.setFixedLengthStreamingMode( 0 );
		}

		HttpResponse response = httpClient.execute( req );

		final int responseCode = response.getStatusLine().getStatusCode();

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

		String dataString = "";

		//считываем json-ответ
		try (InputStreamReader inputStreamReader = new InputStreamReader( response.getEntity().getContent() ); BufferedReader rd = new BufferedReader( inputStreamReader ))
		{
			StringBuilder result = new StringBuilder();

			for( String line; (line = rd.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;
	}

	protected JSONObject requestImpl( URL url, Method method, Map<String, String> requestProperties, String resource, String id, String request, boolean form )
		throws IOException, BGException, JSONException
	{
		String dataString = requestImpl0( url, method, requestProperties, resource, id, request, 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> requestProperties, String resource, String id, Map<String, Object> params )
	    throws IOException, BGException, JSONException
	{
		logger.info( params );

		StringBuilder sb = new StringBuilder();

		if( params != null && params.size() > 0 )
		{
			for( Map.Entry<String, Object> e : params.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 requestImpl( url, method, requestProperties, resource, id, sb.toString(), true );
	}

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

	public JSONObject invoke( URL url, Method method, Map<String, String> requestProperties, String resource, String id, Object obj )
		throws IOException, BGException, JSONException
	{
		StringWriter sb = new StringWriter();

		if( obj != null )
		{
			if( obj instanceof JSONObject )
			{
				((JSONObject)obj).write( sb );
			}
			else if( obj instanceof JSONArray )
			{
				((JSONArray)obj).write( sb );
			}
			else
			{
				throw new IllegalArgumentException( "obj" );
			}
		}

		return requestImpl( url, method, requestProperties, resource, id, sb.toString(), false );
	}

	public JSONArray invokeAndGetArray( URL url, Method method, Map<String, String> requestProperties, String resource, String id, Object obj )
		throws IOException, BGException, JSONException
	{
		StringWriter sb = new StringWriter();

		if( obj != null )
		{
			if( obj instanceof JSONObject )
			{
				((JSONObject)obj).write( sb );
			}
			else if( obj instanceof JSONArray )
			{
				((JSONArray)obj).write( sb );
			}
			else
			{
				throw new IllegalArgumentException( "obj" );
			}
		}

		String dataString = requestImpl0( url, method, requestProperties, resource, id, sb.toString(), 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> requestProperties, String resource, String id, JSONObject obj )
		throws IOException, BGException, JSONException
	{
		final StringWriter sb = new StringWriter();

		if( obj != null )
		{
			obj.write( sb );
		}

		final String dataString = requestImpl0( url, method, requestProperties, resource, id, sb.toString(), false );

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

			throw ex;
		}
	}

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

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

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

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