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

import bitel.billing.server.contract.bean.Contract;
import bitel.billing.server.contract.bean.ContractAddressParamValue;
import bitel.billing.server.contract.bean.ContractManager;
import bitel.billing.server.contract.bean.ContractParameterManager;
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 ru.bitel.bgbilling.apps.tv.access.TvAccess;
import ru.bitel.bgbilling.apps.tv.access.om.AbstractOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.AccountOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.ProductOrderEvent;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.modules.tv.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDevice;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDeviceType;
import ru.bitel.bgbilling.modules.tv.common.om.OrderManager;
import ru.bitel.bgbilling.modules.tv.common.om.OrderManagerAdapter;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.JsonClientException;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.Method;
import ru.bitel.bgbilling.modules.tv.server.TvUtils;
import ru.bitel.bgbilling.modules.tv.dyn.TvDynUtils;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpec;
import ru.bitel.oss.systems.inventory.service.common.bean.ServiceSpec;

import javax.annotation.Resource;
import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class LifeStreamV2OrderManager
	extends OrderManagerAdapter
	implements OrderManager
{
	private static final Logger logger = LogManager.getLogger();

	@Resource(name = "access")
	private TvAccess access;

	private JsonClient jsonClient;

	private ContractManager contractManager;
	private ContractParameterManager contractParameterManager;

	/**
	 * Полная или частичная синхронизация продуктов.
	 */
	private boolean productSyncMode;

	/**
	 * Если true - синхронизация на уровне сервисов, а не продуктов.
	 */
	private boolean serviceMode;

    private int customerFioPid;
	private int customerFirstNamePid;
	private int customerMiddleNamePid;
	private int customerLastNamePid;
	private int customerAddressPid;
	private int customerEmailPid;

	private List<Integer> customerEmailSources;

	private String customerEmailDomain;
	private String loginPrefix;
	private String operatorName;
	
	private boolean disableProductsWithAccountDeviceState;

	private JSONObject invoke( final Method method, final String resource, final JSONObject obj )
		throws IOException, BGException, JSONException
	{
		return jsonClient.invoke( method, null, ("v2/" + resource), null, obj );
	}

	private JSONObject invoke( final Method method, final String resource, final String id, final String sub, final JSONObject obj )
		throws IOException, BGException, JSONException
	{
		return jsonClient.invoke( method, null, "v2/" + resource + (id != null ? ("/" + id) : "") + (sub != null ? ("/" + sub) : ""), null, obj );
	}

	private JSONArray invokeAndGetArray( final Method method, final String resource, final String id, final String sub, final JSONObject obj )
		throws IOException, BGException, JSONException
	{
		return jsonClient.invokeAndGetArray( method, null, "v2/" + resource + (id != null ? ("/" + id) : "") + (sub != null ? ("/" + sub) : ""), null, obj );
	}

	@Override
	public Object init( ServerContext ctx, int moduleId, TvDevice tvDevice, TvDeviceType tvDeviceType, ParameterMap config )
		throws Exception
	{
		logger.info( "init" );

		super.init( ctx, moduleId, tvDevice, tvDeviceType, config );

		URL url = TvUtils.getUrl( tvDevice, config, "om.url", "lifestream.api.url", "test.lfstrm.tv", 80 );

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

		String login = config.get( "om.login", config.get( "lifestream.api.login", tvDevice.getUsername() ) );
		String password = config.get( "om.password", config.get( "lifestream.api.password", tvDevice.getPassword() ) );

		this.jsonClient = new JsonClient( url, login, password );

		this.productSyncMode = config.getInt( "om.product.syncMode", 1 ) > 0;
		this.serviceMode = config.getInt( "om.product.serviceMode", 0 ) > 0;

		int customerNamePid = config.getInt( "customer.name.pid", 0 );
		customerFioPid = config.getInt( "customer.fio.pid", 0 );
		customerLastNamePid = config.getInt( "customer.lastName.pid", customerNamePid );
		customerFirstNamePid = config.getInt( "customer.firstName.pid", 0 );
		customerMiddleNamePid = config.getInt( "customer.middleName.pid", 0 );
		customerAddressPid = config.getInt( "customer.address.pid", 0 );
		customerEmailPid = config.getInt( "customer.email.pid", 0 );
		customerEmailSources = Utils.toIntegerList( config.get( "customer.email.sources", "1" ) );
		customerEmailDomain = config.get( "customer.email.domain", null );

        operatorName = config.get( "account.operator.name" );
		loginPrefix = Utils.maskBlank( config.get( "account.login.prefix", "" ), "" );
		
		disableProductsWithAccountDeviceState = config.getInt( "om.product.disableWithAccount", 0 ) > 0;

		return null;
	}

	@Override
	public Object destroy()
		throws Exception
	{
		return null;
	}

	@Override
	public Object connect( ServerContext ctx )
		throws Exception
	{
		super.connect( ctx );

		contractManager = new ContractManager( ctx.getConnection() );
		contractParameterManager = new ContractParameterManager( ctx.getConnection() );

		return null;
	}

	@Override
	public Object disconnect( ServerContext ctx )
		throws Exception
	{
		try
		{
			contractParameterManager = null;

			contractManager.recycle();
			contractManager = null;
		}
		finally
		{
			if( jsonClient != null )
			{
				jsonClient.disconnect();
			}
		}

		return super.disconnect( ctx );
	}

	@Override
	public Object accountCreate( AccountOrderEvent e, ServerContext ctx )
		throws Exception
	{
		logger.info( "accountCreate" );

		return accountModify( e, ctx );
	}

	@Override
	public Object accountModify( AccountOrderEvent e, ServerContext ctx )
		throws Exception
	{
		logger.info( "accountModify" );

		final String userId = accountModify0( e, ctx );

		try
		{
			// синхронизируем все продукты
            productsModifySyncFull( e, ctx, userId, e.getNewState() == TvAccount.STATE_ENABLE );
		}
		catch( Exception ex )
		{
			logger.error( ex.getMessage(), ex );
		}

		return null;
	}

	/**
	 * Редактирование и создание аккаунта.
	 * @param e
	 * @param ctx
	 * @return userId
	 * @throws Exception
	 */
	private String accountModify0( final AccountOrderEvent e, final ServerContext ctx )
		throws Exception
	{
		logger.info( "accountModify0" );

		String userId;
		String email = null;

		TvAccount tvAccount = e.getNewTvAccount() != null ? e.getNewTvAccount() : e.getOldTvAccount();
		for( final Integer s : customerEmailSources )
		{
			switch( s )
			{
				case 2:
					email = tvAccount.getLogin();
					break;

				case 3:
					email = tvAccount.getIdentifier();
					break;

				default:
					email = TvDynUtils.getEmail( contractParameterManager, tvAccount.getContractId(), customerEmailPid );
					break;
			}

			if ( Utils.notBlankString( email ) && email.contains( "@" ) )
			{
				break;
			}
			else
			{
				email = null;
			}
		}

		if ( Utils.isBlankString( email ) && Utils.notBlankString( customerEmailDomain ) )
		{
			email = tvAccount.getLogin() + "@" + customerEmailDomain;
		}

		if ( Utils.isBlankString( email ) )
		{
			logger.warn( "E-mail is null for " + tvAccount );
		}

		final Contract contract = contractManager.getContractById( e.getContractId() );
		// создание аккаунта
		if( e.getOldTvAccount() == null || Utils.isBlankString( e.getOldTvAccount().getDeviceAccountId() ) )
		{
			JSONObject lifeStreamContract = new JSONObject();
			lifeStreamContract.put( "username", loginPrefix + tvAccount.getLogin() );
			lifeStreamContract.put( "password", tvAccount.getPassword() );
			lifeStreamContract.put( "email", email );
			lifeStreamContract.put( "info", getInfo( tvAccount, contract ) );

			JSONObject result = invoke( Method.post, "accounts", lifeStreamContract );
			userId = result.getString( "id" );
			e.getEntry().setDeviceAccountId( userId );
			accountStateModify0( e, userId );
		}
		else // обновление аккаунта
		{
			userId = e.getOldTvAccount().getDeviceAccountId();

			JSONObject lifeStreamContract = new JSONObject();
			lifeStreamContract.put( "username", loginPrefix + tvAccount.getLogin() );
			// проставляем пароль только если он изменился
            if( !Utils.maskNull( e.getOldTvAccount().getPassword() ).equals( Utils.maskNull( tvAccount.getPassword() ) ) )
            {
                lifeStreamContract.put( "password", tvAccount.getPassword() );
            }
			lifeStreamContract.put( "email", email );
			lifeStreamContract.put( "info", getInfo( tvAccount, contract ) );

			JSONObject result = invoke( Method.post, "accounts", userId, "update", lifeStreamContract );
			userId = result.getString( "id" );
			e.getEntry().setDeviceAccountId( userId );

			if( e.getOldState() != e.getNewState() )
			{
				accountStateModify0( e, userId );
			}
		}

		return userId;
	}
	
	private JSONObject getInfo( TvAccount tvAccount, Contract contract )
	{
		String fio = "";
		if ( customerFioPid > 0 )
		{
		    fio = contractParameterManager.getStringParam( contract.getId(), customerFioPid );
		}
		else
		{
		    String[] name = TvDynUtils.getName( contractParameterManager, contract, customerLastNamePid, customerFirstNamePid, customerMiddleNamePid );
		    fio = name[0];
		}
        String address = null;
        if ( customerAddressPid > 0 )
        {
        	ContractAddressParamValue contractAddressParamValue = contractParameterManager.getAddressParam( contract.getId(), customerAddressPid );
        	if ( contractAddressParamValue != null )
        	{
        		address = contractAddressParamValue.getAddress();
        	}
        }

        JSONObject info = new JSONObject();
		info.put( "contractId", tvAccount.getContractId() );
		info.put( "tvAccountId", tvAccount.getId() );
        info.put( "fio", fio );
		if ( Utils.notBlankString( address ) )
		{
			info.put( "address", address );
		}
		if ( Utils.notBlankString( operatorName ) )
		{
		    info.put( "operator", operatorName );
		}
		return info;
	}

	@Override
	public Object accountRemove( AccountOrderEvent e, ServerContext ctx )
		throws Exception
	{
		logger.info( "accountRemove" );

		final String userId = e.getOldTvAccount().getDeviceAccountId();

		if( Utils.isBlankString( userId ) )
		{
			logger.warn( "deviceAccountId is empty for " + e.getOldTvAccount() );
			return null;
		}

		try
		{
			JSONObject result = invoke( Method.delete, "accounts", userId, null, null );

			if( logger.isDebugEnabled() )
			{
				logger.debug( result );
			}
		}
		catch( JsonClientException ex )
		{
			if( ex.getResponseCode() == 404 )
			{
				logger.info( "Error 404 - account already removed" );
				return null;
			}
			
            if( ex.getResponseCode() == 403 )
            {
                logger.info( "Error 403 - account already removed" );
                return null;
            }

			throw ex;
		}

		return null;
	}

	/**
	 * Полная синхронизация продуктов/пакетов.
	 * @param e
	 * @param ctx
	 * @param abonentAccountId
	 * @return
	 * @throws Exception
	 */
	private Object productsModifySyncFull( final AbstractOrderEvent e, final ServerContext ctx, final String userId, final boolean accountEnabled )
		throws Exception
	{
		logger.debug( "productsModifyFullSync" );

		final Set<String> servicesToAdd = new HashSet<String>();

        if( accountEnabled || !disableProductsWithAccountDeviceState )
        {
            if( serviceMode )
            {
                // получаем полный список активных сервисов
                for( ServiceSpec serviceSpec : e.getFullServiceSpecSetToEnable() )
                {
                    servicesToAdd.add( serviceSpec.getIdentifier().trim() );
                }
            }
            else
            {
                // получаем список активных продуктов
                for( ProductSpec productSpec : e.getFullProductSpecSetToEnable() )
                {
                    logger.info( "Product: " + productSpec );

                    servicesToAdd.add( productSpec.getIdentifier().trim() );
                }

                // добавляем продукты-опции
                if( !(e instanceof AccountOrderEvent) || ((AccountOrderEvent)e).getNewState() == TvAccount.STATE_ENABLE )
                {
                    for( ProductSpec productSpec : e.getNewDeviceOptionProductSpecs() )
                    {
                        logger.info( "Product (option): " + productSpec );

                        servicesToAdd.add( productSpec.getIdentifier().trim() );
                    }
                }
            }
        }

		// удаляем некорректные записи
		servicesToAdd.remove( 0L );

		// текущие подписки ID сервиса-пакета <-> ID записи привязки сервиса-пакета к контракту
		Set<String> currentServiceIds = new HashSet<String>();

		// получаем список текущих активных сервисов
		JSONArray subscriptionArray = invokeAndGetArray( Method.get, "accounts", userId, "subscriptions", null );

		for( int i = 0, size = subscriptionArray.length(); i < size; i++ )
		{
			JSONObject serviceSubscription = subscriptionArray.getJSONObject( i );

			// id сервиса-пакета MW
			String serviceId = serviceSubscription.getString( "id" );

			currentServiceIds.add( serviceId );
		}

		logger.info( "Current serviceIds: " + currentServiceIds + ", need serviceIds: " + servicesToAdd );

		// удаляем те, что неактивны в биллинге, но есть в текущих
		for( String serviceId : currentServiceIds )
		{
			if( !servicesToAdd.contains( serviceId ) )
			{
				logger.debug( "delete subscription: " + serviceId );

				JSONObject subscription = new JSONObject();
				subscription.put( "id", serviceId );
				subscription.put( "valid", false );

				JSONObject result = invoke( Method.post, "accounts", userId, "subscriptions", subscription );

				if( logger.isDebugEnabled() )
				{
					logger.debug( result );
				}
			}
		}

		// добавляем те, что активны в биллинге, но в текущих - нет
		for( String serviceId : servicesToAdd )
		{
			if( !currentServiceIds.contains( serviceId ) )
			{
				logger.debug( "add subscription: " + serviceId );

				JSONObject subscription = new JSONObject();
				subscription.put( "id", serviceId );
				subscription.put( "valid", true );

				JSONObject result = invoke( Method.post, "accounts", userId, "subscriptions", subscription );

				if( logger.isDebugEnabled() )
				{
					logger.debug( result );
				}
			}
		}

		return null;
	}

	@Override
	public Object productsModify( final ProductOrderEvent e, final ServerContext ctx )
		throws Exception
	{
		logger.debug( "productsModify" );

		final String userId = e.getTvAccount().getDeviceAccountId();

        if( this.productSyncMode
            || (disableProductsWithAccountDeviceState && e.getTvAccount().getDeviceState() != TvAccount.STATE_ENABLE) )
        {
            return productsModifySyncFull( e, ctx, userId, e.getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
        }

		final Set<Long> servicesToRemove = new HashSet<Long>();
		final Set<Long> servicesToAdd = new HashSet<Long>();

		if( serviceMode )
		{
			for( ServiceSpec serviceSpec : e.getServiceSpecSetToRemove() )
			{
				servicesToRemove.add( Utils.parseLong( serviceSpec.getIdentifier().trim() ) );
			}

			for( ServiceSpec serviceSpec : e.getServiceSpecSetToAdd() )
			{
				servicesToAdd.add( Utils.parseLong( serviceSpec.getIdentifier().trim() ) );
			}
		}
		else
		{
			for( ProductSpec productSpec : e.getProductSpecSetToRemove() )
			{
				servicesToRemove.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
			}

			for( ProductSpec productSpec : e.getDeviceOptionProductSpecSetToDisable() )
			{
				servicesToRemove.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
			}

			for( ProductSpec productSpec : e.getProductSpecSetToAdd() )
			{
				servicesToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
			}

			for( ProductSpec productSpec : e.getDeviceOptionProductSpecSetToEnable() )
			{
				servicesToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
			}
		}

		servicesToRemove.remove( 0L );
		servicesToAdd.remove( 0L );

		if( servicesToRemove.size() > 0 )
		{
			return productsModifySyncFull( e, ctx, userId, e.getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
		}

		for( Long serviceId : servicesToAdd )
		{
			logger.debug( "add subscription: " + serviceId );

			JSONObject subscription = new JSONObject();
			subscription.put( "id", serviceId );
			subscription.put( "valid", true );

			JSONObject result = invoke( Method.post, "accounts", userId, "subscriptions", subscription );

			if( logger.isDebugEnabled() )
			{
				logger.debug( result );
			}
		}

		return null;
	}

	@Override
	public Object accountOptionsModify( AbstractOrderEvent e, ServerContext ctx )
		throws Exception
	{
		logger.debug( "accountOptionsModify" );

		final String userId = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();

		return accountOptionsModify0( e, ctx, userId, e.getTvAccountRuntime().getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
	}

	private Object accountOptionsModify0( AbstractOrderEvent e, ServerContext ctx, final String userId, final boolean accountEnabled )
		throws Exception
	{
		logger.debug( "accountOptionsModify0" );

        return productsModifySyncFull( e, ctx, userId, accountEnabled );
	}

	@Override
	public Object accountStateModify( AccountOrderEvent e, ServerContext ctx )
		throws Exception
	{
		logger.info( "accountStateModify" );

		if( e.getOldTvAccount() == null || Utils.isBlankString( e.getOldTvAccount().getDeviceAccountId() ) )
		{
			return accountModify( e, ctx );
		}

		String userId = e.getOldTvAccount().getDeviceAccountId();

		accountStateModify0( e, userId );

		//if( e.isOptionsModified() )
		{
			accountOptionsModify0( e, ctx, userId, e.getNewState() == TvAccount.STATE_ENABLE );
		}

		return null;
	}

	private void accountStateModify0( final AccountOrderEvent e, final String userId )
		throws Exception
	{}
}
