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

import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

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.contract.bean.Contract;
import bitel.billing.server.contract.bean.ContractManager;
import ru.bitel.bgbilling.apps.tv.access.om.AbstractOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.AccountOrderEvent;
import ru.bitel.bgbilling.apps.tv.access.om.ProductOrderEvent;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.contract.api.server.bean.ContractDao;
import ru.bitel.bgbilling.kernel.module.common.bean.User;
import ru.bitel.bgbilling.modules.tv.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDevice;
import ru.bitel.bgbilling.modules.tv.common.bean.TvDeviceType;
import ru.bitel.bgbilling.modules.tv.common.om.OrderManager;
import ru.bitel.bgbilling.modules.tv.common.om.OrderManagerAdapter;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.JsonClientException;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.Method;
import ru.bitel.bgbilling.modules.tv.dyn.JsonClient.Request;
import ru.bitel.bgbilling.modules.tv.dyn.TvDynUtils;
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.ProductSpec;
import ru.bitel.oss.systems.inventory.service.common.bean.ServiceSpec;

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

    private String account;
    private String password;

    private JsonClient jsonClient;

    private ContractDao contractDao;
    private ContractManager contractManager;

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

    /**
     * Если true - синхронизация на уровне сервисов, а не продуктов.
     */
    private boolean serviceMode;

    private int customerFirstNamePid;
    private int customerMiddleNamePid;
    private int customerLastNamePid;

    private int moduleId;
    private TvAccountDao tvAccountDao;
    
    private String loginPrefix;

    /**
     * Форматирование для login.
     */
    private String loginFormat;

    private Set<Long> allTariffs;

    @Override
    public Object init( ServerContext ctx, int moduleId, TvDevice tvDevice, TvDeviceType tvDeviceType, ParameterMap config )
        throws Exception
    {
        logger.info( "init" );

        super.init( ctx, moduleId, tvDevice, tvDeviceType, config );

        this.account = config.get( "om.login", config.get( "moovi.api.login", tvDevice.getUsername() ) );
        this.password = config.get( "om.password", config.get( "moovi.api.password", tvDevice.getPassword() ) );

        this.jsonClient = new JsonClient( new URL( "https://api.billing.moovi-iptv.ru/" ), null, null );
        this.jsonClient.setBasicAuth( false );

        this.productSyncMode = config.getInt( "om.product.syncMode", 1 ) > 0;
        this.serviceMode = config.getInt( "om.product.serviceMode", 0 ) > 0;

        int customerNamePid = config.getInt( "customer.name.pid", 0 );
        customerLastNamePid = config.getInt( "customer.lastName.pid", customerNamePid );
        customerFirstNamePid = config.getInt( "customer.firstName.pid", 0 );
        customerMiddleNamePid = config.getInt( "customer.middleName.pid", 0 );
        
        loginPrefix = Utils.maskBlank( config.get( "account.login.prefix", "" ), "" );

        String loginFormat = config.get( "account.login.format", config.get( "om.account.loginFormat", null ) );
        if( Utils.notBlankString( loginFormat ) )
        {
            this.loginFormat = loginFormat;
        }
        else
        {
            this.loginFormat = null;
        }

        return null;
    }

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

    @Override
    public Object connect( ServerContext ctx )
        throws Exception
    {
        contractDao = new ContractDao( ctx.getConnection(), User.USER_SERVER );
        contractManager = new ContractManager( ctx.getConnection() );

        tvAccountDao = new TvAccountDao( ctx.getConnection(), moduleId );

        JSONObject credentials = new JSONObject();
        credentials.put( "account", this.account );
        credentials.put( "password", this.password );

        JSONObject result = request( Method.post, "getTariffs" )
            .setBody( credentials )
            .execute()
            .getJsonObject();

        this.allTariffs = Optional.ofNullable( result.optJSONArray( "tariffs" ) )
            .map( array -> StreamSupport.stream( array.spliterator(), false ) )
            .orElse( Stream.empty() )
            .map( tariff -> Utils.parseLong( tariff.toString() ) )
            .collect( Collectors.toSet() );
        
        logger.info( "All tariffs: " + this.allTariffs );

        return null;
    }

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

        if( jsonClient != null )
        {
            jsonClient.disconnect();
        }

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

        return null;
    }

    private Request request( Method method, String resource )
        throws IOException, BGException, JSONException
    {
        return jsonClient.newRequest()
            .setMethod( method )
            .setResource( resource )
            .setHeader( "Content-Type", "application/json" )
            .setHeader( "Accept", "*/*" );
    }

    public static long[] parseMooviAccountId( TvAccount tvAccount )
    {
        if( tvAccount == null )
        {
            return null;
        }

        String deviceAccountId = tvAccount.getDeviceAccountId();
        if( Utils.isBlankString( deviceAccountId ) )
        {
            return null;
        }

        String[] params = deviceAccountId.split( "-" );
        if( params.length < 2 )
        {
            return null;
        }

        long userId = Utils.parseInt( params[0] );
        long userAccountId = Utils.parseInt( params[1] );

        if( userId <= 0 || userAccountId <= 0 )
        {
            return null;
        }

        return new long[] { userId, userAccountId };
    }

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

        return accountModify( e, ctx );
    }

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

        final long mooviUserId = accountModify0( e, ctx );

        try
        {
            // синхронизируем все продукты
            productsModifySyncFull( e, ctx, mooviUserId, e.getNewState() == TvAccount.STATE_ENABLE );
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
        }

        return null;
    }

    /**
     * Редактирование и создание аккаунта.
     * 
     * array{
     * account: string, // , обязательный логин учетной записи для апи
     * password: string, // , обязательный пароль учетной записи для апи
     * group: int, // . id . необязательный установить группу пользователя При создании
     * нового если непередан устанавливается группа по умолчанию , .
     * fio: string, // . обязательный Установить фио абонента
     * contract: string, // . / обязательный Установить изменить номер до говора абонента
     * agent_contract_id: int, // . необязательный Установить а гентс кий до говор для
     * пользователя
     * activity: boolean, // . (1- , 0 необязательный Установить стат ус абонента ак тивен -
     * услу ги заблокированы ). «1». по умолчанию
     * is_service: boolean, // . / (1- необязательный Установить изменить стат ус абонента
     * служебный обычный по умолчанию , 0- ). «0».
     * is_business: boolean, // . (1- , 0- ). необязательный стат ус абонента юрлицо физлицо по
     * умолчанию «0».
     * ext_id: string, // . ID, ID необязательный внешний если требуется сохранить привяз к у к
     * внешней системы .
     * }
     * 
     * @param e
     * @param ctx
     * @return userId
     * @throws Exception
     */
    private long accountModify0( final AccountOrderEvent e, final ServerContext ctx )
        throws Exception
    {
        logger.info( "accountModify0" );

        TvAccount tvAccount = e.getNewTvAccount() != null ? e.getNewTvAccount() : e.getOldTvAccount();

        long[] mooviAccountId = parseMooviAccountId( e.getOldTvAccount() );

        // создание аккаунта
        if( e.getOldTvAccount() == null || mooviAccountId == null )
        {
            mooviAccountId = new long[2];

            final Contract contract = contractManager.getContractById( e.getContractId() );

            final String[] name = TvDynUtils.getName( contractDao, contract, customerLastNamePid, customerFirstNamePid, customerMiddleNamePid );

            final String login = loginPrefix + TvDynUtils.getLogin( tvAccount, loginFormat );

            JSONObject mooviUser = new JSONObject();
            mooviUser.put( "account", this.account );
            mooviUser.put( "password", this.password );

            mooviUser.put( "fio", name[0] );
            mooviUser.put( "contract", contract.getTitle() );
            mooviUser.put( "activity", e.getNewState() == TvAccount.STATE_ENABLE ? 1 : 0 );

            //mooviUser.put( "ext_id", String.valueOf( tvAccount.getId() ) );

            JSONObject result = request( Method.post, "addUser" )
                .setBody( mooviUser )
                .execute()
                .getJsonObject();

            mooviAccountId[0] = result.getLong( "id" );

            JSONObject mooviUserAccount = new JSONObject();
            mooviUserAccount.put( "account", this.account );
            mooviUserAccount.put( "password", this.password );

            mooviUserAccount.put( "user_id", mooviAccountId[0] );
            mooviUserAccount.put( "count", 5 );
            mooviUserAccount.put( "login", login );
            mooviUserAccount.put( "pin", tvAccount.getPassword() );

            result = request( Method.post, "addUserAccount" )
                .setBody( mooviUserAccount )
                .execute()
                .getJsonObject();

            mooviAccountId[1] = result.getLong( "id" );

            e.getEntry().setDeviceAccountId( String.valueOf( mooviAccountId[0] ) + "-" + String.valueOf( mooviAccountId[1] ) );

            accountStateModify0( e, mooviAccountId[0] );
        }
        else // обновление аккаунта
        {
            final Contract contract = contractManager.getContractById( e.getContractId() );

            final String[] name = TvDynUtils.getName( contractDao, contract, customerLastNamePid, customerFirstNamePid, customerMiddleNamePid );

            final String login = loginPrefix + TvDynUtils.getLogin( tvAccount, loginFormat );

            JSONObject mooviUser = new JSONObject();
            mooviUser.put( "account", this.account );
            mooviUser.put( "password", this.password );

            mooviUser.put( "id", mooviAccountId[0] );
            mooviUser.put( "fio", name[0] );
            mooviUser.put( "contract", contract.getTitle() );
            mooviUser.put( "activity", e.getNewState() == TvAccount.STATE_ENABLE ? 1 : 0 );

            //mooviUser.put( "ext_id", String.valueOf( tvAccount.getId() ) );

            JSONObject result = request( Method.post, "changeUser" )
                .setBody( mooviUser )
                .execute()
                .getJsonObject();

            mooviAccountId[0] = result.getLong( "id" );

            JSONObject mooviUserAccount = new JSONObject();
            mooviUserAccount.put( "account", this.account );
            mooviUserAccount.put( "password", this.password );

            mooviUserAccount.put( "user_id", mooviAccountId[0] );
            mooviUserAccount.put( "id", mooviAccountId[1] );
            mooviUserAccount.put( "count", 5 );
            mooviUserAccount.put( "login", login );
            mooviUserAccount.put( "pin", tvAccount.getPassword() );

            result = request( Method.post, "changeUserAccount" )
                .setBody( mooviUserAccount )
                .execute()
                .getJsonObject();

            mooviAccountId[1] = result.getLong( "id" );

            e.getEntry().setDeviceAccountId( String.valueOf( mooviAccountId[0] ) + "-" + String.valueOf( mooviAccountId[1] ) );
        }

        return mooviAccountId[0];
    }

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

        final long[] mooviAccountId = parseMooviAccountId( e.getOldTvAccount() );

        if( mooviAccountId == null )
        {
            logger.warn( "deviceAccountId is empty for " + e.getOldTvAccount() );
            return null;
        }

        try
        {
            JSONObject mooviUser = new JSONObject();
            mooviUser.put( "account", this.account );
            mooviUser.put( "password", this.password );

            mooviUser.put( "id", mooviAccountId[0] );

            JSONObject result = request( Method.post, "deleteUser" )
                .setBody( mooviUser )
                .execute()
                .getJsonObject();

            if( logger.isDebugEnabled() )
            {
                logger.debug( result );
            }
        }
        catch( JsonClientException ex )
        {
            if( ex.getResponseCode() == 404 )
            {
                logger.info( "Error 404 - account already removed" );
                return null;
            }

            if( ex.getResponseCode() == 403 )
            {
                logger.info( "Error 403 - account already removed" );
                return null;
            }

            throw ex;
        }

        return null;
    }

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

        final long[] mooviAccountId = parseMooviAccountId( e.getOldTvAccount() );

        if( e.getOldTvAccount() == null || mooviAccountId == null )
        {
            return accountModify( e, ctx );
        }

        accountStateModify0( e, mooviAccountId[0] );

        // if( e.isOptionsModified() )
        {
            accountOptionsModify0( e, ctx, mooviAccountId[0], e.getNewState() == TvAccount.STATE_ENABLE );
        }

        return null;
    }

    private void accountStateModify0( final AccountOrderEvent e, final long mooviUserId )
        throws Exception
    {
        JSONObject mooviUser = new JSONObject();
        mooviUser.put( "account", this.account );
        mooviUser.put( "password", this.password );

        mooviUser.put( "id", mooviUserId );
        mooviUser.put( "activity", e.getNewState() == TvAccount.STATE_ENABLE ? 1 : 0 );

        JSONObject result = request( Method.post, "changeUser" )
            .setBody( mooviUser )
            .execute()
            .getJsonObject();

        if( logger.isDebugEnabled() )
        {
            logger.debug( result );
        }
    }

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

        final long[] mooviAccountId = parseMooviAccountId( e.getTvAccountRuntime().getTvAccount() );

        return accountOptionsModify0( e, ctx, mooviAccountId[0], e.getTvAccountRuntime().getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
    }

    private Object accountOptionsModify0( AbstractOrderEvent e, ServerContext ctx, final long mooviUserId, final boolean accountEnabled )
        throws Exception
    {
        logger.debug( "accountOptionsModify0" );

        return productsModifySyncFull( e, ctx, mooviUserId, accountEnabled );
    }

    /**
     * Полная синхронизация продуктов/пакетов.
     * 
     * @param e
     * @param ctx
     * @param abonentAccountId
     * @return
     * @throws Exception
     */
    private Object productsModifySyncFull( final AbstractOrderEvent e, final ServerContext ctx, final long mooviUserId, final boolean accountEnabled )
        throws Exception
    {
        logger.debug( "productsModifyFullSync" );

        final Set<Long> tariffsToAdd = new HashSet<Long>();

        if( accountEnabled )
        {
            if( serviceMode )
            {
                // получаем полный список активных сервисов
                for( ServiceSpec serviceSpec : e.getFullServiceSpecSetToEnable() )
                {
                    tariffsToAdd.add( Utils.parseLong( serviceSpec.getIdentifier().trim() ) );
                }
            }
            else
            {
                // получаем список активных продуктов
                for( ProductSpec productSpec : e.getFullProductSpecSetToEnable() )
                {
                    logger.info( "Product: " + productSpec );

                    tariffsToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
                }

                // добавляем продукты-опции
                if( !(e instanceof AccountOrderEvent) || ((AccountOrderEvent)e).getNewState() == TvAccount.STATE_ENABLE )
                {
                    for( ProductSpec productSpec : e.getNewDeviceOptionProductSpecs() )
                    {
                        logger.info( "Product (option): " + productSpec );

                        tariffsToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
                    }
                }
            }
        }

        // удаляем некорректные записи
        tariffsToAdd.remove( 0L );

        logger.info( "Need serviceIds: " + tariffsToAdd );

        // удаляем всё
        JSONObject deleteUserTariffs = new JSONObject();
        deleteUserTariffs.put( "account", this.account );
        deleteUserTariffs.put( "password", this.password );

        deleteUserTariffs.put( "id", mooviUserId );
        deleteUserTariffs.put( "tariffs", this.allTariffs );

        JSONArray result = request( Method.post, "deleteUserTariffs" )
            .setBody( deleteUserTariffs )
            .execute()
            .getJsonArray();

        if( logger.isDebugEnabled() )
        {
            logger.debug( result );
        }

        // добавляем активные
        if( tariffsToAdd.size() > 0 )
        {
            JSONObject addUserTariffs = new JSONObject();
            addUserTariffs.put( "account", this.account );
            addUserTariffs.put( "password", this.password );

            addUserTariffs.put( "id", mooviUserId );
            addUserTariffs.put( "tariffs", tariffsToAdd );

            result = request( Method.post, "addUserTariffs" )
                .setBody( addUserTariffs )
                .execute()
                .getJsonArray();

            if( logger.isDebugEnabled() )
            {
                logger.debug( result );
            }
        }

        return null;
    }

    @Override
    public Object productsModify( final ProductOrderEvent productOrderEvent, final ServerContext ctx )
        throws Exception
    {
        logger.debug( "productsModify" );

        final long[] mooviAccountId = parseMooviAccountId( productOrderEvent.getTvAccount() );

        if( this.productSyncMode )
        {
            if ( mooviAccountId == null )
            {
                logger.error( "mooviAccountId = null" );
                return null;
            }
            if ( productOrderEvent.getTvAccount() == null )
            {
                logger.error( "productOrderEvent.getTvAccount() = null" );
                return null;
            }
            return productsModifySyncFull( productOrderEvent, ctx, mooviAccountId[0], productOrderEvent.getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
        }

        final Set<Long> tariffsToRemove = new HashSet<Long>();
        final Set<Long> tariffsToAdd = new HashSet<Long>();

        if( serviceMode )
        {
            for( ServiceSpec serviceSpec : productOrderEvent.getServiceSpecSetToRemove() )
            {
                tariffsToRemove.add( Utils.parseLong( serviceSpec.getIdentifier().trim() ) );
            }

            for( ServiceSpec serviceSpec : productOrderEvent.getServiceSpecSetToAdd() )
            {
                tariffsToAdd.add( Utils.parseLong( serviceSpec.getIdentifier().trim() ) );
            }
        }
        else
        {
            for( ProductSpec productSpec : productOrderEvent.getProductSpecSetToRemove() )
            {
                tariffsToRemove.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
            }

            for( ProductSpec productSpec : productOrderEvent.getDeviceOptionProductSpecSetToDisable() )
            {
                tariffsToRemove.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
            }

            for( ProductSpec productSpec : productOrderEvent.getProductSpecSetToAdd() )
            {
                tariffsToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
            }

            for( ProductSpec productSpec : productOrderEvent.getDeviceOptionProductSpecSetToEnable() )
            {
                tariffsToAdd.add( Utils.parseLong( productSpec.getIdentifier().trim() ) );
            }
        }

        tariffsToRemove.remove( 0L );
        tariffsToAdd.remove( 0L );

        if( tariffsToRemove.size() > 0 )
        {
            return productsModifySyncFull( productOrderEvent, ctx, mooviAccountId[0], productOrderEvent.getTvAccount().getDeviceState() == TvAccount.STATE_ENABLE );
        }

        // добавляем
        JSONObject addUserTariffs = new JSONObject();
        addUserTariffs.put( "account", this.account );
        addUserTariffs.put( "password", this.password );

        addUserTariffs.put( "id", mooviAccountId[0] );
        addUserTariffs.put( "tariffs", tariffsToAdd );

        JSONObject result = request( Method.post, "addUserTariffs" )
            .setBody( addUserTariffs )
            .execute()
            .getJsonObject();

        if( logger.isDebugEnabled() )
        {
            logger.debug( result );
        }

        return null;
    }
}
