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

import static ru.bitel.common.PatternStringGenerator.insertPatternPart;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

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

import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.module.common.bean.BGModule;
import ru.bitel.bgbilling.kernel.module.server.ModuleCache;
import ru.bitel.bgbilling.kernel.script.server.dev.GlobalScriptBase;
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.TvDeviceMap;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDeviceMap.TvDeviceMapItem;
import ru.bitel.bgbilling.modules.tv.common.event.TvAccountModifiedEvent;
import ru.bitel.bgbilling.modules.tv.common.event.access.TvAccountDeviceStateAndOptionsModifiedEvent;
import ru.bitel.bgbilling.modules.tv.common.service.TvAccountService;
import ru.bitel.bgbilling.modules.tv.common.service.TvDeviceService;
import ru.bitel.bgbilling.modules.tv.server.TvUtils;
import ru.bitel.bgbilling.modules.tv.server.bean.TvAccountDao;
import ru.bitel.bgbilling.modules.tv.server.bean.enums.ModuleSetupKeys;
import ru.bitel.bgbilling.modules.tv.server.runtime.TvAccountSpecRuntime;
import ru.bitel.bgbilling.modules.tv.server.runtime.TvAccountSpecRuntimeMap;
import ru.bitel.bgbilling.server.util.ModuleSetup;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.common.sql.ConnectionSet;

