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

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

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

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

import ru.bitel.bgbilling.common.BGException;
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.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.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.TimeUtils;
import ru.bitel.common.Utils;
import ru.bitel.common.sql.ConnectionSet;

public class CommpassTerminalSynchronizingTask
	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 String val = Optional.ofNullable( moduleSetup.get( ModuleSetupKeys.OM_COMMPASS_DEVICE_TYPE_IDS ) )
		    .orElse( moduleSetup.get( ModuleSetupKeys.OM_DEVICE_TYPE_IDS ) );
		final List<Integer> deviceTypeIds = Utils.toIntegerList( val );
		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();

		String host = null;
		int port = 0;

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

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

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

		URL url = URI.create( config.get( "om.url", config.get( "commpass.api.url", "https://" + host + ":" + port + "/" ) ) ).toURL();

		logger.info( "URL: {}", url );

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

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

		logger.info( "terminalTvAccountSpecId: " + terminalTvAccountSpecId );

		if ( terminalTvAccountSpecRuntime == null )
		{
			print( "terminalTvAccountSpecRuntime is null!" );
			logger.error( "terminalTvAccountSpecRuntime is null!" );
		}

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

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

		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, JsonClient jsonClient )
		throws Exception
	{
		Date now = new Date();

		Calendar calendar = new GregorianCalendar();
		TimeUtils.clear_MIN_MIL_SEC( calendar );
		calendar.add( Calendar.HOUR, -240 * 3 );

		JSONArray stbArray;

		try
		{
			stbArray = jsonClient.invokeAndGetArray( Method.get, null, "rest/stb/format/json/registered_from", TimeUtils.format( calendar, "yyyy-MM-dd HH:mm:ss" ), null );
		}
		catch( BGException ex )
		{
			logger.info( "Request return: " + ex.getMessage() );
			print( "Request return: " + ex.getMessage() );
			return;
		}

		print( "STB: \n" + String.valueOf( stbArray ) + "\n" );

		final Map<Long, List<JSONObject>> stbMap = new HashMap<Long, List<JSONObject>>();

		for ( int i = 0, size = stbArray.length(); i < size; i++ )
		{
			final JSONObject stb = stbArray.getJSONObject( i );

			final long subscriberId = stb.getLong( "subscriber_id" );

			List<JSONObject> list = stbMap.get( subscriberId );
			if ( list == null )
			{
				list = new ArrayList<JSONObject>( 5 );
				stbMap.put( subscriberId, list );
			}

			list.add( stb );
		}

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

			final long subscriberId = Utils.parseLong( tvAccount.getDeviceAccountId() );
			if ( subscriberId <= 0 )
			{
				logger.info( "SubscriberId not specified for tvAccount:" + tvAccount.getId() );
				print( "SubscriberId not specified for tvAccount:" + tvAccount.getId() );
				continue;
			}

			processAccount( context, connectionSet, moduleId, tvDeviceMap, tvDevice, tvAccountDao, terminalTvAccountSpecRuntime, tvAccountService, stbMap, subscriberId, tvAccount );

			context.commit();
		}
	}

	private void processAccount( ServerContext context, ConnectionSet connectionSet, int moduleId, TvDeviceMap tvDeviceMap, TvDevice tvDevice, TvAccountDao tvAccountDao, TvAccountSpecRuntime terminalTvAccountSpecRuntime, TvAccountService tvAccountService, Map<Long, List<JSONObject>> stbMap, long subscriberId, TvAccount parentTvAccount )
		throws Exception
	{
		List<TvAccount> children = tvAccountDao.listChildren( parentTvAccount.getId() );

		List<JSONObject> stbList = stbMap.get( subscriberId );
		if ( stbList == null )
		{
			return;
		}

		STB:
		for ( JSONObject stb : stbList )
		{
			if ( children != null )
			{
				for ( TvAccount child : children )
				{
					if ( Utils.parseLong( child.getDeviceAccountId() ) == stb.getLong( "id" ) )
					{
						continue STB;
					}

					if ( Utils.isBlankString( child.getDeviceAccountId() )
						&& stb.getString( "mac_addr" ).equalsIgnoreCase( TvUtils.macAddressToString( child.getMacAddressListBytes() ) ) )
					{
						logger.info( "Found not linked by deviceAccountId terminal" );
						print( "Found not linked by deviceAccountId terminal" );

						String deviceAccountId = String.valueOf( stb.getLong( "id" ) );

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

						continue STB;
					}
				}
			}

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

			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( stb.getString( "mac_addr" ) ) ) );

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

			tvAccount.setComment( "" );

			tvAccount.setDeviceState( TvAccount.STATE_ENABLE );
			tvAccount.setDeviceAccountId( String.valueOf( stb.getLong( "id" ) ) );

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

			print( 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:" + stb.getLong( "id" ) + " with tvAccount:" + tvAccount.getId() );
			print( "Link terminal:" + stb.getLong( "id" ) + " with tvAccount:" + tvAccount.getId() );
		}
	}
}
