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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
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.TimeZone;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import bitel.billing.common.KernelConst;
import bitel.billing.server.contract.bean.Contract;
import bitel.billing.server.contract.bean.ContractEmailParamValue;
import bitel.billing.server.contract.bean.ContractManager;
import bitel.billing.server.contract.bean.ContractParameterManager;
import bitel.billing.server.contract.bean.ContractPatternManager;
import bitel.billing.server.util.PswdGen;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.common.BGMessageException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.contract.api.common.event.ContractModifiedEvent;
import ru.bitel.bgbilling.kernel.contract.balance.common.ChargeService;
import ru.bitel.bgbilling.kernel.contract.balance.common.PaymentService;
import ru.bitel.bgbilling.kernel.contract.balance.common.bean.Charge;
import ru.bitel.bgbilling.kernel.contract.balance.common.bean.Payment;
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalance;
import ru.bitel.bgbilling.kernel.contract.balance.server.ConvergenceBalanceManager;
import ru.bitel.bgbilling.kernel.contract.balance.server.bean.BalanceDao;
import ru.bitel.bgbilling.kernel.contract.param.common.bean.PhoneParamItem;
import ru.bitel.bgbilling.kernel.contract.param.common.bean.PhoneParamValue;
import ru.bitel.bgbilling.kernel.contract.runtime.ContractRuntime;
import ru.bitel.bgbilling.kernel.contract.runtime.ContractRuntimeMap;
import ru.bitel.bgbilling.kernel.event.EventProcessor;
import ru.bitel.bgbilling.modules.tv.access.om.event.OmTvProductsModifyEvent;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.api.common.service.TvAccountService;
import ru.bitel.common.TimeUtils;
import ru.bitel.common.Utils;
import ru.bitel.common.io.IOUtils;
import ru.bitel.common.model.Pair;
import ru.bitel.common.model.Period;
import ru.bitel.oss.systems.inventory.product.common.bean.Product;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductPeriod;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpec;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpecActivationMode;
import ru.bitel.oss.systems.inventory.product.common.event.ProductEntry;
import ru.bitel.oss.systems.inventory.product.common.service.ProductService;
import ru.bitel.oss.systems.inventory.product.server.ProductSpecMap;
import ru.bitel.oss.systems.inventory.product.server.ProductSpecMap.ProductSpecItem;
import ru.bitel.oss.systems.inventory.service.common.bean.Service;
import ru.bitel.oss.systems.inventory.service.common.event.ServiceEntry;
import ru.bitel.oss.systems.order.product.common.service.ProductOrderService;

public class Tv24hWebhookService
{
	private static final Logger logger = Logger.getLogger( Tv24hWebhookService.class );

	private final ServerContext context;
	private final Tv24hConf conf;

	private final String[] path;
	private final String resource;

	private final HttpServletRequest request;
	private final HttpServletResponse response;
	private DateFormat _fmt = TimeUtils.getDateFormat( "yyyy-MM-dd'T'HH:mm:ss", TimeZone.getTimeZone( "GMT" ) );

	public Tv24hWebhookService( ServerContext context, Tv24hConf conf, String[] path, String resource, HttpServletRequest request, HttpServletResponse response )
	{
		this.context = context;
		this.conf = conf;

		this.path = path;
		this.resource = resource;

		this.request = request;
		this.response = response;
	}