public class IptvPortalTerminalSynchronizingTask
	extends GlobalScriptBase
{
	private static final Logger logger = LogManager.getLogger();

	@Override
	public void execute( Setup setup, ConnectionSet connectionSet )
		throws Exception
	{
		final List<BGModule> moduleList = ModuleCache.getInstance().getModulesList( "tv" );

		for ( BGModule module : moduleList )
		{
			processModule( setup, connectionSet, module.getId() );
		}
	}

	private void processModule( Setup setup, ConnectionSet connectionSet, int moduleId )
		throws Exception
	{
		logger.info( "processModule " + moduleId );
		print( "processModule " + moduleId );

		final ServerContext context = ServerContext.get();
		final ModuleSetup moduleSetup = setup.getModuleSetup( moduleId );

		final TvDeviceMap tvDeviceMap = TvDeviceMap.getInstance( moduleId );
		final TvDeviceService tvDeviceService = context.getService( TvDeviceService.class, moduleId );
		final TvAccountService tvAccountService = context.getService( TvAccountService.class, moduleId );

		final TvAccountSpecRuntimeMap tvAccountSpecRuntimeMap = new TvAccountSpecRuntimeMap( connectionSet.getConnection(), moduleId );

		final List<Integer> deviceTypeIds = Utils.toIntegerList( moduleSetup.get( ModuleSetupKeys.OM_DEVICE_TYPE_IDS ) );
		for ( Integer deviceTypeId : deviceTypeIds )
		{
			for ( TvDevice tvDevice : tvDeviceService.deviceList( deviceTypeId ) )
			{
				processDevice( setup, context, connectionSet, moduleId, tvDeviceMap, tvDevice, tvAccountSpecRuntimeMap, tvAccountService );
			}
		}
	}

	private void processDevice( Setup setup, ServerContext context, ConnectionSet connectionSet, int moduleId, TvDeviceMap tvDeviceMap, TvDevice tvDevice, TvAccountSpecRuntimeMap tvAccountSpecRuntimeMap, TvAccountService tvAccountService )
		throws Exception
	{
		logger.info( "processDevice " + tvDevice.getId() );
		print( "processDevice " + tvDevice.getId() );

		final TvDeviceMapItem tvDeviceMapItem = tvDeviceMap.get( tvDevice.getId() );
		final ParameterMap config = tvDeviceMapItem.getConfig();

		int terminalTvAccountSpecId = config.getInt( "om.terminal.tvAccountSpecId", 0 );
		TvAccountSpecRuntime terminalTvAccountSpecRuntime = tvAccountSpecRuntimeMap.get( terminalTvAccountSpecId );

		IptvPortalJsonClient jsonClient = IptvPortalJsonClient.newInstance( tvDevice, config );

		TvAccountDao tvAccountDao = new TvAccountDao( connectionSet.getConnection(), moduleId );

		jsonClient.authorize();
		try
		{
			processDevice( context, connectionSet, moduleId, tvDeviceMap, tvDevice, tvAccountDao, terminalTvAccountSpecRuntime, tvAccountService, jsonClient );
		}
		finally
		{
			jsonClient.disconnect();
		}
	}

	private void processDevice( ServerContext context, ConnectionSet connectionSet, int moduleId, TvDeviceMap tvDeviceMap, TvDevice tvDevice, 
	                            TvAccountDao tvAccountDao, TvAccountSpecRuntime terminalTvAccountSpecRuntime, TvAccountService tvAccountService, IptvPortalJsonClient jsonClient )
		throws Exception
	{
		Date now = new Date();

		List<TvAccount> tvAccountList = tvAccountService.tvAccountList( tvDevice.getId(), null, now, now );
		for ( TvAccount tvAccount : tvAccountList )
		{
			if ( tvAccount.getParentId() > 0 )
			{
				continue;
			}

			final int subscriberId = Utils.parseInt( tvAccount.getDeviceAccountId() );
			if ( subscriberId <= 0 )
			{
				logger.info( "Subscriber not specified for tvAccount:" + tvAccount.getId() );
				print( "Subscriber not specified for tvAccount:" + tvAccount.getId() );
				continue;
			}
			
			processAccount( context, connectionSet, moduleId, tvDevice, tvAccountDao, terminalTvAccountSpecRuntime,
							jsonClient, subscriberId, tvAccount, ( s ) -> print( s ) );
			/*}
			catch(  ex )
			{
			logger.info( "Subscriber not found with id:" + subscriberId + " for tvAccount:" + tvAccount.getId() + ". " + ex.getMessage() );
			print( "Subscriber not found with id:" + subscriberId + " for tvAccount:" + tvAccount.getId() + ". " + ex.getMessage() );
			}*/

			context.commit();
		}
	}

	public static void processAccount( ServerContext context, ConnectionSet connectionSet, int moduleId, 
	                                   TvDevice tvDevice, TvAccountDao tvAccountDao, TvAccountSpecRuntime terminalTvAccountSpecRuntime, 
	                                   IptvPortalJsonClient jsonClient, int subscriberId, TvAccount parentTvAccount,
	                                   Consumer<String> script )
		throws Exception
	{
		List<TvAccount> children = tvAccountDao.listChildren( parentTvAccount.getId() );

		JSONObject res = jsonClient.select( "terminal", "id, mac_addr, inet_addr, disabled",
											new JSONObject( Collections.singletonMap( "eq", Arrays.asList( "subscriber_id", String.valueOf( subscriberId ) ) ) ) );
		if ( !res.has( "result" ) )
		{
			return;
		}
		
		Set<TvAccount> notFoundAccounts = new HashSet<TvAccount>( children );

		JSONArray array = res.getJSONArray( "result" );

		TERMINAL:
		for ( int i = 0, size = array.length(); i < size; i++ )
		{
			JSONArray terminal = array.getJSONArray( i );

			int terminalId = terminal.getInt( 0 );
			String macAddress = terminal.getString( 1 );
			String inetAddr = terminal.getString( 2 );

			if ( children != null )
			{
				for ( TvAccount child : children )
				{
					if ( Utils.parseInt( child.getDeviceAccountId() ) == terminalId )
					{
						if ( Utils.isBlankString( child.getIdentifier() ) || !child.getIdentifier().equals( inetAddr ) )
						{
							logger.info( "Found linked terminal with changed inetAddr" );
							script.accept( "Found linked terminal with changed inetAddr" );

							TvAccount newChild = child.clone();
							newChild.setIdentifier( inetAddr );

							tvAccountDao.update( newChild );
							context.publishAfterCommit( new TvAccountModifiedEvent( moduleId, child.getContractId(), 0, child, newChild ) );
						}
						
						notFoundAccounts.remove( child );

						continue TERMINAL;
					}

					if ( Utils.isBlankString( child.getDeviceAccountId() )
						&& macAddress.equalsIgnoreCase( TvUtils.macAddressToString( child.getMacAddressListBytes() ) ) )
					{
						logger.info( "Found not linked by deviceAccountId terminal" );
						script.accept( "Found not linked by deviceAccountId terminal" );

						String deviceAccountId = String.valueOf( terminalId );

						tvAccountDao.updateDeviceStateAndOptions( child.getId(), deviceAccountId, TvAccount.STATE_ENABLE, child.getDeviceOptionIds(), child.getAccessCode() );
                        context.publishAfterCommit( new TvAccountDeviceStateAndOptionsModifiedEvent( moduleId, parentTvAccount.getContractId(),
                            child.getDeviceId(), child.getId(), deviceAccountId, TvAccount.STATE_ENABLE, null ) );
						
						notFoundAccounts.remove( child );

						continue TERMINAL;
					}
				}
			}

			logger.info( "Found new terminal in MW" );
			script.accept( "Found new terminal in MW" );

			if ( terminalTvAccountSpecRuntime == null )
			{
				logger.error( "Found not linked terminal, but tvAccountSpec not found" );
				script.accept( "Found not linked terminal, but tvAccountSpec not found" );

				continue;
			}

			TvAccount tvAccount = new TvAccount();
			tvAccount.setContractId( parentTvAccount.getContractId() );
			tvAccount.setParentId( parentTvAccount.getId() );
			tvAccount.setSpecId( terminalTvAccountSpecRuntime.tvAccountSpec.getId() );

			tvAccount.setDeviceId( parentTvAccount.getDeviceId() );

			tvAccount.setDateFrom( parentTvAccount.getDateFrom() );
			tvAccount.setDateTo( parentTvAccount.getDateTo() );

			tvAccount.setMacAddressList( Collections.singletonList( TvUtils.parseMacAddress( macAddress ) ) );

			tvAccount.setIdentifier( inetAddr );

			tvAccount.setLogin( null );
			tvAccount.setPassword( null );
			tvAccount.setStatus( TvAccount.STATUS_ACTIVE );

			tvAccount.setComment( "" );

			tvAccount.setDeviceState( TvAccount.STATE_ENABLE );
			tvAccount.setDeviceAccountId( String.valueOf( terminalId ) );

			String titlePattern = terminalTvAccountSpecRuntime.config.get( "title.pattern", "Укажите параметр title.pattern в конфигурации типа сервиса!" );
			titlePattern = insertPatternPart( titlePattern, "deviceIdentifier", tvDevice.getIdentifier() );
			titlePattern = insertPatternPart( titlePattern, "deviceTitle", tvDevice.getTitle() );
			titlePattern = insertPatternPart( titlePattern, "login", tvAccount.getLogin() );
			titlePattern = insertPatternPart( titlePattern, "id", String.valueOf( tvAccount.getId() ) );
			titlePattern = insertPatternPart( titlePattern, "identifier", tvAccount.getIdentifier() );

			StringBuilder macString = new StringBuilder();

			if ( tvAccount.getMacAddressList() != null && tvAccount.getMacAddressList().size() > 0 )
			{
				for ( byte[] mac : tvAccount.getMacAddressList() )
				{
					macString.append( Utils.bytesToString( mac, true, null ) );
					macString.append( ", " );
				}

				macString.setLength( macString.length() - 2 );
			}

			titlePattern = insertPatternPart( titlePattern, "macAddress", macString.toString() );

			tvAccount.setTitle( titlePattern );

			logger.info( titlePattern );
			script.accept( titlePattern );

			tvAccountDao.update( tvAccount );
			tvAccountDao.updateDeviceStateAndOptions( tvAccount.getId(), tvAccount.getDeviceAccountId(), tvAccount.getDeviceState(), null, 0 );
			context.publishAfterCommit( new TvAccountModifiedEvent( moduleId, tvAccount.getContractId(), 0, null, tvAccount ) );

			logger.info( "Link terminal:" + terminalId + " with tvAccount:" + tvAccount.getId() );
			script.accept( "Link terminal:" + terminalId + " with tvAccount:" + tvAccount.getId() );
		}

		if ( (children.size() > 1 && notFoundAccounts.size() < children.size())
			|| (parentTvAccount.getDateTo() != null && new Date().after( parentTvAccount.getDateTo() )) )
		{
			for ( TvAccount tvAccount : notFoundAccounts )
			{
				logger.info( "Unlink terminal:" + tvAccount.getDeviceAccountId() + " with tvAccount:" + tvAccount.getId() );
				script.accept( "Unlink terminal:" + tvAccount.getDeviceAccountId() + " with tvAccount:" + tvAccount.getId() );

				tvAccount.setDateTo( Date.from( LocalDate.now().minus( 1, ChronoUnit.DAYS ).atStartOfDay( ZoneId.systemDefault() ).toInstant() ) );

				tvAccountDao.update( tvAccount );
				tvAccountDao.updateDeviceStateAndOptions( tvAccount.getId(), tvAccount.getDeviceAccountId(), TvAccount.STATE_DELETED, null, 0 );
				context.publishAfterCommit( new TvAccountModifiedEvent( moduleId, tvAccount.getContractId(), 0, tvAccount, null ) );
			}
		}
	}
}
