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

import bitel.billing.server.contract.bean.Contract;
import bitel.billing.server.contract.bean.ContractManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONObject;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.kernel.contract.balance.server.util.BalanceUtils;
import ru.bitel.bgbilling.kernel.module.common.bean.User;
import ru.bitel.bgbilling.modules.tv.common.bean.TvAccount;
import ru.bitel.bgbilling.modules.tv.server.bean.TvAccountDao;
import ru.bitel.bgbilling.modules.tv.server.handler.HttpHandler;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.TimeUtils;
import ru.bitel.common.Utils;
import ru.bitel.common.model.Tied;
import ru.bitel.oss.systems.inventory.product.common.bean.Product;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductOffering;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpec;
import ru.bitel.oss.systems.inventory.product.common.bean.ProductSpecActivationMode;
import ru.bitel.oss.systems.inventory.product.common.service.ProductService;
import ru.bitel.oss.systems.inventory.product.server.bean.ProductDao;
import ru.bitel.oss.systems.inventory.product.server.bean.ProductSpecDao;
import ru.bitel.oss.systems.order.product.common.service.ProductOrderService;
import ru.bitel.oss.systems.order.product.server.service.ProductOrderServiceImpl;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Connection;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;

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

    private int moduleId;

    @Override
    public void handle( int moduleId, HttpServletRequest request, HttpServletResponse response )
    {
        logger.info( "<< " + request.getRequestURI() );

        ServerContext context = new ServerContext( Setup.getSetup(), moduleId, User.USER_SERVER );
        ServerContext.push( context );

        Setup setup = Setup.getSetup();

        this.moduleId = moduleId;

        try(Connection connection = setup.getDBConnectionFromPool())
        {
            request.setCharacterEncoding( "UTF-8" );
            response.setCharacterEncoding( "UTF-8" );

            LifeStreamRequestParser requestParser = new LifeStreamRequestParser( context.getModuleId(), getPath( request ), request );

            // ищем продукт
            ProductSpec productSpec = findProductOrSendError( connection, requestParser.getNewProductId(), response );
            if( productSpec == null )
                return;

            // ищем аккаунт
            TvAccount tvAccount = findTvAccountOrSendError( connection, requestParser, response );
            if( tvAccount == null )
                return;

            // договор
            int contractId = tvAccount.getContractId();
            Contract contract = findContractOrSendError( connection, contractId, requestParser, response );
            if( contract == null )
                return;

            Date activationTime = new Date( TimeUtils.rountToSeconds( System.currentTimeMillis() ) );
            //найденный корректный режим активации для продукта
            ProductSpecActivationMode activationMode = getCorrectActivationModeOrSendError( productSpec, requestParser, response, activationTime );
            if( activationMode == null )
                return;

            ProductService productService = context.getService( ProductService.class, moduleId );
            ProductOrderService productOrderService = context.getService( ProductOrderService.class, moduleId );

            // если это запрос на разрешение активации или запрос старового типа, где всегда нужна проверка разрешения активации
            boolean allowedActivation = true;
            if( !requestParser.isCommit() )
            {
                allowedActivation = checkAllowedProductActivation( productOrderService, productService, connection, requestParser, tvAccount, productSpec, response );
                logger.debug( String.format( "Активация разрешена? -> %s (accountId=%s, contractId=%s, productSpecId=%s)", allowedActivation, tvAccount.getId(), tvAccount.getContractId(), productSpec.getId() ) );
            }

            if( requestParser.isUpsaleV3() )
            {
                logger.debug( "Upsale_V3" );

                if( !requestParser.isCommit() )
                {
                    logger.debug( "RequestType = ensure; activation is allowed? " + allowedActivation );
                    //запрос можно ли добавить новый продукт или сменить уже активный. Проверяем разрешено ли и отвечаем
                    if( allowedActivation )
                    {
                        //отвечаем, что можно активировать
                        sendProcessedResponse( response, "operation_ensured", null, null, BigDecimal.ZERO );
                    }
                    else
                    {
                        //если активация запрещена, то скорей всего по недостаточному балансу, потому что все остальные ошибки отправили бы раньше
                        sendProcessedResponse( response, "no_sufficient_balance", null, null, BigDecimal.ZERO );
                    }
                }
                else
                {
                    if( requestParser.isAddSubscription() && requestParser.isCommit() )
                    {
                        //значит запрос уже на добавление и ранее мы ответили, что добавлять можно
                        logger.debug( "add-subscription" );
                        logger.debug( "commit" );
                        createAndActivateProduct( context, productOrderService, productService, requestParser, response, tvAccount, productSpec, activationTime, activationMode );
                        sendProcessedResponse( response, "operation_commited", null, null, BigDecimal.ZERO );
                    }
                    else if( requestParser.isCommit() )
                    {
                        //значит разрешено сменить текущую на ту, что запрашивали
                        logger.debug( "replace-subscription" );
                        logger.debug( "commit" );
                        //сначала находим старую и отключаем, после чего создаём и активируем новую подписку
                        ProductSpec oldProduct = findProductOrSendError( connection, requestParser.getOldProductId(), response );
                        if( oldProduct == null )
                            return;

                        List<Product> productList = productService.productList( context.getModuleId(), tvAccount.getContractId(), tvAccount.getId(), false, null, null, new Date(), null, false, false );
                        Product product = productList.stream().filter( p -> p.getProductSpecId() == oldProduct.getId() ).findFirst().orElse( null );
                        if( product != null )
                        {
                            logger.info( String.format( "Деактивация продукта ID=%s, для договора ID=%s, productSpecId=%s", product.getId(), tvAccount.getContractId(), oldProduct.getId() ) );
                            productOrderService.productDeactivate( tvAccount.getContractId(), product.getId(), new Date(), false, false, true );
                        }

                        //создание и активация нового продукта на аккаунте
                        try
                        {
                            logger.info( String.format( "Создание и активация продукта productSpecId=%s, для договора ID=%s", productSpec.getId(), tvAccount.getContractId() ) );
                            createAndActivateProduct( context, productOrderService, productService, requestParser, response, tvAccount, productSpec, activationTime, activationMode );
                            //сообщаем, что успешно активировали продукт
                            sendProcessedResponse( response, "subscription_added", null, "", BigDecimal.ZERO );
                        }
                        catch( BGException ex )
                        {
                            logger.error( ex );
                            sendErrorResponse400( response, "no_subscription_rules", "Ошибка сервера. Не удалось активировать подписку", "" );
                        }
                    }
                }
            }
            else
            {
                logger.debug( "Upsale_V2" );
                //стандартный запрос старой версии на активацию подписки
                //проверяем можно ли сменить подписку и отвечаем
                if( allowedActivation )
                {
                    try
                    {
                        createAndActivateProduct( context, productOrderService, productService, requestParser, response, tvAccount, productSpec, activationTime, activationMode );
                        //сообщаем, что успешно активировали продукт
                        sendProcessedResponse( response, "subscription_added", null, "", BigDecimal.ZERO );
                    }
                    catch( BGException ex )
                    {
                        logger.error( ex );
                        sendErrorResponse400( response, "no_subscription_rules", "Ошибка сервера. Не удалось активировать подписку", "" );
                    }
                }
            }
        }
        catch( Exception ex )
        {
            logger.error( ex.getMessage(), ex );
            sendErrorResponse400( response, "no_subscription_rules", ex.getLocalizedMessage(), ex.toString() );
        }
    }

    // проверка возможности активации продукта одним методом
    private boolean checkAllowedProductActivation( ProductOrderService productOrderService, ProductService productService, Connection connection,
                                                   LifeStreamRequestParser requestParser, TvAccount tvAccount, ProductSpec productSpec,
                                                   HttpServletResponse response)
    throws BGException
    {
        boolean result = true;

        try
        {
            //проверка возможности при текущем статусе договора
            productOrderService.isAllowProductActivate( moduleId, tvAccount.getContractId() );
        }
        catch( BGException e )
        {
            sendErrorResponse400( response, "no_sufficient_balance", "", "" );
        }

        //смотрим на баланс, если <=0, то запрещено
        checkBalanceOrSendError(tvAccount.getContractId(), connection, response);

        List<ProductOffering> offerings = getOfferingsListOrSendError( connection, productOrderService, moduleId, tvAccount, productSpec, response );
        if( Utils.isEmptyCollection( offerings ) )
            return false;

        // проверка не подключен ли он уже у клиента
        if( !checkAvaliableProductOrSendError(connection, moduleId, tvAccount, productSpec, response) )
            return false;

        //проверка совместим ли продукт с уже активными на аккаунте
        if( !checkIncompatibleOrSendError( moduleId, tvAccount.getContractId(), tvAccount.getId(), productSpec, productService, requestParser, response ) )
            return false;

        return result;
    }

    private void checkBalanceOrSendError(int contractId, Connection connection, HttpServletResponse response)
    {
        BigDecimal balance = getBalance( contractId, connection );
        if ( balance.compareTo( BigDecimal.ZERO ) <= 0 )
        {
            //кидаем ошибку по балансу
            sendProcessedResponse( response, "no_sufficient_balance", null, null, balance );
        }
    }

    private BigDecimal getBalance(int contractId, Connection connection)
    {
        BigDecimal balance;
        try(BalanceUtils bu = new BalanceUtils( connection ))
        {
            balance = bu.getBalance( new Date(), contractId );
        }
        return balance;
    }

    //создание и активация продукта на аккаунте
    private void createAndActivateProduct( ServerContext context, ProductOrderService productOrderService, ProductService productService,
                                           LifeStreamRequestParser requestParser, HttpServletResponse response, TvAccount tvAccount,
                                           ProductSpec productSpec, Date activationTime, ProductSpecActivationMode activationMode )
    throws BGException
    {
        //создание продукта и его активация
        Product product = createProduct( productService, productOrderService, requestParser, response, tvAccount.getId(), tvAccount.getContractId(), productSpec.getId(), activationTime, activationMode );
        if( product == null )
            return;

        int createdProductId = product.getId();
        activateProduct( context, productOrderService, product );
        logger.info( "Активирован продукт ProductId=" + createdProductId + ", для договора contractId=" + product.getContractId() );
    }

    /**
     * Создание продукта с проверкой периода, возможностью добавления на аккаунт
     */
    private Product createProduct( ProductService productService, ProductOrderService productOrderService, LifeStreamRequestParser requestParser,
                                   HttpServletResponse response, int tvAccountId, int contractId,
                                   int productSpecId, Date activationTime, ProductSpecActivationMode activationMode )
    throws BGException
    {
        Product product = Product.builder()
            .setAccountId( tvAccountId )
            .setContractId( contractId )
            .setProductSpecId( productSpecId )
            .setActivationTime( activationTime )
            .setActivationModeId( activationMode.getId() )
            .setDeviceState( Product.STATE_ENABLED )
            .setUserId( User.USER_SERVER )
            .setTimeFrom( activationTime )
            .setComment( "Добавлен пользователем через платформу" )
            .build();

        productOrderService.adjustProductPeriod( product, activationMode, false );

        List<Product> activeProductList = productOrderService.activeProductList( contractId, tvAccountId, activationTime );
        final Date _productTimeTo = product.getTimeTo();
        try
        {
            product.setTimeTo( new Date( product.getTimeFrom().getTime() + 1000L ) );
            ((ProductOrderServiceImpl)productOrderService).getTieUtils().checkAdd( activeProductList, product, Calendar.MILLISECOND );
        }
        catch( Tied.TieUnresolvedException ex )
        {
            String errorText = "Невозможно активировать продукт с Identifier = " + requestParser.getNewProductId() + " т.к. на договоре есть уже активные продукты";
            logger.error( errorText );
            sendErrorResponse400( response, "error_bad_request", errorText, "" );
            return null;
        }
        finally
        {
            product.setTimeTo( _productTimeTo );
        }

        int productId = productService.productUpdate( product );
        product.setId( productId );

        return product;
    }

    //непосредственный вызов сервиса для активации продукта
    private void activateProduct( ServerContext context, ProductOrderService productOrderService, Product product )
    throws BGException
    {
        // Если всё нормально, то активируем продукт на договоре
        logger.info( "Активация продукта для договора contractId=" + product.getContractId() );
        productOrderService.productActivate( product, new Date(), false, false );
        context.commit();
    }

    private List<ProductOffering> getOfferingsListOrSendError(Connection connection, ProductOrderService productOrderService, int moduleId, TvAccount tvAccount, ProductSpec productSpec, HttpServletResponse response)
        throws BGException
    {
        String errorText = null;

        try
        {
            productOrderService.isAllowProductActivate( moduleId, tvAccount.getContractId() );
        }
        catch( BGException e )
        {
            errorText = "Запрещена смена продукта с указанным статусом договора для договора ID = " + tvAccount.getContractId();
        }

        List<ProductOffering> offerings = productOrderService.productOfferingList( moduleId, tvAccount.getContractId(), tvAccount.getId(), productSpec.getId(), new Date(), true, false );
        if( Utils.isEmptyCollection( offerings ) )
            errorText = String.format( "Запрещена смена/активация продукта productId=%s, tvAccountId=%s, contractId=%s", productSpec.getId(), tvAccount.getId(), tvAccount.getContractId() );

        logger.debug( "Offering size before balance check=" + offerings.size());
        BigDecimal balance = getBalance( tvAccount.getContractId(), connection );
        offerings.removeIf( offering -> offering.getPrice().compareTo( balance ) >= 0 );
        logger.debug( "Offering size after balance check=" + offerings.size());

        if( logger.isDebugEnabled() )
        {
            for( ProductOffering offering : offerings )
            {
                logger.debug( "OFFERING" );
                logger.debug( offering.toString() + " , price=" + offering.getPrice());
            }
        }

        if( Utils.isEmptyCollection( offerings ) )
        {
            sendProcessedResponse( response, "no_sufficient_balance", null, null, BigDecimal.ZERO );
        }
        else if( Utils.notBlankString( errorText ) )
        {
            logger.error( errorText );
            sendErrorResponse400( response, "no_subscription_rules", errorText, "" );
            return null;
        }

        return offerings;
    }

    //поиск тв аккаунта или отправка ошибки, что найти аккаунт не удалось
    private TvAccount findTvAccountOrSendError( Connection connection, LifeStreamRequestParser requestParser, HttpServletResponse response)
        throws BGException
    {
        TvAccount tvAccount = null;
        try ( var tvAccountDao = new TvAccountDao( connection, requestParser.getModuleId() ) )
        {
            tvAccount = tvAccountDao.getByDeviceAccountId( requestParser.getAccountId(), new Date() );
            if ( tvAccount == null )
            {
                sendErrorResponse400( response, "error_no_account", "Не найден аккаунт с ID = " + requestParser.getAccountId(), "" );
            }
        }
        return tvAccount;
    }

    //поиск продукта с полученным offerId или отправка ошибки, если найти продукт не удалось
    private ProductSpec findProductOrSendError( Connection connection, String offerId, HttpServletResponse response )
        throws BGException
    {
        ProductSpec productSpec;
        try ( ProductSpecDao productSpecDao = new ProductSpecDao( connection ) )
        {
            // продукт, который хотят подключить
            productSpec = productSpecDao.getByIdentifier( offerId, moduleId );
            if ( productSpec == null )
            {
                sendErrorResponse400( response, "error_bad_request", "Продукт с Identifier = " + offerId + "[#offerId] не найдена", "" );
            }
        }
        return productSpec;
    }

    //проверка нет ли среди активных продуктов на аккаунте уже такого продукта
    private boolean checkAvaliableProductOrSendError( Connection connection, int moduleId, TvAccount tvAccount, ProductSpec productSpec, HttpServletResponse response )
    throws BGException
    {
        try ( ProductDao productDao = new ProductDao( connection ) )
        {
            // получаем список текущих продуктов на аккаунте
            List<Product> productList = productDao.list( moduleId, tvAccount.getContractId(), tvAccount.getId(), false, new Date() );
            for( Product product : productList )
            {
                if( product.getProductSpecId() == productSpec.getId() )
                {
                    sendErrorResponse400( response, "no_action_required", "Продукт = " + productSpec.getIdentifier() + " уже подключен для договора ID = " + tvAccount.getContractId(), "" );
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Проверяем есть ли у продукта режимы активации и что только 1 режим! Если несколько, то считаем как ошибкой
     * Так же проверяется, что дата активации попадает в период действия режима активации
     *
     * @return вернет корректный режим активации или null, если была какая-то ошибка
     */
    private ProductSpecActivationMode getCorrectActivationModeOrSendError( ProductSpec productSpec, LifeStreamRequestParser requestParser, HttpServletResponse response, Date activationTime )
    {
        ProductSpecActivationMode activationMode = null;
        List<ProductSpecActivationMode> activationModes = productSpec.getActivationModeList();

        String errorText = null;
        if( Utils.isEmptyCollection( activationModes ) )
            errorText = "Для продукта с Identifier = " + requestParser.getNewProductId() + " не задано ни одного режима активации!";
        else if( activationModes.size() > 1 )
            errorText = "Для продукта с Identifier = " + requestParser.getNewProductId() + " задано более одного режима активации!";

        if( Utils.isBlankString( errorText ) )
        {
            activationMode = activationModes.get( 0 );
            if( !TimeUtils.dateInRange( activationTime, activationMode.getDateFrom(), activationMode.getDateTo() ) )
                errorText = "Активация продукта с ProductId=" + productSpec.getId() + " запрещена с заданным режимом активации запрещена!";
        }

        if( Utils.notBlankString( errorText ) )
        {
            logger.error( errorText );
            sendErrorResponse400( response, "error_bad_request", errorText, "" );
            return null;
        }

        return activationMode;
    }

    //поиск договора или отправка ошибки в ответ, что договор не найден
    private Contract findContractOrSendError( Connection connection, int contractId, LifeStreamRequestParser requestParser, HttpServletResponse response)
    {
        Contract contract;
        try( ContractManager contractManager = new ContractManager( connection ) )
        {
            contract = contractManager.getContractById( contractId );
            if( contract == null )
            {
                String errorText = "Для продукта с Identifier = " + requestParser.getNewProductId() + " не найден договор с ID " + contractId;
                logger.error( errorText );
                sendErrorResponse400( response, "error_bad_request", errorText, "" );
                return null;
            }
        }
        return contract;
    }

    private boolean checkIncompatibleOrSendError( int moduleId, int contractId, int accountId, ProductSpec newProduct, ProductService productService, LifeStreamRequestParser requestParser, HttpServletResponse response )
    throws BGException
    {
        if( newProduct == null )
            return true;

        //все продукты на данный момент времени для этого аккаунта
        List<Product> productsOnContract = productService.productList( moduleId, contractId, accountId, false, null, null, new Date(), new Date(), true, true );

        Set<Integer> incompatible = newProduct.getIncompatible();
        if( Utils.isEmptyCollection( incompatible ) )
            return true;

        boolean result = productsOnContract.stream()
                                           .mapToInt( Product::getProductSpecId )
                                           .boxed()
                                           .toList()
                                           .stream()
                                           .noneMatch( incompatible::contains );

        if( !result )
        {
            String errorText = "Продукта с Identifier = " + requestParser.getNewProductId() + " несовместим с продуктами на договоре";
            logger.error( errorText );
            sendErrorResponse400( response, "error_bad_request", errorText, "" );
        }

        return result;
    }

    /**
     * Результат выполнения запроса. Может принимать одно из следующих значений:
     *   subscription_added - подключена дополнительная подписка, детали в поле transaction
     *   subscription_changed - изменена базовая подписка, детали в поле transaction
     *   no_action_required - подписка уже существует и активна
     *   no_sufficient_balance - недостаточно средств для подключения, сумма для пополнения в поле refillAmount
     *   no_subscription_rules - подключение подписки противоречит внутренним правилам биллинга
     * @param response
     * @param result
     */
    private void sendProcessedResponse( HttpServletResponse response, String result, String billingTransactionId, String transactionTimestamp, BigDecimal refillAmount )
    {
        JSONObject responseJson = new JSONObject();
        responseJson.put( "result", result );
        if ( billingTransactionId != null )
        {
            JSONObject transaction = new JSONObject();
            transaction.put( "id", Utils.maskBlank( billingTransactionId, "" ) );
            transaction.put( "timestamp", Utils.maskBlank( transactionTimestamp, "" ) );
            responseJson.put( "transaction", transaction );
        }
        responseJson.put( "refillAmount", refillAmount.setScale( 0, RoundingMode.DOWN ) );

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

        writeJsonToResponse( response, responseJson );
    }

    private void writeJsonToResponse( HttpServletResponse response, JSONObject json )
    {
        response.setContentType( "application/json" );
        response.setStatus( HttpServletResponse.SC_OK );
        try( Writer writer = new OutputStreamWriter( response.getOutputStream(), "UTF-8" ) )
        {
            writer.write( json.toString() );
            writer.flush();
        }
        catch( IOException ex )
        {
            logger.error( ex );
        }
    }

    /**
     * В поле id объекта errorResponse должен быть один из следующих идентификаторов:
     *   error_no_account - абонента нет в биллинге оператора
     *   error_failed_activation - не удалось активировать услугу на TV-платформе, в поле message должно быть значение из поля error ответа от TV платформы
     *   error_bad_request - ошибка в формате запроса или содержании его полей
     * @param id
     * @return
     */
    private void sendErrorResponse400( HttpServletResponse response, String id, String message, String details )
    {
        logger.error( "Send Error Response -> " + message );
        try
        {
            response.setContentType( "application/json" );
            response.sendError( HttpServletResponse.SC_BAD_REQUEST, getErrorResponse( id, message, details ) );
        }
        catch( IOException ex )
        {
            logger.error( ex );
        }
    }

    private void sendErrorResponse500( HttpServletResponse response, String id, String message, String details )
    {
        try
        {
            response.setContentType( "application/json" );
            response.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, getErrorResponse( id, message, details ) );
        }
        catch( IOException ex )
        {
            logger.error( ex );
        }
    }

    /**
     * Формирует json с текстом ошибки
     */
    private String getErrorResponse( String id, String message, String details )
    {
        JSONObject errorJson = new JSONObject();
        errorJson.put( "id", id );
        errorJson.put( "message", message );
        errorJson.put( "details", details );
        return errorJson.toString();
    }
}