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.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import bitel.billing.server.contract.bean.ContractParameterManager;
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.access.om.AbstractOrderEvent;
import ru.bitel.bgbilling.modules.tv.access.om.AccountOrderEvent;
import ru.bitel.bgbilling.modules.tv.access.om.OrderManager;
import ru.bitel.bgbilling.modules.tv.access.om.ProductEntry;
import ru.bitel.bgbilling.modules.tv.access.om.ProductOrderEvent;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvDevice;
import ru.bitel.bgbilling.modules.tv.api.common.bean.TvDeviceType;
import ru.bitel.bgbilling.modules.tv.api.common.event.TvAccountModifiedEvent;
import ru.bitel.bgbilling.modules.tv.api.server.bean.TvAccountDao;
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.tv24h.Tv24hConf.SubscriptionMode;
import ru.bitel.bgbilling.modules.tv.dyn.TvDynUtils;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Utils;
import ru.bitel.oss.systems.inventory.product.common.bean.Product;
import ru.bitel.oss.systems.inventory.product.common.service.ProductService;

public class Tv24hOrderManager
	implements OrderManager
{
	private static final Logger logger = Logger.getLogger( Tv24hOrderManager.class );
	
	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( "https://api.24h.tv/" ), 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 dateTimeFormatter.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" );

		if( !resource.contains( "?" ) )
		{
			resource += "?token=" + token;
		}
		else
		{
			resource += "&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 )
        {
            logger.error( "INVOKE Error metod=>" + method.toString() + ",resource=>" + resource + ",req=>"
                          + obj.toString() + ", 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 e, final ServerContext ctx )
		throws Exception
	{
	    Object result = null;
	    
		logger.info( "accountModify" );

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

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

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

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

		String phone = null;
		if( conf.paramPhoneId > 0 )
		{
			PhoneParamValue phoneValue = contractParameterManager.getPhoneParam( e.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
			e.getEntry().setDeviceAccountId( userId );

			// обработка изменения аккаунта из биллинга
			if( e.getOldTvAccount() != null )
			{
				result = accountModify0( userId, e, email, phone );
			}
		}
		else if( e.getOldTvAccount() == null || Utils.isBlankString( userId ) )
		{
		    result = accountCreate0( e, email, phone );
		}
		
        if( !conf.agentMode && !Boolean.FALSE.equals( result ) )
        {
            ContractRuntime contractRuntime = ContractRuntimeMap.getInstance().getContractRuntime( ctx.getConnection(), e.getContractId() );
            BigDecimal balance = getBalance( ctx, contractRuntime );
            balanceUpdate( token, e.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() != 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;
            }
        }
        
        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 e, ServerContext ctx )
		throws Exception
	{
		logger.info( "productsModify" );

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

		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 = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();

        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 = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();

        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", dateTimeFormatter.format( subscriptionTimeFrom ) );
        subscription.put( "end_at", dateTimeFormatter.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" );
        }
    }

    /**
     * Включение 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 = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();

            invoke( JsonClient.Method.delete, "/v2/users/" + userId + "/subscriptions/" + tv24hSubscriptionId, null );
        }
        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 = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();
            
            final ZonedDateTime now = ZonedDateTime.now( ZoneId.of( "UTC" ) );
            
            JSONObject pause = new JSONObject();
            pause.put( "start_at", dateTimeFormatter.format( now ) );

            invoke( JsonClient.Method.post, "/v2/users/" + userId + "/subscriptions/" + tv24hSubscriptionId + "/pauses", pause );
        }
        else
        {
            logger.info( "deviceProductId is null. skip pause" );
        }
    }

    private void subscriptionDeactivate( final ProductOrderEvent e, final long tv24hSubscriptionId )
        throws IOException, BGException
    {
        logger.info( "subscriptionDeactivate" );
        
        String userId = e.getTvAccountRuntime().getTvAccount().getDeviceAccountId();

        invoke( JsonClient.Method.delete, "/v2/users/" + userId + "/subscriptions/" + tv24hSubscriptionId, null );
    }

    /**
     * Обновление баланса в 24h.
     * @param token
     * @param contractId ID договора
     * @param userId ID в 24h
     * @param balance баланс договора
     * @throws MalformedURLException
     * @throws Exception
     */
    public static void balanceUpdate( 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( "https://api.24h.tv/" ), 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 );
        }
    }
}
