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

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

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 jakarta.annotation.Resource;
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.kernel.contract.api.common.bean.Contract;
import ru.bitel.bgbilling.kernel.contract.api.server.bean.ContractDao;
import ru.bitel.bgbilling.kernel.contract.api.server.dao.CustomerDao;
import ru.bitel.bgbilling.kernel.customer.common.bean.Customer;
import ru.bitel.bgbilling.kernel.module.common.bean.User;
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.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;

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

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

	private JsonClient jsonClient;

	private ContractDao contractDao;
	private CustomerDao customerDao;

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

	/**
	 * Если true - синхронизация на уровне сервисов, а не продуктов.
	 */
	private boolean serviceMode;
	
	private boolean customerFio = false; 
    private int customerFioPid;
	private int customerFirstNamePid;
	private int customerMiddleNamePid;
	private int customerLastNamePid;
	private int customerAddressPid;
	private int customerEmailPid;

	private List<Integer> customerEmailSources;
	
	private String loginPrefix;
    private String operatorName;

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

	public JSONArray invokeAndGetArray( final Method method, final String resource, final String id, final JSONObject obj )
		throws IOException, BGException, JSONException
	{
		return jsonClient.invokeAndGetArray( method, null, id != null ? ("v1/" + id + "/" + resource) : ("v1/" + resource), 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 );

		String host = null;
		int port = 0;

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

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

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

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

		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 );
		customerFio = config.getBoolean( "customer.fio", false );
        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" ) );
		
        operatorName = config.get( "account.operator.name" );
		loginPrefix = Utils.maskBlank( config.get( "account.login.prefix", "" ), "" );

		return null;
	}

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

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

        customerDao = new CustomerDao( ctx.getConnection() );
		contractDao = new ContractDao( ctx.getConnection(), User.USER_SERVER );

		return null;
	}

	@Override
	public Object disconnect( ServerContext ctx )
		throws Exception
	{
        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 );
		}
		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( contractDao, tvAccount.getContractId(), customerEmailPid );
					break;
			}

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

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

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

			JSONObject result = invoke( Method.post, "user", null, lifeStreamContract );
			userId = result.getString( "id" );
			e.getEntry().setDeviceAccountId( userId );
			accountStateModify0( e, userId );
		}
		// изменение аккаунта
		else
		{
			userId = e.getOldTvAccount().getDeviceAccountId();
			
			JSONObject lifeStreamContract = new JSONObject();
			//lifeStreamContract.put( "username", tvAccount.getLogin() );
			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, "user", null, 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 )
	    throws Exception
	{
	    final int contractId = contract.getId();
	    
        String fio = "";
        if ( customerFio )
        {
            int customerId = customerDao.getCustomerLink( contractId, LocalDateTime.now() )
                .map( a -> a.getCustomerId() ).orElse( 0 );
            if ( customerId > 0 )
            {
                List<String> fioItem = new ArrayList<>();
                for ( String key : Arrays.asList( Customer.Keys.FIRST_NAME.key(), 
                                                  Customer.Keys.MIDDLE_NAME.key(), 
                                                  Customer.Keys.LAST_NAME.key() ) )
                {
                    Optional.ofNullable( customerDao.getCustomerParameter( customerId, key ) )
                        .ifPresent( a -> fioItem.add( a ) );
                }
                fio = fioItem.stream().collect( Collectors.joining( " " ) );
            }
        }
        else
        {
            fio = customerFioPid > 0 ? contractDao.optContractParameterTextAsString( contractId, customerFioPid ).orElse( "" ) : 
		                               TvDynUtils.getName( contractDao, contract, customerLastNamePid, customerFirstNamePid, customerMiddleNamePid )[0];
        }
		
        String address = customerAddressPid > 0 ? contractDao.getContractParameterAddressAsString( contractId, customerAddressPid ).orElse( null ) : null;

        JSONObject info = new JSONObject();
		info.put( "contractId", contractId );
		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, "user", userId, 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 )
		throws Exception
	{
		logger.debug( "productsModifyFullSync" );

		final Set<String> servicesToAdd = new HashSet<>();
		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<>();

		// получаем список текущих активных сервисов
		JSONArray subscriptionArray = invokeAndGetArray( Method.get, "subscriptions", userId, 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, "subscriptions", userId, 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, "subscriptions", userId, 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 )
		{
			return productsModifySyncFull( e, ctx, userId );
		}

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

		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, "subscriptions", userId, 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 );
	}

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

		return productsModifySyncFull( e, ctx, userId );
	}

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

		return null;
	}

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