	public void handle()
		throws Exception
	{
        
        
		if( "webhook".equals( resource ) )
		{
		    ByteArrayOutputStream baos = new ByteArrayOutputStream();
	        IOUtils.transfer( request.getInputStream(), baos, 1024 );
	        logger.info( request.getPathInfo() +"<< " + baos.toString( "UTF-8" ) );
	        JSONTokener tokener = new JSONTokener( baos.toString( "UTF-8" ) );
	        JSONObject req = (JSONObject)tokener.nextValue();
            String event = req.optString( "event" );
            if( event != null )
            {
                webhook( req, event );
            }
		}
        else if( "packet".equals( resource ) )
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IOUtils.transfer( request.getInputStream(), baos, 1024 );
            logger.info( request.getPathInfo() +"<< " + baos.toString( "UTF-8" ) );
            JSONTokener tokener = new JSONTokener( baos.toString( "UTF-8" ) );
            JSONObject req = (JSONObject)tokener.nextValue();
            activateRequest( req );
        }
        else if( "delete_subscription".equals( resource ) )
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IOUtils.transfer( request.getInputStream(), baos, 1024 );
            logger.info( request.getPathInfo() +"<< " + baos.toString( "UTF-8" ) );
            JSONTokener tokener = new JSONTokener( baos.toString( "UTF-8" ) );
            JSONObject req = (JSONObject)tokener.nextValue();
            deactivateRequest( req );
        }
        else if( "auth".equals( resource ) )
        {
               userAuth(  );
        }
        else
		{
            
			logger.warn( "Unknown message" );
		}
	}

	private void webhook( final JSONObject req, final String event )
		throws Exception
	{
		switch( event )
		{
			case "user_create":
				userCreate( req, event );
				break;

			case "user_login":
				userCreate( req, event );
				break;

            case "transaction_create":
                if( conf.agentMode )
                {
                    transactionCreate( req, event );
                }
                else
                {
                    logger.info( "Skip webhook transaction_create in non agent mode" );
                }

                break;

            case "subscription_create":
                if( conf.agentMode )
                {
                    subscriptionCreate( req, event );
                }
                else
                {
                    logger.info( "Skip webhook subscription_create in non agent mode" );
                }

                break;

			case "subscription_update":
                if( conf.agentMode )
                {
                    subscriptionUpdate( req, event );
                }
                else
                {
                    logger.info( "Skip webhook subscription_update in non agent mode" );
                }
				
				break;

			default:
				logger.warn( "Unknown event: " + event );
				break;
		}
	}

	private TvAccount tvAccountFindByUsername( TvAccountService tvAccountService, String username, String userId )
		throws BGException
	{
		Date now = new Date();

		List<TvAccount> list = tvAccountService.tvAccountSearch( username, userId, 0, null, null, true );
		if( list != null )
		{
			for( TvAccount a : list )
			{
				if( a.getParentId() > 0 )
				{
					continue;
				}

				if( !TimeUtils.dateInRange( now, a.getDateFrom(), a.getDateTo() ) )
				{
					continue;
				}

				return a;
			}
		}

		return null;
	}

	protected void userCreate( final JSONObject req, final String event )
		throws Exception
	{
		logger.info( "userCreate" );

		JSONObject user = req.getJSONObject( "user" );

		long id = user.getLong( "id" );
		String lastName = user.optString( "last_name" );
		String firstName = user.optString( "first_name" );
		String username = user.getString( "username" );
		String remoteIp = filterCustomerIp( user.optString( "x_forwarded_for" ) );
		String email = user.optString( "email" );
		String timezone = user.optString( "timezone" );
		String phone = user.optString( "phone" );

		final ContractManager contractManager = new ContractManager( context.getConnection() );
		final ContractParameterManager contractParameterManager = new ContractParameterManager( context.getConnection() );

		final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );

		TvAccount tvAccount = tvAccountFindByUsername( tvAccountService, username, null );
		if( tvAccount != null )
		{
			logger.info( "Found already created tvAccount:" + tvAccount.getId() );
		}

		final Contract contract;

		if( tvAccount != null )
		{
			contract = contractManager.getContractById( tvAccount.getContractId() );

			logger.info( "Found contract with ID=" + contract.getId() );
			
			if( Utils.isBlankString( contract.getComment() ) )
			{
				String fi = getCustomerName( lastName, firstName );

				contract.setComment( fi );

				contractManager.updateContract( contract );
			}
		}
		else
		{
			contract = contractGetOrCreate( contractManager, lastName, firstName, phone, remoteIp );
		}
		
		LocalDate date = LocalDate.now();
		BalanceDao balanceDao = new BalanceDao( context.getConnection() );
		
		final BigDecimal balance;
		if( contract.isDependSub() )
		{
			balance = balanceDao.getBalance( contract.getSuperId(), date.getYear(), date.getMonthValue() );
		}
		else
		{
			balance = balanceDao.getBalance( contract.getId(), date.getYear(), date.getMonthValue() );
		}

		balanceDao.recycle();

		if( conf.paramUserId > 0 )
		{
			contractParameterManager.updateStringParam( contract.getId(), conf.paramUserId, String.valueOf( id ), 0 );
		}

		if( conf.paramNameId > 0 && (Utils.notBlankString( lastName ) && Utils.notBlankString( firstName )) )
		{
			contractParameterManager.updateStringParam( contract.getId(), conf.paramNameId, getCustomerName( lastName, firstName ), 0 );
		}

		if( conf.paramLastNameId > 0 && Utils.notBlankString( lastName ) )
		{
			contractParameterManager.updateStringParam( contract.getId(), conf.paramLastNameId, lastName, 0 );
		}

		if( conf.paramFirstNameId > 0 && Utils.notBlankString( firstName ) )
		{
			contractParameterManager.updateStringParam( contract.getId(), conf.paramFirstNameId, firstName, 0 );
		}

		if( conf.paramEmailId > 0 && Utils.notBlankString( email ) )
		{
			ContractEmailParamValue emailValue = new ContractEmailParamValue();
			emailValue.setEmail( email );
			contractParameterManager.updateEmailParam( contract.getId(), conf.paramEmailId, emailValue, 0 );
		}

		if( conf.paramPhoneId > 0 && Utils.notBlankString( phone ) )
		{
			PhoneParamValue phoneValue = new PhoneParamValue();

			PhoneParamItem ppi = new PhoneParamItem();
			ppi.setPhone( phone );

			phoneValue.addPhoneItem( ppi );
			phoneValue.setPhones( phone );

			contractParameterManager.updatePhoneParam( contract.getId(), conf.paramPhoneId, phoneValue, 0 );
		}

		context.commit();
		
		boolean newAccount = false;

		if( tvAccount == null )
		{
		    newAccount = true;
			tvAccount = new TvAccount();

			tvAccount.setContractId( contract.getId() );
			tvAccount.setSpecId( conf.tvAccountSpecId );
			tvAccount.setDateFrom( new Date() );
			tvAccount.setDateTo( null );
			tvAccount.setStatus( TvAccount.STATUS_ACTIVE );
		}

		tvAccount.setLogin( username );
		//serv.setPassword( username );

		tvAccount.setIdentifier( String.valueOf( id ) );
		tvAccount.setDeviceAccountId( tvAccount.getIdentifier() );

		tvAccountService.tvAccountUpdate( tvAccount.getContractId(), tvAccount, false, newAccount, 0 );

		logger.info( "Created/updated tvAccount:" + tvAccount.getId() );

		//		TvAccountDao tvAccountDao = new TvAccountDao( context.getConnection(), context.getModuleId() );
		//		tvAccountDao.updateDeviceStateAndOptions( tvAccountId, deviceAccountId, state, deviceOptions, accessCode );

		contractManager.recycle();
		contractParameterManager.close();

		context.commit();

		user = new JSONObject();

		user.put( "id", id );
        user.put( "provider_uid", "bgb" + tvAccount.getId() );

		user.put( "username", tvAccount.getLogin() );
		user.put( "password", tvAccount.getPassword() );

		user.put( "is_provider_free", false );

		JSONObject account = new JSONObject();
		account.put( "id", contract.getId() );
		account.put( "amount", Utils.formatCost( balance ) );

		user.put( "account", account );

		JSONObject resp = new JSONObject();
		resp.put( "user", user );

		logger.info( ">> " + resp );

		resp.write( response.getWriter() );
	}
	
	
    protected void userAuth()
        throws Exception
    {
        logger.info( "auth" );

        String phone = request.getParameter( "phone" );
        String username = phone;
        int id = Utils.parseInt( request.getParameter( "mbr_id" ), -1 );
        if ( Utils.isEmptyString( phone ) || id < -1 )
        {
            logger.error( "Error auth paramenter" );
            return;
        }
        final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );
        TvAccount tvAccount = tvAccountFindByUsername( tvAccountService, username, null );
        JSONObject resp = new JSONObject();
        if ( tvAccount != null )
        {
            // нашли аккаунт
            logger.info( "Found already created tvAccount:" + tvAccount.getId() );
            int cid = tvAccount.getContractId();
            resp.put( "user_id", "bgb" + tvAccount.getId() );
        }
        else
        {
            // не нашли аккаунт
            resp.put( "status", "-1" );
            resp.put( "err", "-1" );
            resp.put( "errmsg", "User not found" );
        }

        context.commit();

        logger.info( ">> " + resp );
        resp.write( response.getWriter() );
    }
	
