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

import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

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 bitel.billing.server.admin.errorlog.AlarmSender;
import bitel.billing.server.admin.errorlog.bean.AlarmErrorMessage;
import bitel.billing.server.contract.bean.ContractParameterManager;
import ru.bitel.bgbilling.apps.tv.access.om.AbstractOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.AccountOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.ProductEntry;
import ru.bitel.bgbilling.apps.tv.access.om.ProductOrderEvent;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.contract.balance.server.bean.BalanceDao;
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.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDevice;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDeviceType;
import ru.bitel.bgbilling.modules.tv.common.event.TvAccountModifiedEvent;
import ru.bitel.bgbilling.modules.tv.common.om.OrderManager;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.JsonClientException;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.Method;
import ru.bitel.bgbilling.modules.tv.dyn.TvDynUtils;
import ru.bitel.bgbilling.modules.tv.dyn.tv24h.Tv24hConf.SubscriptionMode;
import ru.bitel.bgbilling.modules.tv.server.bean.TvAccountDao;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.oss.systems.inventory.product.common.bean.Product;
import ru.bitel.oss.systems.inventory.product.common.service.ProductService;

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

    // private static final String dateTimePattern =
    // private static final String dateTimePattern =
    // "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'";
    private static final String dateTimePattern2 = "yyyy-MM-dd'T'HH:mm:ss'Z'";

    // private final DateTimeFormatter dateTimeFormatter =
    // DateTimeFormatter.ofPattern( dateTimePattern ).withZone( ZoneId.of( "UTC"
    // ) );
    private final DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern( dateTimePattern2 ).withZone( ZoneId.of( "UTC" ) );

    private String token;

    private JsonClient jsonClient;

    private Tv24hConf conf;

    private int moduleId;
    private TvAccountDao tvAccountDao;

    @Override
    public Object init( ServerContext ctx, int moduleId, TvDevice tvDevice, TvDeviceType tvDeviceType, ParameterMap config )
        throws Exception
    {
        this.moduleId = moduleId;
        this.token = config.get( "om.tv24h.token", Utils.maskBlank( tvDevice.getSecret(), tvDevice.getPassword() ) );
        this.conf = ctx.getSetup().getConfig( moduleId, Tv24hConf.class );
        return null;
    }

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

    @Override
    public Object connect( ServerContext ctx )
        throws Exception
    {
        jsonClient = new JsonClient( new URL( conf.providerURL ), null, null );
        tvAccountDao = new TvAccountDao( ctx.getConnection(), moduleId );

        return null;
    }

    @Override
    public Object disconnect( ServerContext ctx )
        throws Exception
    {
        if ( jsonClient != null )
        {
            jsonClient.disconnect();
        }

        if ( tvAccountDao != null )
        {
            tvAccountDao.recycle();
            tvAccountDao = null;
        }

        return null;
    }

    private ZonedDateTime parseTime( String time )
    {
        try
        {
            return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( time, ZonedDateTime::from );

        }
        catch( DateTimeParseException ex )
        {
            try
            {
                return dateTimeFormatter2.parse( time, ZonedDateTime::from );
            }
            catch( DateTimeParseException ex2 )
            {
                try
                {
                    return dateTimeFormatter2.parse( time, ZonedDateTime::from );
                }
                catch( DateTimeParseException ex3 )
                {
                    throw ex3;
                }
            }
        }
    }

    private JSONObject invoke( Method method, String resource, Object obj )
        throws IOException, BGException, JSONException
    {
        Map<String, String> requestOptions = new HashMap<>();
        requestOptions.put( "Content-Type", "application/json" );
        requestOptions.put( "Accept", "application/json" );

        resource += !resource.contains( "?" ) ? "?token=" + token : "&token=" + token;

        try
        {
            return jsonClient.invoke( method, requestOptions, resource, null, obj );
        }
        catch( JsonClientException ex )
        {
            logger.error( "INVOKE Error metod=>" + method.toString() + ", resource=>" + resource + ", respose=>" + ex.getData() );
            throw ex;
        }
    }

    private JSONArray invokeAndGetArray( Method method, String resource, Object obj )
        throws IOException, BGException, JSONException
    {
        Map<String, String> requestOptions = new HashMap<>();
        requestOptions.put( "Content-Type", "application/json" );
        requestOptions.put( "Accept", "application/json" );

        if ( !resource.contains( "?" ) )
        {
            resource += "?token=" + token;
        }
        else
        {
            resource += "&token=" + token;
        }
        try
        {
            return jsonClient.invokeAndGetArray( method, requestOptions, resource, null, obj );
        }
        catch( JsonClientException ex )
        {
            String req= obj==null?"null":obj.toString();
            logger.error( "INVOKE Error metod=>" + method.toString() + ",resource=>" + resource + ",req=>"
                          + req + ", respose=>" + ex.getData() );
            throw ex;
        }
    }

    @Override
    public Object accountCreate( AccountOrderEvent e, ServerContext ctx )
        throws Exception
    {
        logger.info( "accountCreate" );

        accountModify( e, ctx );

        return null;
    }

    @Override
    public Object accountModify( final AccountOrderEvent accountOrderEvent, final ServerContext ctx )
        throws Exception
    {
        Object result = null;

        logger.info( "accountModify" );

        // id из 24h может быть прописан в поле Идентификатор
        String userId = accountOrderEvent.getNewTvAccount().getDeviceAccountId();
        if ( Utils.isBlankString( userId ) )
        {
            userId = accountOrderEvent.getTvAccountRuntime().getTvAccount().getIdentifier();
            if ( Utils.isBlankString( userId ) && accountOrderEvent.getOldTvAccount() != null )
            {
                userId = accountOrderEvent.getOldTvAccount().getDeviceAccountId();
            }
        }

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

        ContractParameterManager contractParameterManager = new ContractParameterManager( ctx.getConnection() );

        String email = null;
        if ( conf.paramEmailId > 0 )
        {
            email = TvDynUtils.getEmail( contractParameterManager, accountOrderEvent.getContractId(), conf.paramEmailId );
            logger.info( "email: " + email );
        }

        String phone = null;
        if ( conf.paramPhoneId > 0 )
        {
            PhoneParamValue phoneValue = contractParameterManager.getPhoneParam( accountOrderEvent.getContractId(), conf.paramPhoneId );
            if ( phoneValue != null && phoneValue.size() > 0 )
            {
                phone = phoneValue.getPhoneItem( 0 ).getPhone();
                logger.info( "phone: " + phone );
            }
        }

        contractParameterManager.close();

        if ( Utils.isBlankString( phone ) )
        {
            logger.info( "phone is empty string" );
        }

        if ( Utils.isBlankString( email ) )
        {
            logger.info( "email is empty string" );
        }

        // уже существует?
        if ( Utils.notBlankString( userId ) )
        {
            // обработка создания со стороны MW
            accountOrderEvent.getEntry().setDeviceAccountId( userId );

            // обработка изменения аккаунта из биллинга
            if ( accountOrderEvent.getOldTvAccount() != null )
            {
                result = accountModify0( userId, accountOrderEvent, email, phone );
            }
        }
        else if ( accountOrderEvent.getOldTvAccount() == null || Utils.isBlankString( userId ) )
        {
            result = accountCreate0( accountOrderEvent, email, phone );
        }

        if ( !conf.agentMode && !Boolean.FALSE.equals( result ) )
        {
            ContractRuntime contractRuntime = ContractRuntimeMap.getInstance().getContractRuntime( ctx.getConnection(), accountOrderEvent.getContractId() );
            BigDecimal balance = getBalance( ctx, contractRuntime );
            balanceUpdate( conf.providerURL, token, accountOrderEvent.getContractId(), userId, balance );
        }

        return null;
    }

    /**
     * Создание (или обновление, если пользователь с таким телефоном уже есть).
     * 
     * @param e
     * @param email
     * @param phone
     * @return
     * @throws IOException
     * @throws BGException
     */
    private Object accountCreate0( final AccountOrderEvent e, final String email, final String phone )
        throws IOException, BGException
    {
        try
        {
            accountCreate1( e, email, phone );
        }
        catch( JsonClientException ex )
        {
            if ( ex.getResponseCode() != 400 )
            {
                throw ex;
            }

            logger.info( "Found error" );

            try
            {
                JSONObject message = new JSONObject( ex.getData() );

                if ( message.get( "detail" ).toString().contains( "User with this username already exists." ) )
                {
                    logger.info( "Skip username already exists error" );

                    // TODO: обработка
                    return false;
                }
                else
                {
                    throw ex;
                }
            }
            catch( Exception ex2 )
            {
                logger.info( ex2.getMessage(), ex2 );
                throw ex;
            }
        }
        catch( IllegalStateException ex )
        {
            logger.info( "Skip username already exists error" );

            // TODO: обработка
            return false;
        }

        return null;
    }

    private void accountCreate1( final AccountOrderEvent e, final String email, final String phone )
        throws IOException, BGException
    {
        logger.info( "create account" );

        JSONObject user = new JSONObject();
        user.put( "username", e.getNewTvAccount().getLogin() );
        user.put( "password", e.getNewTvAccount().getPassword() );
        user.put( "provider_uid", "bgb" + String.valueOf( e.getTvAccountId() ) );
        user.put( "is_provider_free", conf.providerFree );

        if ( Utils.notBlankString( phone ) )
        {
            user.put( "phone", phone );
        }

        if ( Utils.notBlankString( email ) )
        {
            user.put( "email", email );
        }

        String userId = null;

        try
        {
            user = invoke( Method.post, "/v2/users", user );
            userId = String.valueOf( user.getInt( "id" ) );
        }
        catch( JsonClientException ex )
        {
            if ( ex.getResponseCode() == 400 )
            {
                try
                {
                    JSONObject message = new JSONObject( ex.getData() );

                    // номер телефона уже заведен - по нему можем искать
                    if ( message.get( "detail" ).toString().contains( "User with this phone already exists." ) )
                    {
                        JSONArray users = invokeAndGetArray( Method.get, "/v2/users?phone=" + phone, null );
                        for ( int i = 0, size = users.length(); i < size; i++ )
                        {
                            JSONObject existingUser = users.getJSONObject( 0 );
                            userId = String.valueOf( existingUser.getInt( "id" ) );

                            logger.info( "modify already existing account" );

                            user = invoke( Method.patch, "/v2/users/" + userId, user );

                            break;
                        }
                    }
                    else
                    {
                        throw ex;
                    }
                }
                catch( Exception ex2 )
                {
                    logger.info( ex2.getMessage(), ex2 );
                    throw ex;
                }
            }
        }

        if ( userId == null )
        {
            throw new IllegalStateException( "userId is null" );
        }

        e.getEntry().setDeviceAccountId( userId );

        try
        {
            TvAccount newTvAccount = tvAccountDao.get( e.getNewTvAccount().getId() );
            if ( newTvAccount != null )
            {
                TvAccount oldTvAccount = newTvAccount.clone();

                newTvAccount.setIdentifier( userId );
                tvAccountDao.update( newTvAccount );

                EventProcessor.getInstance().publish( new TvAccountModifiedEvent( moduleId, e.getContractId(), 0, oldTvAccount, newTvAccount ) );
            }
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }
    }

    private Object accountModify0( String userId, AccountOrderEvent e, String email, String phone )
        throws IOException, BGException
    {
        try
        {
            accountModify1( userId, e, email, phone );
        }
        catch( JsonClientException ex )
        {
            if (  ex.getResponseCode()  / 100 != 4  )
            {
                throw ex;
            }

            logger.info( "Found error" );

            try
            {
                JSONObject message = new JSONObject( ex.getData() );
                logger.info( "message ok" );
                String errorMsg = message.get( "detail" ).toString();
                logger.info( "errorMsg"+errorMsg );
                if ( errorMsg.contains( "User with this username already exists." ) )
                {
                    logger.info( "Skip username already exists error" );

                    // TODO: обработка
                    return false;
                }
                else if ( errorMsg.contains( "Не найдено." ) )
                {
                    logger.info( "Skip username user not exists." );
                    String text = "Ошибка Tv24hOrderManager пользователь не заведен в админке  [" + userId + "]";
                    AlarmSender.sendAlarm( new AlarmErrorMessage( "Tv24hOrderManager.userNotFound",
                                                                  text,
                                                                  text ), System.currentTimeMillis() );
                    return false;
                }
                else
                {
                    throw ex;
                }
            }
            catch( Exception ex2 )
            {
                
                logger.info( ex2.getMessage(), ex2 );
                throw ex;
            }
        }

        return null;
    }

    private void accountModify1( String userId, AccountOrderEvent e, String email, String phone )
        throws IOException, BGException
    {
        logger.info( "modify account" );

        JSONObject user = new JSONObject();

        if ( !e.getOldTvAccount().getLogin().equals( e.getNewTvAccount().getLogin() ) )
        {
            user.put( "username", e.getNewTvAccount().getLogin() );
        }

        if ( !e.getOldTvAccount().getPassword().equals( e.getNewTvAccount().getPassword() ) )
        {
            user.put( "password", e.getNewTvAccount().getPassword() );
        }

        if ( Utils.notBlankString( phone ) )
        {
            user.put( "phone", phone );
        }

        if ( Utils.notBlankString( email ) )
        {
            user.put( "email", email );
        }

        user.put( "provider_uid", "bgb" + String.valueOf( e.getTvAccountId() ) );

        if ( user.length() > 0 )
        {
            user.put( "is_provider_free", conf.providerFree );

            user = invoke( Method.patch, "/v2/users/" + userId, user );
        }
        else
        {
            logger.info( "No changes included" );
        }
    }

    @Override
    public Object accountRemove( AccountOrderEvent e, ServerContext ctx )
        throws Exception
    {
        logger.info( "accountRemove" );

        return null;
    }

    @Override
    public Object accountStateModify( AccountOrderEvent e, ServerContext ctx )
        throws Exception
    {
        logger.info( "accountStateModify" );

        return null;
    }

    @Override
    public Object accountOptionsModify( AbstractOrderEvent e, ServerContext ctx )
        throws Exception
    {
        logger.info( "accountOptionsModify" );

        return null;
    }

    @Override
    public Object productsModify( ProductOrderEvent productOrderEvent, ServerContext serverContext )
        throws Exception
    {
        logger.info( "productsModify" );

        for ( ProductEntry productEntry : productOrderEvent.getProductEntryList() )
        {
            logger.info( productEntry.getOldProduct() + " " + productEntry.getOldState() );
            logger.info( productEntry.getNewProduct() + " " + productEntry.getNewState() );

            if ( conf.agentMode )
            {
                if ( productEntry.getNewState() == Product.STATE_DISABLED )
                {
                    Product product = serverContext.getService( ProductService.class, 0 ) .productGet( productOrderEvent.getContractId(), productEntry.getNewProduct().getId() );
                    // деактивировали вручную из биллинга
                    if ( product.getDeactivationTime() != null )
                    {
                        subscriptionDeactivate( productOrderEvent, product.getDeviceProductId() );
                    }
                }
                // добавление нового пакета из биллинга
                else if ( productEntry.getNewState() == Product.STATE_ENABLED && productEntry.getOldState() == Product.STATE_REMOVED )
                {
                    subscriptionActivateAgent( productOrderEvent, productEntry );
                }
            }
            else
            {
                if ( productEntry.getNewState() == Product.STATE_DISABLED || productEntry.getNewState() == Product.STATE_REMOVED )
                {
                    logger.info( "conf.subscriptionMode !!!!!!!! " + conf.subscriptionMode );
                    if ( conf.subscriptionMode == SubscriptionMode.renew && productEntry.getNewState() != Product.STATE_REMOVED
                         && (productEntry.getNewProduct().getTimeTo() == null
                             || productEntry.getNewProduct().getTimeTo().after( new Date() )) )
                    {
                        Product product = productEntry.getNewProduct();
                        subscriptionPause( productOrderEvent, product.getDeviceProductId() );
                    }
                    else
                    {
                        Product product = productEntry.getOldProduct();
                        subscriptionDeactivate( productOrderEvent, product.getDeviceProductId() );
                        logger.info( "subscriptionDeactivate !!!!!!!!!!!!" );
                    }
                }
                else
                {
                    subscriptionActivate( productOrderEvent, productEntry );
                }
            }
        }

        return null;
    }

    /**
     * В агентском режиме просто активируем и указываем renew=true, 24h сам
     * списывает, продлевает подписку и оповещает об изменениях через webhook.
     * 
     * @param e
     * @param pe
     * @throws IOException
     * @throws BGException
     * @throws JsonClientException
     */
    private void subscriptionActivateAgent( final ProductOrderEvent e, final ProductEntry pe )
        throws IOException,
        BGException,
        JsonClientException
    {
        String productIdentifier = pe.getProductSpec().getIdentifier();

        if ( Utils.isBlankString( productIdentifier ) )
        {
            logger.warn( "Can't activate productSpec:" + pe.getProductSpec().getId() + " with empty identifier!" );
            return;
        }

        JSONObject subscription = new JSONObject();
        subscription.put( "packet_id", Utils.parseInt( productIdentifier ) );
        subscription.put( "renew", true );

        String userId = getUserId( e );
        if(userId == null)
        {
            //нету идентификатора, это ошибка
            return;
        }

        try
        {
            JSONArray subscriptions = invokeAndGetArray( JsonClient.Method.post, "/v2/users/" + userId
                                                                                 + "/subscriptions", new JSONArray( Collections.singleton( subscription ) ) );

            subscription = subscriptions.getJSONObject( 0 );

            logger.info( "subscription id = " + subscription.get( "id" ) );

            pe.getNewProduct().setDeviceProductId( String.valueOf( subscription.get( "id" ) ) );
        }
        catch( JsonClientException ex )
        {
            try
            {
                JSONObject message = new JSONObject( ex.getData() );

                if ( message.get( "detail" ).toString().contains( "You already subscribe on this packet." ) )
                {
                    logger.warn( "You already subscribe on this packet" );
                }
                else if ( message.get( "detail" ).toString().contains( "You Need billing account for subscription." ) )
                {
                    logger.warn( "You Need billing account for subscription" );
                }
                else
                {
                    throw ex;
                }
            }
            catch( Exception ex2 )
            {
                logger.info( ex2.getMessage(), ex2 );
                throw ex;
            }
        }
    }

    private void subscriptionActivate( final ProductOrderEvent e, final ProductEntry pe )
        throws IOException,
        BGException,
        JsonClientException,
        JSONException,
        ParseException
    {
        try
        {
            subscriptionActivate0( e, pe );
        }
        catch( JsonClientException ex )
        {
            try
            {
                JSONObject message = new JSONObject( ex.getData() );

                if ( message.get( "detail" ).toString().contains( "You already subscribe on this packet." ) )
                {
                    logger.warn( "You already subscribe on this packet" );
                }
                else if ( message.get( "detail" ).toString().contains( "You Need billing account for subscription." ) )
                {
                    logger.warn( "You Need billing account for subscription" );
                }
                else
                {
                    throw ex;
                }
            }
            catch( Exception ex2 )
            {
                logger.info( ex2.getMessage(), ex2 );
                throw ex;
            }
        }
    }

    /**
     * В неагентском режиме всегда сами продлеваем периоды подписки.
     * 
     * @param e
     * @param pe
     * @throws IOException
     * @throws BGException
     * @throws JsonClientException
     * @throws JSONException
     * @throws ParseException
     */
    private void subscriptionActivate0( final ProductOrderEvent e, final ProductEntry pe )
        throws IOException,
        BGException,
        JsonClientException,
        JSONException,
        ParseException
    {
        String productIdentifier = pe.getProductSpec().getIdentifier();

        if ( Utils.isBlankString( productIdentifier ) )
        {
            logger.warn( "Can't activate productSpec:" + pe.getProductSpec().getId() + " with empty identifier!" );
            return;
        }

        String userId = getUserId( e );
        if(userId == null)
        {
            //нету идентификатора, это ошибка
            return;
        }

        int packetId = Utils.parseInt( productIdentifier );

        final ZonedDateTime now = ZonedDateTime.now( ZoneId.of( "UTC" ) );

        // текущая подписка
        JSONObject currentSubscription = null;
        // время окончания последней подписки на MW
        ZonedDateTime lastSubscriptionEnd = null;

        // текущие подписки
        JSONArray subscriptions = invokeAndGetArray( JsonClient.Method.get, "/v2/users/" + userId
                                                                            + "/subscriptions?packet_ids="
                                                                            + packetId, null );
        // ищем время окончания текущей подписки
        for ( int i = 0, size = subscriptions.length(); i < size; i++ )
        {
            final JSONObject subscription = subscriptions.getJSONObject( i );
            final JSONObject packet = subscription.getJSONObject( "packet" );

            if ( packet.getInt( "id" ) != packetId )
            {
                continue;
            }

            boolean renew = subscription.getBoolean( "renew" ); // на пакете
                                                                // включено
                                                                // автопродление?

            if ( renew ) // должна быть только одна подписка с renew==true
            {
                currentSubscription = subscription;
                break;
            }
            else
            {
                ZonedDateTime endAt = parseTime( subscription.getString( "end_at" ) );
                if ( lastSubscriptionEnd == null || endAt.isAfter( lastSubscriptionEnd ) )
                {
                    lastSubscriptionEnd = endAt;

                    if ( now.isBefore( endAt ) )
                    {
                        currentSubscription = subscription;
                    }
                }
            }
        }

        if ( currentSubscription != null )
        {
            logger.info( "Found active subscription " + String.valueOf( currentSubscription.get( "id" ) ) );
        }

        // в режиме renew при необходимости отключить - ставим на паузу
        // при необходимости включить - пытаемся снять с паузы, если не находим
        // подписку - создаем новую
        if ( conf.subscriptionMode == SubscriptionMode.renew && currentSubscription != null )
        {
            final String tv24hSubscriptionId = String.valueOf( currentSubscription.get( "id" ) );

            if ( !currentSubscription.getBoolean( "renew" ) ) // необходимо
                                                              // переключить на
                                                              // renew, мы будем
                                                              // оперировать
                                                              // паузами
            {
                currentSubscription = enableRenew( userId, currentSubscription, tv24hSubscriptionId );
            }

            if ( currentSubscription != null )
            {
                if ( currentSubscription.getBoolean( "is_paused" ) ) // если
                                                                     // подписка
                                                                     // на паузе
                                                                     // - нам
                                                                     // надо ее
                                                                     // включить
                {
                    JSONObject currentPause = findCurrentPause( currentSubscription );

                    if ( currentPause != null ) // отменяем паузу
                    {
                        invoke( JsonClient.Method.delete, "/v2/users/" + userId + "/subscriptions/"
                                                          + tv24hSubscriptionId + "/pauses/"
                                                          + String.valueOf( currentPause.get( "id" ) ), null );

                        pe.getNewProduct().setDeviceProductId( String.valueOf( currentSubscription.get( "id" ) ) );
                        return;
                    }
                    else // если вдруг паузу не нашли - отменяем подписку,
                         // начнем новую
                    {
                        subscriptionDeactivate( e, tv24hSubscriptionId );
                        lastSubscriptionEnd = null;
                    }
                }
                else
                {
                    logger.info( "Subscription already active" );

                    pe.getNewProduct().setDeviceProductId( String.valueOf( currentSubscription.get( "id" ) ) );
                    return;
                }
            }
        }

        ZonedDateTime subscriptionTimeFrom = ZonedDateTime.ofInstant( pe.getNewProduct()
                                                                        .getSubscriptionTimeFrom()
                                                                        .toInstant(), ZoneId.of( "UTC" ) );
        ZonedDateTime subscriptionTimeTo = ZonedDateTime.ofInstant( pe.getNewProduct()
                                                                      .getSubscriptionTimeTo()
                                                                      .toInstant(), ZoneId.of( "UTC" ) );

        if ( lastSubscriptionEnd != null ) // устанавливаем время начала новой
                                           // подписки не раньше чем время
                                           // окончания предыдущей
        {
            lastSubscriptionEnd = lastSubscriptionEnd.withNano( 0 ).plusSeconds( 1 );
            if ( lastSubscriptionEnd.isAfter( subscriptionTimeFrom ) )
            {
                subscriptionTimeFrom = lastSubscriptionEnd;
            }
        }

        subscriptionTimeTo.plusHours( 3 );

        JSONObject subscription = new JSONObject();
        subscription.put( "packet_id", Utils.parseInt( productIdentifier ) );
        subscription.put( "renew", conf.subscriptionMode == SubscriptionMode.renew ); // в
                                                                                      // режиме
                                                                                      // renew
                                                                                      // подписка
                                                                                      // продлится
                                                                                      // сама
        subscription.put( "start_at", dateTimeFormatter2.format( subscriptionTimeFrom ) );
        subscription.put( "end_at", dateTimeFormatter2.format( subscriptionTimeTo ) );
        try
        {
            // создаем новую подписку
            subscriptions = invokeAndGetArray( JsonClient.Method.post, "/v2/users/" + userId
                                                                       + "/subscriptions", new JSONArray( Collections.singleton( subscription ) ) );
            subscription = subscriptions.getJSONObject( 0 );

            logger.info( "subscription id = " + subscription.get( "id" ) );

            pe.getNewProduct().setDeviceProductId( String.valueOf( subscription.get( "id" ) ) );
        }
        catch( Exception ex )
        {
            logger.error( "ERROR BUT SKIP" );
        }
    }

    private String getUserId( final ProductOrderEvent e )
    {
        String userId = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();
        if ( Utils.isEmptyString( userId ) )
        {
            TvAccount acc = e.getTvAccountRuntime().getTvAccount();
            String eMsg = "Ошибка Tv24hOrderManager не заполнен идентификатор id=> " + acc.getId() + ", login=> "
                          + acc.getLogin();
            logger.error( "ERROR=>"+eMsg );
            AlarmSender.sendAlarm( new AlarmErrorMessage( "Tv24hOrderManager.userId",
                                                          "Ошибка Tv24hOrderManager не заполнен идентификатор ["
                                                                                      + acc.getId() + "]",
                                                          eMsg ), System.currentTimeMillis() );
        }
        return userId;
    }

    /**
     * Включение renew для указанной подписки. Если включение renew не удалось -
     * возвращает null. Иначе - currentSubscription.
     * 
     * @param userId
     * @param currentSubscription
     * @param tv24hSubscriptionId
     * @return
     * @throws IOException
     * @throws BGException
     * @throws JsonClientException
     */
    private JSONObject enableRenew( String userId, JSONObject currentSubscription, final String tv24hSubscriptionId )
        throws IOException,
        BGException,
        JsonClientException
    {
        JSONArray subscriptions;
        JSONObject subscription = new JSONObject();
        subscription.put( "renew", true );

        try
        {
            // обновляем на renew
            subscriptions = invokeAndGetArray( JsonClient.Method.patch, "/v2/users/" + userId + "/subscriptions/"
                                                                        + tv24hSubscriptionId, subscription );
            subscription = subscriptions.getJSONObject( 0 );
        }
        catch( JsonClientException ex )
        {
            if ( ex.getResponseCode() != 400 )
            {
                throw ex;
            }

            logger.info( "Found error" );

            try
            {
                JSONObject message = new JSONObject( ex.getData() );

                if ( message.get( "detail" ).toString().contains( "Недоступный метод для текущей подписки." ) )
                {
                    logger.info( "Already closed subsciption. Possibly time not synchronized" );

                    currentSubscription = null;
                }
                else
                {
                    throw ex;
                }
            }
            catch( Exception ex2 )
            {
                logger.info( ex2.getMessage(), ex2 );
                throw ex;
            }
        }
        return currentSubscription;
    }

    private JSONObject findCurrentPause( JSONObject currentSubscription )
    {
        JSONObject result = null;

        JSONArray pauses = currentSubscription.getJSONArray( "pauses" );

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

            if ( pause.optString( "end_at" ) == null ) // ищем паузу с
                                                       // бесконечным периодом
            {
                result = pause;
            }
        }

        return result;
    }

    private void subscriptionDeactivate( final ProductOrderEvent e, final String deviceProductId )
        throws IOException, BGException
    {
        logger.info( "subscriptionDeactivate" );

        if ( Utils.notBlankString( deviceProductId ) )
        {
            long tv24hSubscriptionId = Utils.parseLong( deviceProductId.split( "-" )[0] );
            String userId = getUserId( e );
            if(userId == null)
            {
                //нету идентификатора, это ошибка
                return;
            }
            try 
            {
                invoke( JsonClient.Method.delete, "/v2/users/" + userId + "/subscriptions/" + tv24hSubscriptionId, null );
            }
            catch( JsonClientException ex )
            {
                if ( ex.getResponseCode() / 100 != 4 )
                {
                    throw ex;
                }

                logger.info( "Found error" );

                try
                {
                    JSONObject message = new JSONObject( ex.getData() );
                    logger.info( "message ok" );
                    String errorMsg = message.get( "detail" ).toString();
                    logger.info( "errorMsg" + errorMsg );
                    if ( errorMsg.contains( "Не найдено" ) )
                    {
                        logger.info( "Skip user not exists." );
                        String text = "Ошибка Tv24hOrderManager подписки не в админке [" + userId + " subsxcription=>" + tv24hSubscriptionId + "] ";
                        AlarmSender.sendAlarm( new AlarmErrorMessage( "Tv24hOrderManager.subscriptionsNotFound", text, text ), System.currentTimeMillis() );
                    }
                    else
                    {
                        throw ex;
                    }
                }
                catch( Exception ex2 )
                {
                    logger.info( ex2.getMessage(), ex2 );
                    throw ex;
                }
            }
        }
        else
        {
            logger.info( "deviceProductId is null. skip deactivation" );
        }
    }

    private void subscriptionPause( final ProductOrderEvent e, final String deviceProductId )
        throws IOException,
        BGException
    {
        logger.info( "subscriptionPause" );

        if ( Utils.notBlankString( deviceProductId ) )
        {
            long tv24hSubscriptionId = Utils.parseLong( deviceProductId.split( "-" )[0] );
            String userId = getUserId( e );
            if(userId == null)
            {
                //нету идентификатора, это ошибка
                return;
            }

            final ZonedDateTime now = ZonedDateTime.now( ZoneId.of( "UTC" ) );

            JSONObject pause = new JSONObject();
            pause.put( "start_at", dateTimeFormatter2.format( now ) );

            try
            {
                invoke( JsonClient.Method.post, "/v2/users/" + userId + "/subscriptions/" + tv24hSubscriptionId + "/pauses", pause );
            }
            catch( JsonClientException ex )
            {
                String data = ex.getData();
                if ( data != null )
                {
                    JSONObject dataJson = null;
                    try
                    {
                        dataJson = new JSONObject( data );
                    }
                    catch( Exception ex2 )
                    {
                        throw ex;
                    }
                    int statusCode = dataJson.optInt( "status_code" );
                    if ( statusCode != 404 )
                    {
                        throw ex;
                    }
                    logger.info( "Subscriptions " + tv24hSubscriptionId + " not found. Skip..." );                 
                }
            }
        }
        else
        {
            logger.info( "deviceProductId is null. skip pause" );
        }
    }

    /**
     * Обновление баланса в 24h.
     * 
     * @param token
     * @param contractId
     *            ID договора
     * @param userId
     *            ID в 24h
     * @param balance
     *            баланс договора
     * @throws MalformedURLException
     * @throws Exception
     */
    public static void balanceUpdate( String providerURL, String token, int contractId, String userId, BigDecimal balance )
        throws MalformedURLException,
        Exception
    {
        JSONObject req = new JSONObject();
        req.put( "id", contractId );
        req.put( "amount", Utils.formatCost( balance ) );

        JsonClient jsonClient = new JsonClient( new URL( providerURL ), null, null );

        Map<String, String> requestOptions = new HashMap<>();
        requestOptions.put( "Content-Type", "application/json" );
        requestOptions.put( "Accept", "application/json" );

        try
        {
            jsonClient.invoke( JsonClient.Method.post, requestOptions, "/v2/users/" + userId
                                                                       + "/provider/account?token="
                                                                       + token, null, req );
        }
        catch( JsonClientException ex )
        {
            logger.error( "INVOKE Error metod=>post,resource=>" + "/v2/users/" + userId + "/provider/account?token="
                          + token + ",res=>" + req.toString() + ", respose=>" + ex.getData() );
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }

        jsonClient.disconnect();
    }

    /**
     * Получение текущего баланса договора.
     * 
     * @param context
     * @param contractRuntime
     * @return
     * @throws BGException
     */
    static BigDecimal getBalance( final ServerContext context, final ContractRuntime contractRuntime )
        throws BGException
    {
        final LocalDate date = LocalDate.now();

        try (BalanceDao balanceDao = new BalanceDao( context.getConnection() ))
        {
            BigDecimal balance;

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

            return balance;
        }
        catch( Exception ex )
        {
            throw new BGException( ex );
        }
    }
}
