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

import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.json.JSONObject;

import bitel.billing.server.contract.bean.ContractUtils;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.modules.tv.access.TvAccess;
import ru.bitel.bgbilling.modules.tv.access.om.AbstractOrderEvent;
import ru.bitel.bgbilling.modules.tv.access.om.AccountOrderEvent;
import ru.bitel.bgbilling.modules.tv.access.om.AccountOrderEvent.AccountEntry;
import ru.bitel.bgbilling.modules.tv.access.om.OrderManager;
import ru.bitel.bgbilling.modules.tv.access.om.OrderManagerAdapter;
import ru.bitel.bgbilling.modules.tv.access.om.ProductOrderEvent;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvDevice;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvDeviceType;
import ru.bitel.bgbilling.modules.tv.api.server.TvUtils;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.Method;
import ru.bitel.bgbilling.modules.tv.runtime.TvAccountSpecRuntime;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.sql.ConnectionSet;
import ru.bitel.oss.kernel.entity.common.bean.EntityAttrList;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpec;

public class MinistraOrderManager
    extends OrderManagerAdapter
    implements OrderManager
{
	private static final Logger logger = Logger.getLogger( MinistraOrderManager.class );
	
	/**
	 * Изначальный вариант - если логин родительского аккаунта не пустой, то используется его значение, иначе - tv_account.id.
	 */
	public static final int ACCOUNT_MODE_DEFAULT = 0;
	
	/**
	 * В качестве account_number используется tv_account.id родительского аккаунта.
	 */
	public static final int ACCOUNT_MODE_ID = 1;
	
	/**
	 * В качестве account_number используется Идентификатор родительского аккаунта. Если он пустой, то используется tv_account.id родительского аккаунта.
	 */
	public static final int ACCOUNT_MODE_IDENTIFIER = 2;

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

	private JsonClient jsonClient;

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

		logger.info( "init" );
		
		this.config = new MinistraConfig( tvDevice, config );

		URL url = this.config.getUrl();
		
		logger.info( "URL: " + url );
		
		this.jsonClient = new JsonClient( url, this.config.getLogin(), this.config.getPassword() );

		return null;
	}

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

	private ConnectionSet connectionSet;

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

		this.connectionSet = ctx.getConnectionSet();

		return null;
	}

	@Override
	public Object disconnect( ServerContext ctx )
	    throws Exception
	{
		if( connectionSet != null )
		{
			connectionSet.recycle();
			connectionSet = null;
		}

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

		return super.disconnect( ctx );
	}

	/**
	 * Добавление/редактирование/удаление.
	 * @param entry
	 * @param params
	 * @param account если true, то это корневой аккунт
	 * @return
	 * @throws Exception
	 */
	private Object accountModify0( final AccountEntry entry, final Map<String, Object> params, boolean account )
	    throws Exception
	{
		logger.info( "accountModify0" );

		TvAccount tvAccount = entry.getNewTvAccount() != null ? entry.getNewTvAccount() : entry.getOldTvAccount();

		TvAccountSpecRuntime tvAccountSpecRuntime = access.tvAccountSpecRuntimeMap.get( tvAccount.getSpecId() );

        String stbType = tvAccountSpecRuntime.config.get( "stb_type", "" );
        if( (Utils.isBlankString( stbType ) && tvAccountSpecRuntime.config.getInt( "user", 0 ) <= 0)
            || Utils.isBlankString( tvAccount.getLogin() ) )
        {
            logger.info( "Skip non user" );
            return null;
        }

		final String login = config.getLogin( tvAccount );

		// при удалении params==null
		if( params != null )
		{
			params.put( "login", login );
			params.put( "password", tvAccount.getPassword() );
			params.put( "stb_type", stbType );
        }

        logger.info( "Old state " + entry.getOldState() );

        byte[] macAddress = tvAccount.getMacAddressList() != null && tvAccount.getMacAddressList().size() > 0 ? tvAccount.getMacAddressList().get( 0 ) : null;
        final String macAddressString = TvUtils.macAddressToString( macAddress );
        
        if( params != null && Utils.notEmptyString( macAddressString ) )
        {
            params.put( "stb_mac", macAddressString );
        }

        if( entry.getNewState() == TvAccount.STATE_DELETED ) // удаление
        {
            logger.info( "delete" );

            JSONObject result = jsonClient.request( Method.delete, null, "users", login, null );
            logger.info( result );
        }
        else if( entry.getOldState() == TvAccount.STATE_DELETED ) // добавление
        {
            JSONObject result = jsonClient.request( Method.post, null, "users", null, params );
            logger.info( result );

            if( "ERROR".equals( result.getString( "status" ) ) && "Login already in use".equals( result.getString( "error" ) ) )
            {
                result = jsonClient.request( Method.put, null, "users", login, params );
                logger.info( result );
            }
        }
        else // обновление
        {
            JSONObject result = jsonClient.request( Method.put, null, "users", login, params );
            logger.info( result );

            if( "ERROR".equals( result.getString( "status" ) ) && "Account not found".equals( result.getString( "error" ) ) )
            {
                result = jsonClient.request( Method.post, null, "accounts", null, params );
                logger.info( result );
            }
        }

        return null;
	}

	@Override
	public Object accountCreate( AccountOrderEvent e, ServerContext ctx )
	    throws Exception
	{
		return accountModify( e, ctx );
	}

	/**
	 * Вызывается при редактировании родительского (корневого) аккаунта, а также при добавлении/редактировании/удалении дочерних аккаунтов.
	 */
	@Override
	public Object accountModify( final AccountOrderEvent e, final ServerContext ctx )
	    throws Exception
	{
		logger.info( "accountModify" );

		final TvAccount tvAccount = e.getNewTvAccount();
		final short state = e.getNewState();

		// аккаунт - логин или ID родительского аккаунта
		final String accountNumber = config.getAccountNumber( tvAccount );

		ContractUtils contractUtils = new ContractUtils( connectionSet.getConnection() );
		String fullName = contractUtils.getContractTitle( tvAccount.getContractId(), true );

		TvAccountSpecRuntime tvAccountSpecRuntime = e.getTvAccountRuntime().tvAccountSpecRef.get();
		String stbType = tvAccountSpecRuntime.config.get( "stb.type", "" );

		Map<String, Object> params = new HashMap<String, Object>();
		params.put( "account_number", accountNumber );
		params.put( "full_name", fullName );
		params.put( "stb_type", stbType );
		params.put( "subscribed", "" );
		params.put( "status", (state == TvAccount.STATE_ENABLE) ? "1" : "0" );

		// получаем нужный тариф
		String tariffIdentifier = null;
		for( ProductSpec productSpec : e.getFullProductSpecSetToEnable() )
		{
			logger.info( "Product: " + productSpec );

			if( isTariff( productSpec ) )
			{
				tariffIdentifier = productSpec.getIdentifier();
			}
		}

		if( Utils.isBlankString( tariffIdentifier ) )
		{
			logger.info( "Custom tariff not found" );
			tariffIdentifier = config.getTariffDefault();
		}

		if( Utils.notBlankString( tariffIdentifier ) )
		{
			params.put( "tariff_plan", tariffIdentifier );
		}

		// синхронизация 
		accountModify0( e.getEntry(), params, true );

		for( AccountEntry child : e.getChildrenEntryList() )
		{
			accountModify0( child, params, false );
		}
		
		// для пустого аккаунта не нужна синхронизация
        if( e.getChildrenEntryList().size() > 0
            || (Utils.notBlankString( tvAccount.getLogin() ) && (Utils.notBlankString( stbType ) || tvAccountSpecRuntime.config.getInt( "user", 0 ) > 0)) )
        {
            productsModifySyncFull( e, ctx, accountNumber );
        }

		return null;
	}

	/**
	 * Вызывается при удалении родительского (корневого) аккаунта.
	 */
	@Override
	public Object accountRemove( AccountOrderEvent e, ServerContext ctx )
	    throws Exception
	{
		for( AccountEntry child : e.getChildrenEntryList() )
		{
			accountModify0( child, null, false );
		}

		accountModify0( e.getEntry(), null, true );

		return null;
	}

	private boolean isTariff( ProductSpec productSpec )
	{
		if( config.getTariffProductParentIds() != null && config.getTariffProductParentIds().contains( productSpec.getParentId() ) )
		{
			return true;
		}

		if( config.getTariffProductEntitySpecAttrId() > 0 )
		{
			EntityAttrList ea = (EntityAttrList)productSpec.getEntityAttributes().get( config.getTariffProductEntitySpecAttrId() );
			if( ea != null && ea.getValue() == config.getTariffProductEntityAttrValue() )
			{
				return true;
			}
		}

		return false;
	}

	private void tariffModify( TvAccount tvAccount, String tariffIdentifier )
	    throws Exception
	{
		/* обновление тарифа по лицевому счету не отрабатывает
		params.put( "tariff_plan", tariffIdentifier );

		JSONObject result = request( Method.put, "accounts", getAccountNumber( e.getTvAccount() ), params );
		logger.info( result );

		params.clear();*/

		logger.info( "Update tariff to " + tariffIdentifier );

		Map<String, Object> params = new HashMap<String, Object>();
		params.put( "tariff_plan", tariffIdentifier );

		tariffModify0( tvAccount, tariffIdentifier, params );

		if( tvAccount.getChildren() != null ) for( TvAccount child : tvAccount.getChildren() )
		{
			tariffModify0( child, tariffIdentifier, params );
		}
	}

    private void tariffModify0( TvAccount tvAccount, String tariffIdentifier, Map<String, Object> params )
        throws Exception
    {
        final TvAccountSpecRuntime tvAccountSpecRuntime = access.tvAccountSpecRuntimeMap.get( tvAccount.getSpecId() );
        final String stbType = tvAccountSpecRuntime.config.get( "stb_type", "" );
        
        if( (Utils.isBlankString( stbType ) && tvAccountSpecRuntime.config.getInt( "user", 0 ) <= 0)
            || Utils.isBlankString( tvAccount.getLogin() ) )
        {
            logger.info( "Skip changing tariff: not user" );
            return;
        }

        logger.info( "Changing tariff for " + tvAccount + " to " + tariffIdentifier );

        final String login = config.getLogin( tvAccount );

        JSONObject result = jsonClient.request( Method.put, null, "users", login, params );
        logger.info( result );
    }

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

		// если полная синхронизация - перезаписываем все подписки
		if( this.config.getProductSyncMode() )
		{
			final String accountNumber = config.getAccountNumber( e.getTvAccount() );
			return productsModifySyncFull( e, ctx, accountNumber );
		}
		
		final Map<String, Object> params = new HashMap<String, Object>();

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

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

        String newTariffIdentifier = null;

        for( final ProductSpec productSpec : Utils.iterable( e.getProductSpecSetToAdd(), e.getDeviceOptionProductSpecSetToEnable() ) )
        {
            if( !isTariff( productSpec ) )
            {
                packagesToSubscribe.add( productSpec.getIdentifier().trim() );
            }
            // нужно установить новый тариф
            else
            {
                newTariffIdentifier = productSpec.getIdentifier();
            }
        }

        for( final ProductSpec productSpec : Utils.iterable( e.getProductSpecSetToRemove(), e.getDeviceOptionProductSpecSetToDisable() ) )
		{
			if( !isTariff( productSpec ) )
			{
				packagesToUnsubscribe.add( productSpec.getIdentifier().trim() );
			}
			// тарифный продукт закрыт, если нового нет - нужно выставить тариф по умолчанию
			else if( Utils.isBlankString( newTariffIdentifier ) )
			{
			    newTariffIdentifier = this.config.getTariffDefault();
			}
		}

		if( Utils.notBlankString( newTariffIdentifier ) )
		{
			tariffModify( e.getTvAccount(), newTariffIdentifier );
		}

		if( packagesToSubscribe.size() > 0 )
		{
			params.put( "subscribed[]", packagesToSubscribe );
		}

		if( packagesToUnsubscribe.size() > 0 )
		{
			params.put( "unsubscribed[]", packagesToUnsubscribe );
		}

		if( params.size() == 0 )
		{
			logger.info( "Nothing to change" );
			return null;
		}

		JSONObject result = jsonClient.request( Method.put, null, "account_subscription", config.getAccountNumber( e.getTvAccount() ), params );
		logger.info( result );

		return null;
	}
	
	/**
	 * Полная синхронизация продуктов/пакетов.
	 * @param e
	 * @param ctx
	 * @param accountNumber
	 * @return
	 * @throws Exception
	 */
	private Object productsModifySyncFull( final AbstractOrderEvent e, final ServerContext ctx, final String accountNumber )
		throws Exception
	{
		logger.info( "Full sync mode" );

		final Map<String, Object> params = new HashMap<String, Object>();

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

		String tariffIdentifier = null;

		for( ProductSpec productSpec : e.getFullProductSpecSetToEnable() )
		{
			logger.info( "Product: " + productSpec );

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

                if( isTariff( productSpec ) )
                {
                    tariffIdentifier = productSpec.getIdentifier();
                }
                else
                {
                    packagesToSubscribe.add( productSpec.getIdentifier().trim() );
                }
            }
        }

		if( Utils.isBlankString( tariffIdentifier ) )
		{
			logger.info( "Custom tariff not found" );
			tariffIdentifier = this.config.getTariffDefault();
		}

		if( Utils.notBlankString( tariffIdentifier ) )
		{
			TvAccount tvAccount;

			if( e instanceof AccountOrderEvent )
			{
				final AccountOrderEvent oe = ((AccountOrderEvent)e);

				tvAccount = oe.getOldTvAccount();
				if( tvAccount == null )
				{
					tvAccount = oe.getNewTvAccount();
				}
			}
			else if( e instanceof ProductOrderEvent )
			{
				tvAccount = ((ProductOrderEvent)e).getTvAccount();
			}
			else
			{
				tvAccount = e.getTvAccountRuntime().getTvAccount();
			}

			tariffModify( tvAccount, tariffIdentifier );
		}

		if( packagesToSubscribe.size() > 0 )
		{
			logger.info( "Update subscriptions" );

			params.put( "subscribed[]", packagesToSubscribe );

			JSONObject result = jsonClient.request( Method.post, null, "account_subscription", accountNumber, params );
			logger.info( result );
		}
		else
		{
			logger.info( "Delete subscriptions" );

			JSONObject result = jsonClient.request( Method.delete, null, "account_subscription", accountNumber, params );
			logger.info( result );
		}

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

		final String accountNumber = config.getAccountNumber( e.getTvAccountRuntime().getTvAccount() );

		return accountOptionsModify0( e, ctx, accountNumber );
	}

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

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

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

		final String accountNumber = config.getAccountNumber( e.getOldTvAccount() );
		
		Map<String, Object> params = new HashMap<String, Object>();
		params.put( "account_number", accountNumber );
		params.put( "status", (e.getNewState() == TvAccount.STATE_ENABLE) ? "1" : "0" );
		
		if( e.getNewState() == TvAccount.STATE_ENABLE )
		{
			accountStateModify0( e.getEntry(), params );

			for( AccountEntry child : e.getChildrenEntryList() )
			{
				accountStateModify0( child, params );
			}

			if( e.isOptionsModified() )
			{
				accountOptionsModify0( e, ctx, accountNumber );
			}
		}
		else
		{
			if( e.isOptionsModified() )
			{
				accountOptionsModify0( e, ctx, accountNumber );
			}

			accountStateModify0( e.getEntry(), params );

			for( AccountEntry child : e.getChildrenEntryList() )
			{
				accountStateModify0( child, params );
			}
		}

		return null;
	}

	private Object accountStateModify0( final AccountEntry entry, final Map<String, Object> params )
		throws Exception
	{
		logger.info( "accountStateModify0" );

		TvAccount tvAccount = entry.getNewTvAccount() != null ? entry.getNewTvAccount() : entry.getOldTvAccount();

		TvAccountSpecRuntime tvAccountSpecRuntime = access.tvAccountSpecRuntimeMap.get( tvAccount.getSpecId() );
		String stbType = tvAccountSpecRuntime.config.get( "stb_type", "" );
        if( (Utils.isBlankString( stbType ) && tvAccountSpecRuntime.config.getInt( "user", 0 ) <= 0)
            || Utils.isBlankString( tvAccount.getLogin() ) )
		{
			logger.info( "Skip non user" );
			return null;
		}
		
        final String login = config.getLogin( tvAccount );

        byte[] macAddress = tvAccount.getMacAddressList() != null && tvAccount.getMacAddressList().size() > 0 ? tvAccount.getMacAddressList().get( 0 ) : null;
        final String macAddressString = TvUtils.macAddressToString( macAddress );
        
        if( params != null && Utils.notEmptyString( macAddressString ) )
        {
            params.put( "stb_mac", macAddressString );
        }

        JSONObject result = jsonClient.request( Method.put, null, "users", login, params );
        logger.info( result );

        if( "ERROR".equals( result.getString( "status" ) ) && "Account not found".equals( result.getString( "error" ) ) )
        {
            result = jsonClient.request( Method.post, null, "users", null, params );
            logger.info( result );
        }

        return null;
	}
}