//	//TODO:Переделать потом на нормальные параметры, и вынести все это в утильный класс для  h24
//    private int findContractByPhone( String phone )
//    {
//
//        int res = -1;
//        try
//        {
//            Connection con = context.getConnection();
//            Statement st = con.createStatement();
//            String q = "SELECT cid FROM contract_parameter_type_phone_item WHERE pid="
//                       + String.valueOf( conf.paramPhoneId ) + " and phone='" + phone + "'";
//            // log(q);
//            ResultSet rs = st.executeQuery( q );
//            boolean first = true;
//            while ( rs.next() )
//            {
//                if ( !first )
//                {
//                    logger.info( "phone:" + phone + " MULTI CONTRACT" );
//                    return -1;
//                }
//                first = false;
//                res = rs.getInt( 1 );
//            }
//            rs.close();
//        }
//        catch( Exception ex )
//        {
//            logger.info( ex.getLocalizedMessage() );
//        }
//        return res;
//
//    }
	

	private Contract contractGetOrCreate( final ContractManager contractManager, String lastName, String firstName, String phone, String remoteIp )
		throws Exception, BGException
	{
		final Contract contract;

		final String pswd = PswdGen.generatePassword( context.getSetup() );

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

		logger.info( "TV mid=" + context.getModuleId() );
		logger.info( "contract pattern id=" + conf.contractPatternId );

		String contractTitlePattern = new ContractPatternManager( context.getConnection() ).getPattern( conf.contractPatternId ).getNamePattern();
		String contractTitle = contractTitlePattern.replaceAll( "\\{phone\\}", phone );
		// если нет phone - то создаем по стандартному макросу
		if( contractTitlePattern.equals( contractTitle ) )
		{
			contractTitle = null;
		}

		contract =
			contractManager.createFromPattern( conf.contractPatternId, contractTitle, new GregorianCalendar(), pswd, Collections.emptyMap(), true, 0,
											   KernelConst.SUB_MODE_DEPEND_BALANCE, null, 0, params );

		if( contract != null )
		{
			context.commit();
			logger.info( "Created contract with ID=" + contract.getId() );
		}

		String fi = getCustomerName( lastName, firstName );

		contract.setComment( fi );

		contractManager.updateContract( contract );
		
		return contract;
	}
	
	protected String getCustomerName( String lastName, String firstName )
	{
		return (Utils.maskBlank( lastName, "" ) + " " + Utils.maskBlank( firstName, "" )).trim();
	}

	/**
	 * Поиск ip-адреса клиента
	 *
	 * @param ipString {@link String} может содержать несколько адресов через ","
	 * @return
	 */
	protected String filterCustomerIp( String ipString )
	{
		return Utils.toList( ipString, "," ).stream().filter( s -> checkCustomerIp( s.trim() ) ).findFirst().map( s1 -> s1.trim() ).orElse( "" );
	}
	
	protected boolean checkCustomerIp( String ipString )
	{
		return true;
	}

	private void userLogin( final JSONObject req, final String event )
		throws JSONException, IOException, BGException
	{
		logger.info( "userLogin" );

		JSONObject user = req.getJSONObject( "user" );

		long id = user.getLong( "id" );
		String username = user.getString( "username" );

		final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );
		final TvAccount tvAccount = tvAccountFindByUsername( tvAccountService, username, null );

		if( tvAccount == null )
		{
			logger.warn( "Account not found for username:" + username );
			return;
		}

		if( id != Utils.parseLong( tvAccount.getDeviceAccountId(), -1 ) )
		{
			logger.warn( "Account deviceIdentifier not equals for username:" + username );
			return;
		}

		final ConvergenceBalance balance =
			ConvergenceBalanceManager.getInstance().getBalance( context.getConnectionSet(), tvAccount.getContractId(), System.currentTimeMillis() );

		user = new JSONObject();

		user.put( "id", id );
        user.put( "provider_uid", "bgb" + tvAccount.getId() );

		JSONObject account = new JSONObject();
		account.put( "id", tvAccount.getContractId() );
		account.put( "amount", Utils.formatCost( balance.getBalance() ) );

		user.put( "account", account );

		JSONObject resp = new JSONObject();
		resp.put( "user", user );

		logger.info( ">> " + resp );

		resp.write( response.getWriter() );
	}

	/*
	{
	            "event":	"transaction_create",
	            "transaction":	{
	                            "id":	987654321,
	                            "amount":	"399.00",
	                            "user":	{
	                                            "id":	832432439,
	                                            "provider_uid":	12345
	                            },
	                            "account":	{
	                                            "id":	45678
	                            }
	            }
	}
	
	{
	            "transaction":	{
	                            "id":	987654321,
	                            "pid":	1234567,
	                            "account":	{
	                                            "id":	45678,
	                                            "amount":	"201.00"
	                            }
	            }
	}
	 */
	/**
	 * Списание денежных средств
	 * @param req
	 * @param event
	 * @throws JSONException
	 * @throws IOException
	 * @throws BGException
	 */
	private void transactionCreate( final JSONObject req, final String event )
		throws JSONException, IOException, BGException
	{
		logger.info( "transactionCreate" );

		JSONObject transaction = req.getJSONObject( "transaction" );

		long tv24hTransactionId = transaction.getLong( "id" );
		BigDecimal amount = Utils.parseBigDecimal( transaction.getString( "amount" ), BigDecimal.ZERO );

		JSONObject user = transaction.getJSONObject( "user" );
		long userId = user.getLong( "id" );
		
        int accountId = getAccountId( user );
        if( accountId <= 0 )
        {
            logger.error( "Field 'provider_uid' not valid!" );
            response.sendError( HttpServletResponse.SC_NOT_FOUND, "Не заполнено поле 'provider_uid'" );

            return;
        }

		JSONObject account = transaction.getJSONObject( "account" );
		int tv24hAccountId = account.optInt( "id", -1 );

		if( tv24hAccountId <= 0 )
		{
			logger.error( "Balance error (account.id)" );
			response.sendError( HttpServletResponse.SC_FORBIDDEN );

			//EventProcessor.getInstance().publish( new ContractModifiedEvent( 0, tvAccount.getContractId() ) );

			return;
		}

		final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );
		final TvAccount tvAccount = tvAccountService.tvAccountGet( accountId );

		if( tvAccount == null )
		{
			logger.error( "TvAccount not found with id:" + accountId );
			response.sendError( HttpServletResponse.SC_NOT_FOUND );
			return;
		}

		if( tvAccount.getParentId() > 0 )
		{
			logger.error( "TvAccount with id:" + accountId + " is child" );
			response.sendError( HttpServletResponse.SC_NOT_FOUND );
			return;
		}

		if( tvAccount.getContractId() != tv24hAccountId )
		{
			logger.error( "TvAccount with id:" + accountId + " has contractId!=" + tv24hAccountId );
			response.sendError( HttpServletResponse.SC_NOT_FOUND );
			return;
		}

		LocalDate date = LocalDate.now();
		BalanceDao balanceDao = new BalanceDao( context.getConnection() );

		final ContractRuntime contractRuntime = ContractRuntimeMap.getInstance().getContractRuntime( context.getConnectionSet(), tvAccount.getContractId() );

		BigDecimal balance;
		if( contractRuntime.getSuperContractId() > 0 )
		{
			balance = balanceDao.getBalance( contractRuntime.getSuperContractId(), date.getYear(), date.getMonthValue() );
		}
		else
		{
			balance = balanceDao.getBalance( tvAccount.getContractId(), date.getYear(), date.getMonthValue() );
		}

		ConvergenceBalance convergenceBalance =
			ConvergenceBalanceManager.getInstance().getBalance( context.getConnectionSet(), tvAccount.getContractId(), System.currentTimeMillis() );

		if( convergenceBalance.getLimit().compareTo( balance.subtract( amount ) ) > 0 )
		{
			logger.error( "Balance/limit error" );
			response.sendError( HttpServletResponse.SC_CONFLICT );

			// здесь отправка события, которая в свою очередь вызывает отправку текущего баланса на MW
			EventProcessor.getInstance().publish( new ContractModifiedEvent( 0, tvAccount.getContractId() ) );

			return;
		}

		int chargeId;

		if( conf.paymentTypeId > 0 && BigDecimal.ZERO.compareTo( amount ) > 0 )
		{
			final PaymentService paymentService = context.getService( PaymentService.class, 0 );

			final Payment payment = new Payment();
			payment.setContractId( tvAccount.getContractId() );
			payment.setTypeId( conf.chargeTypeId );
			payment.setUserId( 0 );
			payment.setSum( amount.negate() );
			payment.setComment( "24h.tv:" + tv24hTransactionId );
			payment.setDate( new Date() );

			paymentService.paymentUpdate( payment, null );

			chargeId = payment.getId();
		}
		else
		{
			final ChargeService chargeService = context.getService( ChargeService.class, 0 );

			final Charge charge = new Charge();
			charge.setContractId( tvAccount.getContractId() );
			charge.setTypeId( conf.chargeTypeId );
			charge.setUserId( 0 );
			charge.setSum( amount );
			charge.setComment( "24h.tv:" + tv24hTransactionId );
			charge.setDate( new Date() );

			chargeService.chargeUpdate( charge );

			chargeId = charge.getId();
		}

		context.commit();

		if( contractRuntime.getSuperContractId() > 0 )
		{
			balance = balanceDao.getBalance( contractRuntime.getSuperContractId(), date.getYear(), date.getMonthValue() );
		}
		else
		{
			balance = balanceDao.getBalance( tvAccount.getContractId(), date.getYear(), date.getMonthValue() );
		}

		balanceDao.recycle();

		transaction = new JSONObject();
		transaction.put( "id", tv24hTransactionId );
		transaction.put( "pid", chargeId );

		account = new JSONObject();
		account.put( "id", tvAccount.getContractId() );
		account.put( "amount", Utils.formatCost( balance ) );

		transaction.put( "account", account );

		JSONObject resp = new JSONObject();
		resp.put( "transaction", transaction );

		logger.info( ">> " + resp );

		resp.write( response.getWriter() );
	}

	/**
	 * Получение accountId из поля provider_uid, в котором может быть префикс bgb.
	 * @param user
	 * @return
	 */
    private int getAccountId( JSONObject user )
    {
        String providerUid = user.optString( "provider_uid", null );
        if( providerUid != null && providerUid.startsWith( "bgb" ) )
        {
            providerUid = providerUid.substring( 3 );
        }

        return Utils.parseInt( providerUid, -1 );
    }

	/*
	{
	            "event":	"subscription_create",
	            "subscription":	{
	                            "id":	41236419,
	                            "packet":	{
	                                            "id":	8,
	                                            "name":	"VIP",
	                                            "price":	"1000.00",												"base":	true
	                            },
	                            "transaction":	{
	                                            "id":	1234567,
	                                            "amount":	"-1000.00",
	                                            "status":	"hold"
	                            },
	                            "start_at":	"2017-03-24T19:54:18Z",
	                            "end_at":	"2017-04-24T19:54:18Z",
	                            "created_at":	"2017-03-24T19:54:18Z",
	                            "updated_at":	"2017-03-24T19:54:18Z",
	                            "renew":	true
	            }
	}
	 */
	private void subscriptionCreate( final JSONObject req, final String event )
		throws JSONException, IOException, BGException, ParseException
	{
		logger.info( "subscriptionCreate" );

		subscriptionUpdate( req, event, true );
	}

	/*
	 {
	             "event":	"subscription_update",
	            "subscription":	{
	                            "id":	41236419,
	                            "packet":	{
	                                            "id":	8,
	                                            "name":	"VIP",
	                                            "price":	"1000.00",
	                                            "base":	true
	                            },
	                            "transaction":	{
	                                            "id":	1234567,
	                                            "amount":	"-32.25",
	                                            "status":	"complete"
	                            },
	                            "start_at":	"2017-03-24T19:54:18Z",
	                            "end_at":	"2017-03-25T00:54:18Z",
	                            "created_at":	"2017-03-24T19:54:18Z",
	                            "updated_at":	"2017-03-25T00:54:18Z",
	                            "renew":	false
	            }
	}
	 */
	private void subscriptionUpdate( final JSONObject req, final String event )
		throws JSONException, IOException, BGException, ParseException
	{
		logger.info( "subscriptionUpdate" );

		subscriptionUpdate( req, event, false );
	}

	private Date parseDate( String s )
		throws ParseException
	{
		return (Utils.isBlankString( s ) || "null".equals( s )) ? null : _fmt.parse( s );
	}

	/**
	 * Обновление или добавление подписки
	 * @param req
	 * @param event
	 * @param add
	 * @throws JSONException
	 * @throws IOException
	 * @throws BGException
	 * @throws ParseException
	 */
	private void subscriptionUpdate( final JSONObject req, final String event, boolean add )
		throws JSONException, IOException, BGException, ParseException
	{
		JSONObject subscription = req.getJSONObject( "subscription" );

		long tv24hSubscriptionId = subscription.getLong( "id" );

		JSONObject packet = subscription.getJSONObject( "packet" );
		JSONObject user = subscription.getJSONObject( "user" );

		JSONArray pauses = subscription.optJSONArray( "pauses" );

		long tv24hUserId = user.getLong( "id" );

		long tv24hSubscriptionTransactionId = 0;
		BigDecimal tv24hSubscriptionTransactionAmount = BigDecimal.ZERO;
		JSONObject transaction = subscription.optJSONObject( "transaction" );
		if( transaction != null )
		{
			tv24hSubscriptionTransactionId = transaction.getLong( "id" );
			tv24hSubscriptionTransactionAmount = Utils.parseBigDecimal( transaction.getString( "amount" ), BigDecimal.ZERO );
			if( BigDecimal.ZERO.compareTo( tv24hSubscriptionTransactionAmount ) != 0 )
			{
				tv24hSubscriptionTransactionAmount = tv24hSubscriptionTransactionAmount.negate();
			}
		}

		Date startAt = parseDate( subscription.getString( "start_at" ) );
		Date endAt = parseDate( subscription.getString( "end_at" ) );

		long packetId = packet.getLong( "id" );

		final ProductService productService = context.getService( ProductService.class, 0 );

		final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );
		final TvAccount tvAccount = tvAccountFindByUsername( tvAccountService, null, String.valueOf( tv24hUserId ) );

		if( tvAccount == null )
		{
			logger.error( "TvAccount not found with deviceAccountId:" + tv24hUserId );
			response.sendError( HttpServletResponse.SC_NOT_FOUND );
			return;
		}

		ProductSpecItem productSpecItem = ProductSpecMap.getInstance().getByIdentifier( String.valueOf( packetId ) );

		Product product = null;

		List<Product> productList = productService.productList( -1, tvAccount.getContractId(), tvAccount.getId(), true, null, null, null, null, false, false );

		Collections.reverse( productList );

		// подписка может быть уже активирована, т.е. это обновление или повторный запрос
		for( Product p : productList )
		{
			if( Utils.notEmptyString( p.getDeviceProductId() ) && p.getDeviceProductId().startsWith( tv24hSubscriptionId + "-" ) )
			{
				product = p;
				break;
			}
		}

		// подписка могла быть активирована через биллинг, т.е. запись уже лежит в БД, не нужно создавать новую
		if( product == null )
		{
			for( Product p : productList )
			{
				if( Utils.isBlankString( p.getDeviceProductId() ) && productSpecItem != null && productSpecItem.productSpec.getId() == p.getProductSpecId() )
				{
					product = p;
					break;
				}
			}
		}

		if( product == null )
		{
			if( !add )
			{
				logger.warn( "Not found product for subscription_update with subscription.id=" + tv24hSubscriptionId + ". Creating new" );
			}

			logger.info( "Create new product with productSpecIdentifier=" + packetId );

			if( productSpecItem == null )
			{
				logger.error( "Not found productSpec with identifier=" + packetId );
				return;
			}

			product = new Product();
			product.setContractId( tvAccount.getContractId() );
			product.setAccountId( tvAccount.getId() );

			product.setProductSpecId( productSpecItem.productSpec.getId() );
			product.setActivationModeId( productSpecItem.activationModeMap.values().stream().findFirst().map( a -> a.getId() ).orElse( 0 ) );

			if( product.getActivationModeId() <= 0 )
			{
				logger.error( "Not found activation mode for productSpec:" + productSpecItem.productSpec.getId() );
				response.sendError( HttpServletResponse.SC_NOT_FOUND );
				return;
			}

			product.setAccountId( tvAccount.getId() );

			product.setActivationTime( new Date() );
		}
		else
		{
			logger.info( "Update existing product with productSpecIdentifier=" + packetId );
		}

		String deviceProductId = tv24hSubscriptionId + "-";
		deviceProductId += tv24hSubscriptionTransactionId > 0 ? tv24hSubscriptionTransactionId : "";
		product.setDeviceProductId( deviceProductId );

		product.setActivationPrice( tv24hSubscriptionTransactionAmount );

		product.setTimeFrom( startAt );
		product.setTimeTo( endAt );

		List<Period> activePeriods = extractActivePeriods( product, pauses );

		if( product.getTimeFrom().compareTo( new Date() ) > 0 )
		{
			product.setDeviceState( Product.STATE_ENABLED );
		}
		else if( product.getTimeTo().compareTo( Date.from( Instant.now().plus( 60, ChronoUnit.SECONDS ) ) ) < 0 )
		{
			product.setDeviceState( Product.STATE_DISABLED );
		}
		else
		{
			Date date1 = new Date();
			Date date2 = Date.from( Instant.now().plus( 30, ChronoUnit.SECONDS ) );

			short state = Product.STATE_DISABLED;

			for( Period p : activePeriods )
			{
				if( TimeUtils.checkDateIntervalsIntersection( date1, date2, p.getDateFrom(), p.getDateTo() ) )
				{
					state = Product.STATE_ENABLED;
					break;
				}
			}

			product.setDeviceState( state );
		}

		if( add )
		{
			product.setComment( "Создано через webhook" );
		}
		else
		{
			product.setComment( "Обновлено через webhook" );
		}

		product.setDescription( "" );

		productService.productUpdate( product );

		logger.info( "Product created/updated with id=" + product.getId() );

		List<ProductPeriod> productPeriodList = productService.productPeriodList( tvAccount.getContractId(), product.getId() );

		syncProductPeriods( product, productPeriodList, activePeriods );

		if( product.getDeviceState() == Product.STATE_ENABLED )
		{
			sendProductModifyEvent( tvAccount, product );
		}

		context.commit();
	}

	private void sendProductModifyEvent( final TvAccount tvAccount, Product product )
	{
		List<ProductEntry> productEntryList = new ArrayList<>();

		ProductEntry productEntry = new ProductEntry( product, product, Product.STATE_DISABLED, Product.STATE_ENABLED );

		productEntryList.add( productEntry );

		// по этому продуктe нужно всю информацию отправлять Access'у
		final List<Service> serviceList = product.getServiceList();
		product.setServiceList( null );

		final List<ServiceEntry> serviceEntryList = new ArrayList<ServiceEntry>();

		if( serviceList != null )
		{
			for( Service service : serviceList )
			{
				serviceEntryList.add( new ServiceEntry( service, service, productEntry.getOldState(), Service.STATE_REMOVED ) );
			}
		}

		productEntry.setServiceEntryList( serviceEntryList );

		context.publishAfterCommit( new OmTvProductsModifyEvent( context.getModuleId(), product.getContractId(), 0, tvAccount.getDeviceId(), tvAccount.getId(),
																 productEntryList, null ) );
	}

	private List<Period> extractActivePeriods( Product product, JSONArray pauses )
		throws ParseException
	{
		if( pauses == null || pauses.length() == 0 )
		{
			return Collections.singletonList( new Period( product.getTimeFrom(), product.getTimeTo() ) );
			//return null;
		}

		List<Period> pausePeriodList = new ArrayList<>();

		for( int i = 0, size = pauses.length(); i < size; i++ )
		{
			JSONObject pause = pauses.getJSONObject( i );

			Date pauseStartAt = parseDate( pause.getString( "start_at" ) );
			Date pauseEndAt = parseDate( pause.optString( "end_at" ) );

			Period period = new Period( pauseStartAt, pauseEndAt );

			pausePeriodList.add( period );
		}

		Collections.sort( pausePeriodList, ( a, b ) -> a.getDateFrom().compareTo( b.getDateFrom() ) );

		List<Period> activePeriods = new ArrayList<>();

		Period current = new Period();
		current.setDateFrom( product.getTimeFrom() );

		for( Period pausePeriod : pausePeriodList )
		{
			current.setDateTo( pausePeriod.getDateFrom() );

			activePeriods.add( current );

			current = new Period();
			current.setDateFrom( pausePeriod.getDateTo() );
		}

		if( current.getDateFrom() != null )
		{
			current.setDateTo( product.getTimeTo() );

			activePeriods.add( current );
		}

		return activePeriods;
	}

	private void syncProductPeriods( Product product, List<ProductPeriod> productPeriodList, List<Period> activePeriods )
		throws BGException
	{
		boolean needRecreate = false;
		Pair<ProductPeriod, Period> needUpdateLast = null;
		List<Period> needAdd = null;

		if( productPeriodList.size() > activePeriods.size() )
		{
			needRecreate = true;
		}
		else
		{
			for( int i = 0; i < productPeriodList.size(); i++ )
			{
				ProductPeriod pp = productPeriodList.get( i );
				Period p = activePeriods.get( i );

				if( !TimeUtils.dateEqual( pp.getTimeFrom(), p.getDateFrom() ) || !TimeUtils.dateEqual( pp.getTimeTo(), p.getDateTo() ) )
				{
					// если это последний период - можно обновить
					if( i + 1 == productPeriodList.size() )
					{
						needUpdateLast = new Pair<>( pp, p );
					}
					else
					{
						needRecreate = true;
						break;
					}
				}
			}

			if( !needRecreate && activePeriods.size() > productPeriodList.size() )
			{
				needAdd = new ArrayList<>();

				for( int i = productPeriodList.size(); i < activePeriods.size(); i++ )
				{
					Period p = activePeriods.get( i );

					needAdd.add( p );
				}
			}
		}

		ProductService productService = context.getService( ProductService.class, 0 );

		if( needRecreate )
		{
			for( ProductPeriod pp : productPeriodList )
			{
				productService.productPeriodDelete( product.getContractId(), pp.getId() );
			}

			for( Period p : activePeriods )
			{
				ProductPeriod period = new ProductPeriod();
				period.setContractId( product.getContractId() );
				period.setAccountId( product.getAccountId() );
				period.setProductSpecId( product.getProductSpecId() );
				period.setProductId( product.getId() );
				period.setActivationTime( new Date() );

				period.setProlongationTime( new Date() );
				period.setTimeFrom( p.getDateFrom() );
				period.setTimeTo( p.getDateTo() );

				productService.productPeriodUpdate( product.getContractId(), period );
			}
		}
		else
		{
			if( needUpdateLast != null )
			{
				ProductPeriod period = needUpdateLast.getFirst();
				period.setTimeFrom( needUpdateLast.getSecond().getDateFrom() );
				period.setTimeTo( needUpdateLast.getSecond().getDateTo() );

				period.setVersion( period.getVersion() + 1 );

				period.setProlongationTime( new Date() );

				productService.productPeriodUpdate( product.getContractId(), period );
			}

			if( needAdd != null )
			{
				for( Period p : needAdd )
				{
					ProductPeriod period = new ProductPeriod();
					period.setContractId( product.getContractId() );
					period.setAccountId( product.getAccountId() );
					period.setProductSpecId( product.getProductSpecId() );
					period.setProductId( product.getId() );
					period.setActivationTime( new Date() );

					period.setProlongationTime( new Date() );
					period.setTimeFrom( p.getDateFrom() );
					period.setTimeTo( p.getDateTo() );

					productService.productPeriodUpdate( product.getContractId(), period );
				}
			}
		}
	}
	
	/**
	 * Запрос активации подписки абонентом
	 * @param req
	 * @throws Exception
	 */
    private void activateRequest( final JSONObject req )
        throws Exception
    {
        final JSONObject user = req.getJSONObject( "user" );
        final JSONObject packet = req.getJSONObject( "packet" );
        final int tvAccountId = getAccountId( user );

        final TvAccountService accountService = context.getService( TvAccountService.class, context.getModuleId() );
        final ProductService productService = context.getService( ProductService.class, context.getModuleId() );
        final ProductOrderService productOrderService = context.getService( ProductOrderService.class, context.getModuleId() );

        final TvAccount tvAccount = accountService.tvAccountGet( tvAccountId );

        if( tvAccount == null )
        {
            sendError( "Аккаунт провайдера не найден!" );
            logger.warn( "Account not found: " + tvAccountId );
            return;
        }

        String productIdentifier = String.valueOf( packet.getInt( "id" ) );

        final ProductSpec productSpec = productService.productSpecGetByIdentifier( productIdentifier );
        if( productSpec == null )
        {
            sendError( "Не найден запрашиваемый пакет провайдера." );
            logger.warn( "Product not found with identifier: " + productIdentifier );
            return;
        }
        
        ProductSpecActivationMode productSpecActivationMode = Utils.maskNull( productSpec.getActivationModeList() ).stream()
            .filter( a -> TimeUtils.dateInRange( new Date(), a.getDateFrom(), a.getDateTo() ) )
            .findFirst().orElse( null );
        if( productSpecActivationMode == null )
        {
            sendError( "Не найден подходящий режим активации пакета провайдера." );
            logger.warn( "ProductSpecActivationMode not found for product: " + productIdentifier );
            return;
        }

        Product product = new Product();
        product.setContractId( tvAccount.getContractId() );
        product.setAccountId( tvAccount.getId() );
        product.setTimeFrom( new Date() );

        product.setProductSpecId( productSpec.getId() );
        product.setActivationModeId( productSpecActivationMode.getId() );

        product.setDescription( "" );

        product.setComment( "Активировано из MW" );

        try
        {
            productOrderService.productActivate( product, new Date(), true, true );
            
            JSONObject resp = new JSONObject();
            resp.put( "status", 1 );
            resp.write( response.getWriter() );
        }
        catch( BGMessageException ex )
        {
            sendError( ex.getMessage() );
            logger.info( ex.getMessage() );
            return;
        }
    }

    private void deactivateRequest( JSONObject req )
        throws Exception
    {
        JSONObject user = req.getJSONObject( "user" );
        JSONObject packet = req.getJSONObject( "subscription" ).getJSONObject( "packet" );
        int tvAccountId = getAccountId( user );

        final TvAccountService accountService = context.getService( TvAccountService.class, context.getModuleId() );
        final ProductService productService = context.getService( ProductService.class, context.getModuleId() );
        final ProductOrderService productOrderService = context.getService( ProductOrderService.class, context.getModuleId() );

        final TvAccount tvAccount = accountService.tvAccountGet( tvAccountId );

        if( tvAccount == null )
        {
            sendError( "Аккаунт провайдера не найден!" );
            logger.warn( "Account not found: " + tvAccountId );
            return;
        }

        String productIdentifier = String.valueOf( packet.getInt( "id" ) );

        final ProductSpec productSpec = productService.productSpecGetByIdentifier( productIdentifier );
        if( productSpec == null )
        {
            sendError( "Не найден запрашиваемый пакет провайдера." );
            logger.warn( "ProductSpec not found with identifier: " + productIdentifier );
            return;
        }

        List<Product> productList = productService.productList( context.getModuleId(), tvAccount.getContractId(), tvAccount.getId(), false, null, null,
            new Date(), null, false, false );

        Product product = productList.stream().filter( a -> a.getProductSpecId() == productSpec.getId() ).findFirst().orElse( null );

        if( product == null )
        {
            sendError( "Не найден запрашиваемый пакет на договоре провайдера." );
            logger.warn( "Product not found with identifier: " + productIdentifier + " and contractId: " + tvAccount.getContractId() );
            return;
        }

        try
        {
            productOrderService.productDeactivate( tvAccount.getContractId(), product.getId(), new Date(), true, true, false );
        }
        catch( Exception e )
        {
            sendError( "Не удалось отменить подписку на " + productSpec.getTitle() );
            return;
        }

        JSONObject resp = new JSONObject();
        resp.put( "status", 1 );
        resp.write( response.getWriter() );
    }

    private void sendError( String message )
        throws JSONException, IOException
    {
        JSONObject resp = new JSONObject();
        resp.put( "status", -1 );
        resp.put( "errmsg", message );

        resp.write( response.getWriter() );
    }
}
