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

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.annotation.Resource;

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

import bitel.billing.server.contract.bean.ContractUtils;
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.apps.tv.access.om.AccountOrderEvent.AccountEntry;
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.Method;
import ru.bitel.bgbilling.modules.tv.server.TvUtils;
import ru.bitel.bgbilling.modules.tv.server.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 StalkerOrderManager
    extends OrderManagerAdapter
    implements OrderManager
{
	private static final Logger logger = LogManager.getLogger();

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

	private JsonClient jsonClient;

	private DecimalFormat loginFormat;
	private DecimalFormat accountFormat;

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

	/**
	 * Тарифный план по умолчанию
	 */
	private String tariffDefault;

	/**
	 * Определение продукта-тарифа по родительским узлам
	 */
	private Set<Integer> tariffProductParentIds;

	/**
	 * Определение продукта-тарифа по списковому атрибуту
	 */
	private int tariffProductEntitySpecAttrId;

	/**
	 * Определение продукта-тарифа по списковому атрибуту
	 */
	private int tariffProductEntityAttrValue;

	@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" );

		String host = null;
		int port = 0;

		List<InetSocketAddress> addressList = tvDevice.getHosts();
		if( addressList.size() > 0 )
		{
			InetSocketAddress socketAddress = addressList.get( 0 );
			host = socketAddress.getHostName();
			port = socketAddress.getPort();
		}

		if( Utils.isBlankString( host ) )
		{
			host = "undefined.tv";
		}

		if( port <= 0 )
		{
			port = 80;
		}

		URL url = URI.create( config.get( "om.url", config.get( "stalker.api.url", "http://" + host + ":" + port + "/stalker_portal/api/" ) ) ).toURL();
		
		logger.info( "URL: {}", url );
		
		String login = config.get( "om.login", config.get( "stalker.api.login", tvDevice.getUsername() ) );
		String password = config.get( "om.password", config.get( "stalker.api.password", tvDevice.getPassword() ) );

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

		String loginFormat = config.get( "om.account.loginFormat", null );
		if ( Utils.notBlankString( loginFormat ) )
		{
			this.loginFormat = new DecimalFormat( loginFormat );
		}

		String accountFormat = config.get( "om.account.accountFormat", null );
		if ( Utils.notBlankString( accountFormat ) )
		{
			this.loginFormat = new DecimalFormat( accountFormat );
		}

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

		this.tariffDefault = config.get( "om.tariff.default", "10" );

		this.tariffProductParentIds = Utils.toIntegerSet( config.get( "om.tariff.product.parentIds", null ) );
		if( this.tariffProductParentIds.size() == 0 )
		{
			this.tariffProductParentIds = null;
		}

		this.tariffProductEntitySpecAttrId = config.getInt( "om.tariff.product.entitySpecAttrId", 0 );
		this.tariffProductEntityAttrValue = config.getInt( "om.tariff.product.entityAttrValue", 0 );

		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.close();
			connectionSet = null;
		}

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

		return super.disconnect( ctx );
	}

	private String getAccountNumber( final TvAccount tvAccount )
	{
		assert tvAccount.getParentId() <= 0;

		if( Utils.notBlankString( tvAccount.getLogin() ) )
		{
			return tvAccount.getLogin();
		}

		if( accountFormat != null )
		{
			return accountFormat.format( tvAccount.getId() );
		}

		return String.valueOf( tvAccount.getId() );
	}

	private String getLogin( final TvAccount tvAccount )
	{
		assert tvAccount.getParentId() > 0;

		if( Utils.notBlankString( tvAccount.getLogin() ) )
		{
			return tvAccount.getLogin();
		}

		if( loginFormat != null )
		{
			return loginFormat.format( tvAccount.getId() );
		}

		return String.valueOf( tvAccount.getId() );
	}

	private Object accountModify0( final AccountEntry entry, final Map<String, Object> params )
	    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 ) )
		{
			logger.info( "Skip non stb" );
			return null;
		}

		final String login = getLogin( tvAccount );

		if( params != null )
		{
			params.put( "login", login );
			params.put( "password", tvAccount.getPassword() );
			params.put( "stb_type", stbType );
		}

		List<byte[]> macAddressList = tvAccount.getMacAddressList();
		if( macAddressList != null )
		{
			logger.info( "Old state " + entry.getOldState() );

			for( byte[] macAddress : macAddressList )
			{
				final String macAddressString = TvUtils.macAddressToString( macAddress );
				if( params != null )
				{
					params.put( "stb_mac", macAddressString );
				}

				if( entry.getNewState() == TvAccount.STATE_DELETED )
				{
					logger.info( "delete" );

					JSONObject result = jsonClient.request( Method.delete, null, "accounts", macAddressString, null );
					logger.info( result );
				}
				else if( entry.getOldState() == TvAccount.STATE_DELETED )
				{
					JSONObject result = jsonClient.request( Method.post, null, "accounts", 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, "accounts", macAddressString, params );
						logger.info( result );
					}
				}
				else
				{
					JSONObject result = jsonClient.request( Method.put, null, "accounts", macAddressString, 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();

		final String accountNumber = 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 = this.tariffDefault;
		}

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

		accountModify0( e.getEntry(), params );

		for( AccountEntry child : e.getChildrenEntryList() )
		{
			accountModify0( child, params );
		}
		
		productsModifySyncFull( e, ctx, accountNumber );

		return null;
	}

	@Override
	public Object accountRemove( AccountOrderEvent e, ServerContext ctx )
	    throws Exception
	{
		for( AccountEntry child : e.getChildrenEntryList() )
		{
			accountModify0( child, null );
		}

		accountModify0( e.getEntry(), null );

		return null;
	}

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

		if( tariffProductEntitySpecAttrId > 0 )
		{
			EntityAttrList ea = (EntityAttrList)productSpec.getEntityAttributes().get( tariffProductEntitySpecAttrId );
			if( ea != null && ea.getValue() == tariffProductEntityAttrValue )
			{
				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
	{
		TvAccountSpecRuntime tvAccountSpecRuntime = access.tvAccountSpecRuntimeMap.get( tvAccount.getSpecId() );

		String stbType = tvAccountSpecRuntime.config.get( "stb_type", "" );
		if( Utils.isBlankString( stbType ) )
		{
			logger.info( "Skip changing tariff: non stb" );
			return;
		}

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

		List<byte[]> macAddressList = tvAccount.getMacAddressList();
		if( macAddressList != null )
		{
			for( byte[] macAddress : macAddressList )
			{
				final String macAddressString = TvUtils.macAddressToString( macAddress );

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

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

		// если полная синхронизация - перезаписываем все подписки
		if( this.productSyncMode )
		{
			final String accountNumber = getAccountNumber( e.getTvAccount() );
			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.tariffDefault;
			}
		}

		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", 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.tariffDefault;
		}

		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 = 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 = 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 ) )
		{
			logger.info( "Skip non stb" );
			return null;
		}

		List<byte[]> macAddressList = tvAccount.getMacAddressList();
		if( macAddressList != null )
		{
			for( byte[] macAddress : macAddressList )
			{
				final String macAddressString = TvUtils.macAddressToString( macAddress );

				JSONObject result = jsonClient.request( Method.put, null, "accounts", macAddressString, 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;
	}
}
