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

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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 org.json.JSONTokener;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.common.BGMessageException;
import ru.bitel.bgbilling.common.model.KeyValue;
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.common.bean.ContractCreateData;
import ru.bitel.bgbilling.kernel.contract.api.common.bean.ContractTariff;
import ru.bitel.bgbilling.kernel.contract.api.common.event.ContractModifiedEvent;
import ru.bitel.bgbilling.kernel.contract.api.common.service.ContractService;
import ru.bitel.bgbilling.kernel.contract.api.common.service.ContractTariffService;
import ru.bitel.bgbilling.kernel.contract.api.server.bean.ContractDao;
import ru.bitel.bgbilling.kernel.contract.api.server.dao.ContractModuleDao;
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.common.service.ChargeService;
import ru.bitel.bgbilling.kernel.contract.balance.common.service.PaymentService;
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.pattern.server.bean.ContractPatternDao;
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.kernel.generator.server.bean.BGGeneratorDao;
import ru.bitel.bgbilling.kernel.module.common.bean.ContractModule;
import ru.bitel.bgbilling.kernel.module.common.bean.User;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServ;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServSearchParam;
import ru.bitel.bgbilling.modules.inet.common.bean.InetSessionLog;
import ru.bitel.bgbilling.modules.inet.common.service.InetServService;
import ru.bitel.bgbilling.modules.inet.common.service.InetSessionService;
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.event.access.om.OmTvProductsModifyEvent;
import ru.bitel.bgbilling.modules.tv.common.service.TvAccountService;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient;
import ru.bitel.bgbilling.modules.tv.server.TvUtils;
import ru.bitel.bgbilling.server.util.PswdGen;
import ru.bitel.common.TimeUtils;
import ru.bitel.common.Utils;
import ru.bitel.common.inet.IpRange;
import ru.bitel.common.io.IOUtils;
import ru.bitel.common.model.Pair;
import ru.bitel.common.model.Period;
import ru.bitel.oss.kernel.entity.common.bean.EntityAttrEmail;
import ru.bitel.oss.kernel.entity.common.bean.EntityAttrPhone;
import ru.bitel.oss.kernel.entity.common.bean.EntityAttrText;
import ru.bitel.oss.systems.inventory.product.common.bean.Product;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductOffering;
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;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
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.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.stream.Collectors;

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

    private final ServerContext context;
    private final Tv24hConf conf;
    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" ) );
    private JsonClient jsonClient = null;
    private final String[] path;

    // при одновременных запросах в данный сет кладется login(телефон), чтобы избежать дублирования при создании аккаунтов на договоре
    private static final Set<String> LOGIN_FOR_CREATE_LOCK = Collections.synchronizedSet( new HashSet<>() );

    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 ) )
        {
            webhookHandle();
        }
        else if ( "packet".equals( resource ) )
        {
            activateRequest( parseRequestToJSON( request.getInputStream() ) );
        }
        else if ( "delete_subscription".equals( resource ) )
        {
            deactivateRequest( parseRequestToJSON( request.getInputStream() ) );
        }
        else if ( "auth".equals( resource ) )
        {
            userAuth();
        }
        else if ( "balance".equals( resource ) )
        {
            getUserBalance( parseRequestToJSON( request.getInputStream() ) );
        }
        else
        {
            logger.warn( "Unknown message:"+resource );
        }
    }

    private void webhookHandle()
        throws Exception
    {
        logger.info( "handle webhook request" );
        JSONObject req = parseRequestToJSON( request.getInputStream() );

        //если тип запроса был передан в теле запроса
        String event = req.optString( "event" );
        if ( Utils.notBlankString( event ) )
        {
            webhook( req, event );
        }
        else
        {
            String webhookCommand = path.length > 5 ? path[5] : "";
            // в случае с запросом packet, проверяем не так, как в остальных
            if ( webhookCommand.equals( "packet" ) )
            {
                logger.debug( "Request type defined as packet" );
                activateRequest(req);
            }
            else if ( webhookCommand.equals( "auth" ) )
            {
                logger.debug( "Request type defined as auth" );
                userAuth();
            }
            else if ( webhookCommand.equals( "delete_subscription" ) )
            {
                logger.debug( "Request type defined as auth" );
                deactivateRequest( req );
            }
            else if ( webhookCommand.equals( "balance" ) )
            {
                logger.debug( "Request type defined as auth" );
                getUserBalance( req );
            }
            else
            {
                logger.error( "Cannot define request type for request body = " + req );
            }
        }
    }

    private JSONObject parseRequestToJSON( InputStream is )
        throws IOException
    {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
        {
            IOUtils.transfer( is, baos, 1024 );

            String requestBody = baos.toString( "UTF-8" );

            logger.info( request.getPathInfo() + " << " + requestBody );

            if ( Utils.isBlankString( requestBody ) )
                return new JSONObject();

            return (JSONObject)new JSONTokener( requestBody ).nextValue();
        }
    }

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

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

                break;

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

                break;

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

                break;

            case "update_balance":
                getUserBalance( req );
                break;

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

    private void getUserBalance(JSONObject req)
        throws IOException, Exception
    {
        logger.info( "updateBalance" );

        logger.info( "<<" + req.toString() );

        JSONObject user = req.optJSONObject( "user" );
        String providerUid = user.optString( "provider_uid" );
        int tv24UserId = user.optInt( "id", -1 );
        String phone = user.optString( "phone" );
        if ( !providerUid.startsWith( "bgb" ) )
        {
            logger.error( "Не передан provider_uid!" );
            response.sendError( 404 );
            return;
        }

        int accountId = Utils.parseInt( providerUid.substring( 3 ), -1 );
        if ( accountId < 0 )
        {
            logger.error( "Не передан accountId!" );
            response.sendError( 400 );
            return;
        }

        TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );
        TvAccount tvAccount = tvAccountService.tvAccountGet( -1, accountId );
        if ( tvAccount == null )
        {
            logger.error( "Не удалось найти аккаунт по id=" + accountId );
            return;
        }

        logger.info( "Найден аккаунт ID=" + tvAccount.getId() + ", cid=" + tvAccount.getContractId() );

        if ( Utils.notBlankString( tvAccount.getLogin() ) && !tvAccount.getLogin().equals( phone ) )
        {
            logger.info( "Нашли аккаунт с ID=" + tvAccount.getId() + ", но логины не совпадают. Пришёл логин в запросе = " + phone );
            response.sendError( 400 );
            return;
        }

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

        JSONObject resp = new JSONObject();
        resp.put( "provider_uid", providerUid );
        resp.put( "id", tv24UserId );
        resp.put( "balance", Utils.formatCost( balance.getBalance() ) );
        resp.put( "status", 1 );

        logger.info( ">> " + resp );
        resp.write( response.getWriter() );
    }

    private TvAccount tvAccountFindByLogin( TvAccountService tvAccountService, String login )
        throws Exception
    {
        return tvAccountFind( tvAccountService, login, null, null );
    }

    private TvAccount tvAccountFind( TvAccountService tvAccountService, String username, String deviceAccountId, String identifier )
        throws Exception
    {
        Date now = new Date();

        List<TvAccount> list = tvAccountService.tvAccountSearch( Arrays.asList( new KeyValue( "login", username ), new KeyValue( "extId", deviceAccountId ), new KeyValue( "identifier", identifier ) ), 0, true );
        if ( list != null )
        {
            for ( TvAccount account : list )
            {
                if ( account.getParentId() > 0 )
                {
                    continue;
                }

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

                return account;
            }
        }

        return null;
    }

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

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

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

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

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

        Contract contract = null;

        try ( ContractDao contractDao = new ContractDao( null, 0 ) )
        {
            if ( tvAccount != null )
            {
                contract = contractDao.get( tvAccount.getContractId() );
    
                logger.info( "Found contract with ID=" + contract.getId() );
    
                if ( Utils.isBlankString( contract.getComment() ) )
                {
                    String comment = getCustomerName( lastName, firstName );
                    contract.setComment( comment );
                    contractDao.update( contract );
                }
            }
            else
            {
                int contractId = getContractId( username );
                if ( contractId > 0 )
                {
                    contract = contractDao.get( contractId );
                }
    
                if ( contract == null && conf.isNeedCreateNewContract )
                {
                    contract = contractGetOrCreate( contractDao, lastName, firstName, phone );
                }
            }
    
            if ( contract == null )
            {
                logger.error( "Не удалось создать договор!" );
                response.sendError( HttpServletResponse.SC_BAD_REQUEST );
                return;
            }
        }

        final int contractId = contract.getId();
        
        //возможно сами создали договор и тогда добавляем модуль tv на договор
        addModuleToContract( context.getConnection(), context.getModuleId(), contractId );

        LocalDate date = LocalDate.now();
        int balanceContractId = contract.isDependSub() ? contract.getSuperContractId() : contractId;
        final BigDecimal balance = new BalanceDao( context.getConnection() ).getBalance( balanceContractId, date.getYear(), date.getMonthValue() );

        try ( ContractDao contractDao = new ContractDao( context.getConnection(), User.USER_SERVER ) )
        {
            if ( conf.paramUserId > 0 )
            {
                contractDao.updateContractParameter( contractId, new EntityAttrText( contractId, conf.paramUserId, String.valueOf( tv24UserId ) ) );
            }
    
            if ( conf.paramNameId > 0 && (Utils.notBlankString( lastName ) && Utils.notBlankString( firstName )) )
            {
                contractDao.updateContractParameter( contractId, new EntityAttrText( contractId, conf.paramNameId, getCustomerName( lastName, firstName ) ) );
            }
    
            if ( conf.paramLastNameId > 0 && Utils.notBlankString( lastName ) )
            {
                contractDao.updateContractParameter( contractId, new EntityAttrText( contractId, conf.paramLastNameId, lastName ) );
            }
    
            if ( conf.paramFirstNameId > 0 && Utils.notBlankString( firstName ) )
            {
                contractDao.updateContractParameter( contractId, new EntityAttrText( contractId, conf.paramFirstNameId, firstName ) );
            }
    
            if ( conf.paramEmailId > 0 && Utils.notBlankString( email ) )
            {
            	EntityAttrEmail attrEmail = new EntityAttrEmail( contractId, conf.paramEmailId );
            	attrEmail.setData( email );
            	contractDao.updateContractParameter( contractId, attrEmail );
            }
    
            if ( conf.paramPhoneId > 0 && Utils.notBlankString( phone ) && conf.isNeedUpdatePhoneOnContract )
            {
                EntityAttrPhone attrPhone = new EntityAttrPhone( contractId, conf.paramPhoneId );
                attrPhone.addContact( phone, "" );
    
                contractDao.updateContractParameter( contractId, attrPhone );
            }
        }
        context.commit();

        boolean newAccount = false;

        if ( tvAccount == null )
        {
            newAccount = true;
            tvAccount = new TvAccount()
                .setContractId( contractId )
                .setSpecId( conf.tvAccountSpecId )
                .setDateFrom( new Date() )
                .setDateTo( null )
                .setStatus( TvAccount.STATUS_ACTIVE );
        }

        tvAccount.setLogin( username )
            .setIdentifier( String.valueOf( tv24UserId ) )
            .setDeviceAccountId( tvAccount.getIdentifier() );

        String prefix = tvAccount.getId() > 0 ? "Created" : "Updated";

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

        logger.info( "{} tvAccount: {}", prefix, tvAccount.getId() );

        context.commit();

        JSONObject account = new JSONObject()
            .put( "id", contractId )
            .put( "amount", Utils.formatCost( balance ) );
        
        user = new JSONObject()
            .put( "id", tv24UserId )
            .put( "provider_uid", "bgb" + tvAccount.getId() )
            .put( "username", tvAccount.getLogin() )
            .put( "password", tvAccount.getPassword() )
            .put( "is_provider_free", false )
            .put( "account", account );

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

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

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

    protected void userAuth()
        throws Exception
    {
        logger.info( "auth" );

        String phone = request.getParameter( "phone" );
        int id = Utils.parseInt( request.getParameter( "mbr_id" ), -1 );
        String ip = request.getParameter( "ip" );
        
        logger.info( "auth, username>>" + phone + ", ip>>" + ip );
        if ( Utils.isEmptyString( phone ) || id < -1 )
        {
            logger.error( "Error auth parameter. Phone or mbr_id is empty!" );
            return;
        }
        final TvAccountService tvAccountService = context.getService( TvAccountService.class, context.getModuleId() );

        //если данный логин уже имеется в работе в другом потоке, то ждем (максимум 10 секунд) пока он от туда не удалится (пока другой поток не закоммитит его в базу)
        int counter = 0;
        while( LOGIN_FOR_CREATE_LOCK.contains( phone ) )
        {
            Thread.sleep( 500 );
            counter++;
            if ( counter == 20 )
                break;
        }

        LOGIN_FOR_CREATE_LOCK.add( phone );

        TvAccount tvAccount = tvAccountFindByLogin( tvAccountService, phone );
        JSONObject resp = new JSONObject();
        if ( tvAccount != null )
        {
            // нашли аккаунт
            logger.info( "Found already created tvAccount. AccountID:" + tvAccount.getId() );
            resp.put( "user_id", "bgb" + tvAccount.getId() );

            //возможно модуля тв на договоре не было, то добавляем (встречались случаи, что аккаунт был, а модуля на договоре не было)
            addModuleToContract( context.getConnection(), context.getModuleId(), tvAccount.getContractId() );
        }
        else
        {
            //пробуем найти по номеру телефона например
            int cid = getContractId( phone );
            if ( cid <= 0 )
            {
                cid = findContractIdByIp( ip );
            }

            // если нашли договор для такого номера, то попробуем создать аккаунт
            if ( cid > 0 )
            {
                addModuleToContract( context.getConnection(), context.getModuleId(), cid );

                tvAccount = new TvAccount();

                tvAccount.setContractId( cid );
                tvAccount.setSpecId( conf.tvAccountSpecId );
                tvAccount.setDateFrom( new Date() );
                tvAccount.setDateTo( null );
                tvAccount.setStatus( TvAccount.STATUS_ACTIVE );
                tvAccount.setLogin( phone );
                // serv.setPassword( username );
                tvAccount.setIdentifier( String.valueOf( id ) );
                tvAccount.setDeviceAccountId( tvAccount.getIdentifier() );
                tvAccountService.tvAccountUpdate( tvAccount.getContractId(), tvAccount, false, true, 0 );
                logger.info( "Created/updated tvAccount:" + tvAccount.getId() + " ContractID=" + cid );
                context.commit();
                resp.put( "user_id", "bgb" + tvAccount.getId() );
            }
            else
            {
                logger.info( "Contract not found by phone number= " + phone );
                //
                // не нашли аккаунт
                resp.put( "status", "-1" );
                resp.put( "err", "-1" );
                resp.put( "errmsg", "User not found" );
                // передаем на обработку операторам
                addNofindLog( phone, ip, id );
            }
        }

        context.commit();

        LOGIN_FOR_CREATE_LOCK.remove( phone );

        logger.info( ">> " + resp );
        resp.write( response.getWriter() );
    }

    private void addNofindLog( String phone, String ip, int userId )
    {
        logger.info( "addNofindLog..."  );
        try
        {
            StringBuilder sb = new StringBuilder();
            sb.append( TimeUtils.format( new Date(),"yyyy-MM-dd'T'HH:mm:ss"  ) );
            sb.append( "Не найден пользователь phone->" + phone + "; ip->" + ip + ";userId->" + userId );

            // пробуем найти по ip сессию
            if ( conf.inetId > 0 )
            {
                InetSessionLog session = findAliveInetSessionByIp( ip );
                if ( session != null )
                {
                    sb.append( ";sessionId->" + session.getId() + ";contract->" + session.getContractTitle() + "["
                               + session.getContractId() + "]" );
                }
            }

            sb.append( "\n" );
            logger.error( sb.toString() );
        }
        catch( Exception e )
        {
            logger.error( e );
        }
    }

    private void addModuleToContract( Connection connection, int moduleId, int contractId )
        throws Exception
    {
        new ContractModuleDao( connection ).addContractModule( new ContractModule().setContractId( contractId ).setModuleId( moduleId ) );
        context.publishAfterCommit( new ContractModifiedEvent( User.USER_SERVER, contractId ) );
    }

    private InetServ findInetServByIp( String ip )
        throws Exception
    {
        if ( conf.inetId <= 0 )
        {
            logger.info( "Не удалось получить ID модуля Inet из конфига!" );
            return null;
        }

        try
        {
            byte[] address = InetAddress.getByName( ip ).getAddress();
            IpRange ipRange = new IpRange( address, address );

            return context.getService( InetServService.class, conf.inetId )
                .searchInetServ( new InetServSearchParam().setIp( ipRange ), null )
                .getList()
                .stream()
                .filter( serv -> serv.getDateTo() == null || serv.getDateTo().after( new Date() ) )
                .findFirst()
                .orElse( null );
        }
        catch( UnknownHostException ex )
        {
            throw new BGException(ex);
        }
    }

    //поиск договора сначала по живой сессии с переданным ip,
    //если не нашли, то пробуем найти сервис с таким ip
    private int findContractIdByIp( String ip )
        throws Exception
    {
        if ( conf.inetId <= 0 )
            return -1;

        InetSessionLog session = findAliveInetSessionByIp( ip );
        if ( session != null )
            return session.getContractId();

        InetServ inetService = findInetServByIp( ip );
        if ( inetService != null )
            return inetService.getContractId();

        return -1;
    }

    private InetSessionLog findAliveInetSessionByIp( String ip )
        throws Exception
    {
        InetSessionLog aliveSession = null;
        List<InetSessionLog> sessionList = context.getService( InetSessionService.class, conf.inetId )
                                                  .inetSessionAliveList( new HashSet<>(), new HashSet<>(), "", "", ip, "", null, null, null ).getList();
        if ( !sessionList.isEmpty() )
        {
            aliveSession = sessionList.get( 0 );
        }

        return aliveSession;
    }

    private Contract contractGetOrCreate( final ContractDao contractDao, String lastName, String firstName, String phone )
        throws Exception
    {
        logger.info( String.format( "Create new contract. TV mid=%s, pattern ID=%s", context.getModuleId(), conf.contractPatternId ) );

        var contractPatternDao = new ContractPatternDao( context.getConnection() );
        int namePatternId = contractPatternDao
            .getPattern( conf.contractPatternId )
            .getNamePatternId();
        String contractTitlePattern = namePatternId > 0 ? new BGGeneratorDao( context.getConnection() ).get( namePatternId ).getPattern() : "";
        String contractTitle = contractTitlePattern.replaceAll( "\\{phone\\}", phone );
        // если нет phone - то создаем по стандартному макросу
        if ( contractTitlePattern.equals( contractTitle ) )
        {
            contractTitle = null;
        }
        
        ContractCreateData data = new ContractCreateData()
            .setPatternId( conf.contractPatternId )
            .setTitle( contractTitle )
            .setDateFrom( new Date() )
            .setPassword( PswdGen.generatePassword( context.getSetup() ) )
            .setContractSubMode( ru.bitel.bgbilling.kernel.contract.api.common.bean.Contract.SUB_MODE_DEPENDENT );

        ContractService contractService = context.getService( ContractService.class, 0 );
        final Contract contract = contractDao.get( contractService.contractCreate( data ) );
        if ( contract != null )
        {
            context.commit();
            logger.info( "Created contract with ID=" + contract.getId() );

            contract.setComment( getCustomerName( lastName, firstName ) );
            contractDao.update( contract );
        }
        return contract;
    }

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

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

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

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

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

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

        if ( tv24UserId != 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", tv24UserId );
        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 )
        throws Exception
    {
        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" );

        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.optJSONObject( "account" );
        if ( account == null )
        {
            logger.error( "account == null" );
            sendError( HttpServletResponse.SC_FORBIDDEN, 0, -1, BigDecimal.ZERO, "account == null" );
            return;
        }
        
        int tv24hAccountId = account.optInt( "id", -1 );
        if ( tv24hAccountId <= 0 )
        {
            logger.error( "tv24hAccountId <= 0" );
            sendError( HttpServletResponse.SC_FORBIDDEN, tv24hTransactionId, -1, BigDecimal.ZERO, "tv24hAccountId <= 0" );
            return;
        }

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

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

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

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

        LocalDate date = LocalDate.now();
        final ContractRuntime contractRuntime = ContractRuntimeMap.getInstance()
              .getContractRuntime( context.getConnectionSet(), tvAccount.getContractId() );
        
        int balanceContractId = contractRuntime.getSuperContractId() > 0 ? contractRuntime.getSuperContractId()
                                                                         : tvAccount.getContractId();
        BalanceDao balanceDao = new BalanceDao( context.getConnection() );
        BigDecimal balance = balanceDao.getBalance( balanceContractId, date.getYear(), date.getMonthValue() );

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

        if ( convergenceBalance.getLimit().compareTo( balance.subtract( amount ) ) > 0 )
        {
            sendError( HttpServletResponse.SC_CONFLICT, tv24hTransactionId, tvAccount.getContractId(), balance, "Not enough money in the account" );

            // здесь отправка события, которая в свою очередь вызывает отправку
            // текущего баланса на 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()
                .setContractId( tvAccount.getContractId() )
                .setTypeId( conf.chargeTypeId )
                .setUserId( 0 )
                .setSum( amount.negate() )
                .setComment( "24h.tv:" + tv24hTransactionId )
                .setDate( new Date() );

            paymentService.paymentUpdate( payment, null );

            chargeId = payment.getId();
        }
        else
        {
            final ChargeService chargeService = context.getService( ChargeService.class, 0 );
            final Charge charge = new Charge()
                .setContractId( tvAccount.getContractId() )
                .setTypeId( conf.chargeTypeId )
                .setUserId( 0 )
                .setSum( amount )
                .setComment( "24h.tv:" + tv24hTransactionId )
                .setDate( new Date() );

            chargeService.chargeUpdate( charge );

            chargeId = charge.getId();
        }

        context.commit();

        balance = balanceDao.getBalance( balanceContractId, date.getYear(), date.getMonthValue() );

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

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

        transaction.put( "account", account );

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

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

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

//  C ошибкой без pid:
//
//  {
//      "transaction": {
//          "id": 554039697509187753,
//          "account": {
//              "amount": "2.50",
//              "id": 95393
//          }
//      },
//      "error": "Not enough money in the account"
//  }
    private void sendError( int httpCpode, long tv24hTransactionId, int contractId, BigDecimal balance, String error )
        throws IOException
    {
        JSONObject account = new JSONObject();
        account.put( "id", contractId );
        account.put( "amount", Utils.formatCost( balance ) );
        
        JSONObject transaction = new JSONObject();
        transaction.put( "id", tv24hTransactionId );
        transaction.put( "account", account );
        
        JSONObject resp = new JSONObject();
        resp.put( "transaction", transaction );
        resp.put( "error", error );

        logger.error( ">> " + resp );
        
        response.setStatus( httpCpode );
        resp.write( response.getWriter() );

        response.sendError( httpCpode, error );
    }

    /**
     * Получение 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 )
        throws JSONException, IOException, Exception, ParseException
    {
        logger.info( "subscriptionCreate" );

        subscriptionUpdate( req, true );
    }

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

    /*
     * { "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 } }
     */

    /**
     * Обновление или добавление подписки
     * 
     * @param req
     * @param event тип события
     * @param add нужно ли создавать подписку, если не найдена
     * @throws JSONException
     * @throws IOException
     * @throws BGException
     * @throws ParseException
     */
    private void subscriptionUpdate( final JSONObject req, boolean add )
        throws JSONException, IOException, Exception, ParseException
    {
        logger.info( "subscriptionUpdate" );

        JSONObject subscription = req.getJSONObject( "subscription" );
        long tv24hSubscriptionId = subscription.getLong( "id" );
        JSONObject packet = subscription.getJSONObject( "packet" );
        JSONObject user = subscription.getJSONObject( "user" );

        String tvAccountId = user.optString( "provider_uid" );
        if ( Utils.notBlankString( tvAccountId ) && tvAccountId.startsWith( "bgb" ) )
        {
            tvAccountId = tvAccountId.replace( "bgb", "" );
        }

        // id пользователя на стороне 24h и так же у нас есть в бд, поэтому будем искать по нему тоже
        String identifier = String.valueOf( user.getInt( "id" ) );

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

        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() );
        TvAccount tvAccount = tvAccountService.tvAccountGet( 0, Utils.parseInt( tvAccountId, 0 ) );
        if ( tvAccount == null )
        {
            tvAccount = tvAccountFind( tvAccountService, null , tvAccountId, identifier );
        }

        if ( tvAccount == null )
        {
            logger.error( "TvAccount not found with deviceAccountId:" + tvAccountId + " and identifier:" + identifier );
            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;
            }

            final int activationModeId = productSpecItem.activationModeMap.values().stream()
                .findFirst().map( a -> a.getId() ).orElse( 0 );

            if ( activationModeId <= 0 )
            {
                logger.error( "Not found activation mode for productSpec:" + productSpecItem.productSpec.getId() );
                response.sendError( HttpServletResponse.SC_NOT_FOUND );
                return;
            }
            
            product = new Product()
                .setContractId( tvAccount.getContractId() )
                .setAccountId( tvAccount.getId() )
                .setProductSpecId( productSpecItem.productSpec.getId() )
                .setActivationModeId( activationModeId )
                .setAccountId( tvAccount.getId() )
                .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 );

        OmTvProductsModifyEvent omTvProductsModifyEvent = new OmTvProductsModifyEvent( context.getModuleId(), product.getContractId(),
                                                                                       0, tvAccount.getDeviceId(), tvAccount.getId(), 
                                                                                       productEntryList, null );
        omTvProductsModifyEvent.setSource( Tv24hWebhookService.class.getSimpleName() );
        
        context.publishAfterCommit( omTvProductsModifyEvent  );
    }

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

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

        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 Exception
    {
        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()
                    .setContractId( product.getContractId() )
                    .setAccountId( product.getAccountId() )
                    .setProductSpecId( product.getProductSpecId() )
                    .setProductId( product.getId() )
                    .setActivationTime( new Date() )
                    .setProlongationTime( new Date() )
                    .setTimeFrom( p.getDateFrom() )
                    .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()
                    	.setContractId( product.getContractId() )
                    	.setAccountId( product.getAccountId() )
                    	.setProductSpecId( product.getProductSpecId() )
                    	.setProductId( product.getId() )
                    	.setActivationTime( new Date() )
                    	.setProlongationTime( new Date() )
                    	.setTimeFrom( p.getDateFrom() )
                    	.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 String tv24SubscriptionId = packet.optString( "id" );
        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( 0, 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;
        }

        //если есть несовместимые, то взависимости от стоимости будет получена дата активации нового продукта, иначе просто активируем сейчас
        Date activationTimeNewProduct = new Date();

        //проверяем совместим ли новый продукт с активными на аккаунте
        //если совместим, то просто активируем новый продукт
        ContractRuntime contractRuntime = ContractRuntimeMap.getInstance().getContractRuntime( context.getConnection(), tvAccount.getContractId() );
        if ( !TvUtils.checkIncompatible( contractRuntime, null, tvAccountId, Calendar.getInstance(), Collections.emptySet(), productSpec ) )
        {
            logger.info( "На аккаунте accountId=" + tvAccount.getId() + ", для договора contractId=" + tvAccount.getContractId() + " есть активные несовместимые продукты" );
            //если не совместим, то начинаются пляски по деактивации уже активного продукта
            List<Product> activeProducts = productOrderService.activeProductList( tvAccount.getContractId(), tvAccountId, new Date() );
            //оставляем только те, что несовместимы с новым
            activeProducts.removeIf( activeProduct -> !productSpec.getIncompatible().contains( activeProduct.getProductSpecId() ) );
            logger.info( "Активные несовместимые:" + activeProducts.stream().mapToInt( Product::getProductSpecId ).boxed().map( String::valueOf ).collect( Collectors.joining(";") ) );

            ContractTariffService contractTariffService = context.getService( ContractTariffService.class, 0 );
            List<ContractTariff> contractTariffs = contractTariffService.contractTariffList( tvAccount.getContractId(), true, -1, -1 );
            List<ProductOffering> productOfferingList = new ArrayList<>();
            for ( ContractTariff contractTariff : contractTariffs )
            {
                productOfferingList.addAll( productOrderService.productTariffOfferingList( context.getModuleId(), contractTariff.getTariffPlanId(), new Date(), false ) );
            }

            logger.debug( String.format( "Тарифы на договоре: [%s]; offeringList size:[%s]",
                                         contractTariffs.stream().mapToInt( ContractTariff::getTariffPlanId ).boxed().map( String::valueOf ).collect( Collectors.joining( ",")),
                                         productOfferingList.size() ) );

            //по каждому(но, как правило, будет только 1 на аккаунте активный) выясняем стоимость и деактивируем
            for ( Product product : activeProducts )
            {
                BigDecimal productCost = null;
                ProductOffering offering = productOfferingList.stream().filter( productOffering -> productOffering.getProductSpec().getId() == product.getProductSpecId() ).findFirst().orElse( null );
                if ( offering!= null )
                {
                    productCost = offering.getPrice();
                }

                if ( productCost == null )
                {
                    //не удалось получить стоимость
                    logger.warn( String.format( "Не удалось получить стоимость продукта productSpecId=%s, productId=%s", productSpec.getId(), product.getId() ) );
                    continue;
                }

                BigDecimal newProductPrice = new BigDecimal( packet.getString( "price" ) );
                //если стоимость нового продукта больше стоимости активного, то деактивируем активный продукт и сразу активируем новый
                if ( newProductPrice.compareTo( productCost ) >= 0 )
                {
                    logger.info( String.format( "Стоимость нового продукта выше стоимости продукта=%s, на договоре contractId=%s, деактивируем.", product.getId(), product.getContractId() ) );
                    try
                    {
                        productOrderService.productDeactivate( tvAccount.getContractId(), product.getId(), new Date(), true, true, false );
                    }
                    catch( Exception e )
                    {
                        sendError( "Не удалось отменить подписку на " + productSpec.getTitle() );
                        return;
                    }
                }
                else
                {
                    //если стоимость нового продукта меньше уже активного, то у нового ставим дату начала - дату окончания активного
                    List<ProductPeriod> periods = productService.productPeriodList( tvAccount.getContractId(), product.getId() );
                    periods.sort( Comparator.comparingInt( ProductPeriod::getId ) );
                    Collections.reverse( periods );
                    activationTimeNewProduct = periods.get( 0 ).getTimeFrom();
                    //и для активного указываем renew=false в MW
                    TvDevice device = TvDeviceMap.getInstance( context.getModuleId() ).get( tvAccount.getDeviceId() ).getDevice();
                    String token = device != null ? Utils.maskBlank( device.getSecret(), "" ) : "";
                    logger.info( "Стоимость нового продукта меньше активного. Отправляем renew=false для продукта=" + tv24SubscriptionId );
                    logger.debug( "Время активации нового продукта установлено= " + TimeUtils.formatFullDate( activationTimeNewProduct ) );
                    disableRenewRequest( tvAccount.getIdentifier(), tv24SubscriptionId, token );
                }
            }
        }

        //и если дошли сюда, то активируем новый продукт на аккаунте
        Product product = new Product()
            .setContractId( tvAccount.getContractId() )
            .setAccountId( tvAccount.getId() )
            .setTimeFrom( new Date() )
            .setProductSpecId( productSpec.getId() )
            .setActivationModeId( productSpecActivationMode.getId() )
            .setDescription( "" )
            .setComment( "Активировано из MW" );

        try
        {
            productOrderService.productActivate( product, activationTimeNewProduct, true, true );

            JSONObject resp = new JSONObject();
            resp.put( "status", 1 );
            resp.write( response.getWriter() );
        }
        catch( BGMessageException ex )
        {
            sendError( ex.getMessage() );
            logger.info( ex.getMessage() );
        }
    }

    private JsonClient getJsonClient()
    {
        if ( jsonClient == null )
        {
            try
            {
                jsonClient = new JsonClient( URI.create( conf.providerURL ).toURL(), null, null );
            }
            catch( Exception e )
            {
                throw new RuntimeException( e );
            }
        }
        return jsonClient;
    }

    private JSONObject disableRenewRequest( String userId, String subscriptionId, String token )
        throws BGException, JSONException
    {
        Map<String, String> requestOptions = new HashMap<>();
        requestOptions.put( "Content-Type", "application/json" );
        requestOptions.put( "Accept", "application/json" );

        JSONObject subscription = new JSONObject();
        subscription.put( "renew", false );

        String res = "/v2/users/" + userId + "/subscriptions/" + subscriptionId + "?token=" + token;

        try
        {
            return getJsonClient().invoke( JsonClient.Method.delete, requestOptions, resource, null, subscription );
        }
        catch( JsonClient.JsonClientException ex )
        {
            logger.error( "INVOKE Error metod=>" + JsonClient.Method.delete.toString() + ", resource=>" + res + ", respose=>" + ex.getData() );
            throw ex;
        }
        catch( Exception e )
        {
            throw new RuntimeException( e );
        }
    }

    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( 0, 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() );
    }
    /*
     * Поиск договора по номеру телефона он же username
     */
    private int getContractId(String phone)
    {
        int findedContractId = -1;

        //поиск активных по периоду договоров с переданным номером телефона
        String query = "SELECT phoneItem.cid FROM contract_parameter_type_phone_item AS phoneItem "
                       + "LEFT JOIN contract AS c ON phoneItem.cid=c.id "
                       + "WHERE (c.date2 IS NULL OR c.date2>NOW()) AND phoneItem.pid=" + conf.paramPhoneId + " and phoneItem.phone='" + phone + "'";
        try (Statement st = context.getConnection().createStatement();
            ResultSet rs = st.executeQuery( query ))
        {
            List<Integer> cids = new ArrayList<>();
            while(rs.next())
            {
                cids.add( rs.getInt( 1 ));
            }

            if ( cids.size() > 1 )
            {
                logger.info( "phone:" + phone + " MULTI CONTRACT:" + Utils.toString( cids ));
            }

            if ( cids.size() == 1 )
            {
                findedContractId = cids.get( 0 );
            }

            return findedContractId;
        }
        catch(Exception ex)
        {
            logger.error( ex.getLocalizedMessage());
        }
        return -1;
    }
}