package com.gridnine.xtrip.server.reports.templates

import com.gridnine.xtrip.common.Environment
import com.gridnine.xtrip.common.l10n.model.L10nStringHelper
import com.gridnine.xtrip.common.l10n.model.LocaleHelper
import com.gridnine.xtrip.common.model.EntityContainer
import com.gridnine.xtrip.common.model.booking.*
import com.gridnine.xtrip.common.model.booking.air.Product
import com.gridnine.xtrip.common.model.booking.air.SegmentTariff
import com.gridnine.xtrip.common.model.dict.Airline
import com.gridnine.xtrip.common.model.dict.ContractType
import com.gridnine.xtrip.common.model.dict.DictionaryCache
import com.gridnine.xtrip.common.model.dict.MCOCategory
import com.gridnine.xtrip.common.model.entity.EntityStorage
import com.gridnine.xtrip.common.model.helpers.*
import com.gridnine.xtrip.common.model.profile.Organization
import com.gridnine.xtrip.common.model.system.PaymentType
import com.gridnine.xtrip.common.util.TextUtil
import groovy.transform.Field
import org.apache.commons.lang.StringUtils

import java.util.stream.Collectors

class AeroTicket
{
    String blankOwner
    String subAgent
    String group
    String agent
    String client
    ProductStatus productStatus
    Date issueDate
    String ticketNumber
    int segment
    BigDecimal ticketPrice
    BigDecimal portbiletFee
    BigDecimal portbiletTotalPrice
    BigDecimal subAgentFee
    BigDecimal totalPrice
    String carrier
    String pnr
    String passengerName
    String gdsName
    String paymentType
}

class Total
{
    Total(String object, String valueId, String status)
    {
        this.object = object
        this.valueId = valueId
        this.status = status
    }

    String object
    String valueId
    String status

    boolean equals(o)
    {
        if (this.is(o))
        {
            return true
        }
        if (getClass() != o.class)
        {
            return false
        }

        Total total = (Total) o

        if (object != total.object)
        {
            return false
        }
        if (status != total.status)
        {
            return false
        }
        if (valueId != total.valueId)
        {
            return false
        }

        return true
    }

    int hashCode()
    {
        int result
        result = (object != null ? object.hashCode() : 0)
        result = 31 * result + (valueId != null ? valueId.hashCode() : 0)
        result = 31 * result + (status != null ? status.hashCode() : 0)
        return result
    }
}

@Field int numberOfSoldTickets = 0
@Field int numberOfSegmentsSoldTickets = 0
@Field int numberOfRefundedTickets = 0
@Field int numberOfSegmentsRefundedTickets = 0
@Field int numberOfVoidedTickets = 0
@Field int numberOfSegmentsVoidedTickets = 0
@Field int numberOfExchangedTickets = 0
@Field int numberOfSegmentsExchangedTickets = 0

@Field String specifiedBlankOwners = ''
@Field String specifiedAgencies = ''
@Field String specifiedSubAgents = ''
@Field String specifiedProfileGroup = ''
@Field String specifiedAgents = ''
@Field String specifiedCarrier = ''
@Field String specifiedPaymentType = ''
@Field String specifiedTariffType = ''
@Field String specifiedGds = ''
@Field boolean isGroupedByAgent = false
@Field boolean isGroupedByProfileGroup = false
@Field boolean showClient = false

@Field final static String FARE = 'fare'
@Field final static String PRTB_REWARD = 'prtb_reward'
@Field final static String SUB_AGENT_REWARD = 'subagent_reward'
@Field final static String BLANK_OWNER_FARE = 'owner_fare'
@Field final static String OPERATION_NUMBERS = 'operation_numbers'

processIncomingData()
createSpreadsheetStyles()
composeReport()

@Field private Map<Total, BigDecimal> totalMap = new HashMap<Total, BigDecimal>()
@Field private Set<String> blankOwners = new HashSet<String>()

private String getSpecifiedBlankOwners()
{
    List objectList = parameters['BLANK_OWNER']
    String result = StringUtils.join(new ArrayList() {
        {
            objectList.each {
                add(it.toString())
            }
        }
    }, ', ')
    return result
}

private String getSpecifiedAgencies()
{
    List objectList = parameters['AGENCY']
    String result = StringUtils.join(objectList, ', ')
    return result
}

private String getSpecifiedSubAgents()
{
    List objectList = parameters['SUBAGENT']
    String result = StringUtils.join(new ArrayList() {
        {
            objectList.each {
                def object = EntityStorage.get().resolve(it)?.getEntity()
                add(ProfileHelper.getFullName(object, LocaleHelper.getCurrentLocale(), false))
            }
        }
    }, ', ')
    return result
}

private String getSpecifiedProfileGroup()
{
    def group = parameters['PROFILE_GROUP']
    return group == null ? '' : group.toString()
}

private String getSpecifiedAgents()
{
    List objectList = parameters['AGENT']
    String result = StringUtils.join(new ArrayList() {
        {
            objectList.each {
                def object = EntityStorage.get().resolve(it)?.getEntity()
                add(ProfileHelper.getFullName(object, LocaleHelper.getCurrentLocale(), false))
            }
        }
    }, ', ')
    return result
}

private String getSpecifiedCarriers()
{
    def carrier = parameters['CARRIER']
    return carrier == null ? '' : carrier.toString()
}

private String getSpecifiedPaymentType()
{
    def paymentType = parameters['PAYMENT_TYPE']
    return paymentType == null ? '' : paymentType.toString()
}

private String getSpecifiedTariffType()
{
    def tariffType = parameters['TARIFF_TYPE']
    return tariffType == null ? '' : tariffType.toString()
}

private String getSpecifiedGds()
{
    def gds = parameters['RESERVATION_SYSTEM']
    return gds == null ? '' : gds.toString()
}

private boolean isGroupedByAgent()
{
    return parameters['GROUP_BY_AGENT']
}

private boolean isGroupedByProfileGroup()
{
    return parameters['GROUP_BY_PROFILE_GROUP']
}

private boolean showClient()
{
    return parameters['SHOW_CLIENT']
}

private void processIncomingData()
{
    this.specifiedBlankOwners = getSpecifiedBlankOwners()
    this.specifiedAgencies = getSpecifiedAgencies()
    this.specifiedSubAgents = getSpecifiedSubAgents()
    this.specifiedProfileGroup = getSpecifiedProfileGroup()
    this.specifiedAgents = getSpecifiedAgents()
    this.specifiedCarrier = getSpecifiedCarriers()
    this.specifiedPaymentType = getSpecifiedPaymentType()
    this.specifiedTariffType = getSpecifiedTariffType()
    this.specifiedGds = getSpecifiedGds()
    this.isGroupedByAgent = isGroupedByAgent()
    this.isGroupedByProfileGroup = isGroupedByProfileGroup()
    this.showClient = showClient()
}

private void createSpreadsheetStyles()
{
    createStyle(name: 'title', fontBold: true, h_span: getColumnShiftTotal(16), h_alignment: 'CENTER',
                v_alignment: 'CENTER', fontHeight: 20)
    createStyle(name: 'metadataTitle', fontBold: true, h_span: 2, h_alignment: 'RIGHT', v_alignment: 'CENTER',
                fontHeight: 10)
    createStyle(name: 'metadataValue', parent: 'metadataTitle', h_span: 10, fontBold: false, h_alignment: 'LEFT')
    createStyle(name: 'metadataDateValue', parent: 'metadataValue', format: 'm/d/yy')
    createStyle(name: 'header', fontBold: false, h_alignment: 'CENTER', v_alignment: 'CENTER', fontHeight: 10,
                leftBorder: 'MEDIUM', rightBorder: 'MEDIUM', topBorder: 'MEDIUM', bottomBorder: 'MEDIUM',
                wrapText: true)
    createStyle(name: 'data', fontBold: false, h_alignment: 'CENTER', v_alignment: 'CENTER', fontHeight: 10,
                leftBorder: 'THIN', rightBorder: 'THIN', topBorder: 'THIN', bottomBorder: 'THIN')
    createStyle(name: 'textData', parent: 'data')
    createStyle(name: 'numberData', parent: 'data', h_alignment: 'RIGHT', format: '#,##0.00')
    createStyle(name: 'dateData', parent: 'data', h_alignment: 'RIGHT', format: 'm/d/yy')
    createStyle(name: 'preliminaryTotalText', parent: 'data', h_span: getColumnShiftTotal(6), fontBold: true,
                h_alignment: 'RIGHT')
    createStyle(name: 'finalTotalText', parent: 'data', h_span: 2, fontBold: true, h_alignment: 'RIGHT')
    createStyle(name: 'finalTotalHeaderText', parent: 'data', fontBold: true, h_alignment: 'RIGHT')
    createStyle(name: 'totalNumber', fontBold: true, parent: 'numberData')
    createStyle(name: 'totalIntegerNumber', fontBold: true, parent: 'numberData', format: '0')
}

private void composeReport()
{
    page { 'Авиа билеты' } {
        List<AeroTicket> ticketList = []
        allTickets.each { ProductIndex index ->
            EntityStorage entityStorage = Environment.getPublished(EntityStorage.class);
            def container = entityStorage.get().resolve(index.source)
            if ((container == null) || (container.getEntity() == null))
            {
                return
            }
            BaseProduct prod = BookingHelper.findProductByUid(index.navigationKey, container.entity)
            if ((prod == null) || !(prod instanceof Product))
            {
                return
            }
            else
            {
                BaseProduct baseProduct = prod
                Product product = (Product) baseProduct
                PaymentType paymentType = getPaymentType(product)
                if ((!specifiedPaymentType.isEmpty()) &&
                        ((paymentType == null) || !specifiedPaymentType.contains(paymentType.toString())))
                {
                    return
                }

                boolean negate = (index.getStatus() == ProductStatus.REFUND) ||
                        (index.getStatus() == ProductStatus.EXCHANGE)

                boolean first = true
                for (String ticketNumber : index.getTicketNumbers())
                {
                    AeroTicket ticket = new AeroTicket()
                    populateBlankOwner(index, entityStorage, ticket)
                    populateSubAgent(index, entityStorage, ticket)
                    if (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty())
                    {
                        populateProfileGroup(index, ticket)
                    }
                    if (isGroupedByAgent || !specifiedAgents.isEmpty())
                    {
                        populateAgent(index, product.reservation, ticket)
                    }
                    if (showClient)
                    {
                        populateClient(index, ticket)
                    }
                    populateProductStatus(product, ticket)
                    populateIssueDate(index, ticket)
                    populateTicketNumber(ticketNumber, ticket)
                    populateSegment(product, ticket)
                    populateFare(index, product, negate, ticket, first)
                    populateAgencyReward(index, product, ticket, negate, first)
                    populatePrtbTotalFare(ticket)
                    populateSubAgentReward(product, ticket, negate, first)
                    populateTotalFare(ticket)
                    populateCarrier(index, ticket)
                    populatePnr(index, ticket)
                    populatePassengerName(product, ticket)
                    populateGDS(index, ticket)
                    populatePaymentType(product, ticket)
                    ticketList.add(ticket)
                    first = false;
                }
            }
        }

        fillSpreadSheetHeader()
        fillTableHeader()
        fillTable(ticketList)
        fillTableFooter()
    }
}

private void fillSpreadSheetHeader()
{
    processNextCell(0, 0) {
        rowHeight(30)
        text('Отчет по авиа билетам ', 'title')
    }
    processNextCell(1, 0) {
        rowHeight(12)
        text('Дата составления отчета:', 'metadataTitle')
    }
    processNextCell(0, 1) {
        date(new Date(), 'metadataDateValue')
    }
    processNextCell(1, 0) {
        text('Период:', 'metadataTitle')
    }
    processNextCell(0, 1) {
        text(parameters.REPORT_PERIOD, 'metadataValue')
    }
    if (!specifiedBlankOwners.isEmpty())
    {
        processNextCell(1, 0) {
            text('Владелец бланка:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedBlankOwners, 'metadataValue')
        }
    }
    if (!specifiedAgencies.isEmpty())
    {
        processNextCell(1, 0) {
            text('Агентство:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedAgencies, 'metadataValue')
        }
    }
    if (!specifiedSubAgents.isEmpty())
    {
        processNextCell(1, 0) {
            text('Субагент:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedSubAgents, 'metadataValue')
        }
    }
    if (!specifiedProfileGroup.isEmpty())
    {
        processNextCell(1, 0) {
            text('Группа:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedProfileGroup, 'metadataValue')
        }
    }
    if (!specifiedAgents.isEmpty())
    {
        processNextCell(1, 0) {
            text('Агенты:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedAgents, 'metadataValue')
        }
    }
    if (!specifiedCarrier.isEmpty())
    {
        processNextCell(1, 0) {
            text('Перевозчик:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedCarrier, 'metadataValue')
        }
    }
    if (!specifiedGds.isEmpty())
    {
        processNextCell(1, 0) {
            text('Система бронирования:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedGds, 'metadataValue')
        }
    }
    if (specifiedPaymentType)
    {
        processNextCell(1, 0) {
            text('Форма оплаты:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedPaymentType, 'metadataValue')
        }
    }
    if (specifiedTariffType)
    {
        processNextCell(1, 0) {
            text('Тип тарифа:', 'metadataTitle')
        }
        processNextCell(0, 1) {
            text(specifiedTariffType, 'metadataValue')
        }
    }
}

private void fillTableHeader()
{
    int colWidth = 20
    processNextCell(2, 0) {
        setStyle('header')
        rowHeight(40)
    }
    processNextCell(0, 0) {
        columnWidth(colWidth)
        text('Владелец бланка')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Наименование субагента')
    }
    if (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty())
    {
        processNextCell(0, 1) {
            columnWidth(colWidth)
            text('Группа')
        }
    }
    if (isGroupedByAgent || !specifiedAgents.isEmpty())
    {
        processNextCell(0, 1) {
            columnWidth(colWidth)
            text('Выписывающий агент')
        }
    }
    if (showClient)
    {
        processNextCell(0, 1) {
            columnWidth(colWidth)
            text('Клиент')
        }
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Статус')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Дата выписки')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Номер билета')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Сегменты')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Сумма')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Сбор (ПРТБ)')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Итого (сумма + сбор ПРТБ)')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Сбор (СА)')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Итого (сумма + сбор ПРТБ + сбор СА)')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Перевозчик')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('PNR')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Имя пассажира')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Система бронирования')
    }
    processNextCell(0, 1) {
        columnWidth(colWidth)
        text('Тип оплаты')
    }
}

private void fillTable(List<AeroTicket> ticketList)
{
    if ((isGroupedByAgent || !specifiedAgents.isEmpty()) &&
            (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty()))
    {
        ticketList = ticketList.sort { AeroTicket a, AeroTicket b ->
            a.subAgent <=> b.subAgent ?: a.group <=> b.group ?:
                    a.agent <=> b.agent ?: a.productStatus <=> b.productStatus ?: a.issueDate <=> b.issueDate
        }
    }
    else if (isGroupedByAgent || !specifiedAgents.isEmpty())
    {
        ticketList = ticketList.sort { AeroTicket a, AeroTicket b ->
            a.subAgent <=> b.subAgent ?:
                    a.agent <=> b.agent ?: a.productStatus <=> b.productStatus ?: a.issueDate <=> b.issueDate
        }
    }
    else if (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty())
    {
        ticketList = ticketList.sort { AeroTicket a, AeroTicket b ->
            a.subAgent <=> b.subAgent ?:
                    a.group <=> b.group ?: a.productStatus <=> b.productStatus ?: a.issueDate <=> b.issueDate
        }
    }
    else
    {
        ticketList = ticketList.sort { AeroTicket a, AeroTicket b ->
            a.subAgent <=> b.subAgent ?: a.productStatus <=> b.productStatus ?: a.issueDate <=> b.issueDate
        }
    }

    AeroTicket currentTicket
    AeroTicket nextTicket
    if (ticketList.size() > 0)
    {
        currentTicket = ticketList.first()
    }
    for (int i = 1; i <= ticketList.size(); i++)
    {
        if (i != ticketList.size())
        {
            nextTicket = ticketList.get(i)
        }
        else
        {
            nextTicket = null
        }
        processNextCell(1, 0) {
            rowHeight(12)
            text(currentTicket?.blankOwner, 'textData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.subAgent, 'textData')
        }
        if (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty())
        {
            processNextCell(0, 1) {
                text(currentTicket?.group, 'textData')
            }
        }
        if (isGroupedByAgent || !specifiedAgents.isEmpty())
        {
            processNextCell(0, 1) {
                text(currentTicket?.agent, 'textData')
            }
        }
        if(showClient)
        {
            processNextCell(0, 1) {
                text(currentTicket?.client, 'textData')
            }
        }
        processNextCell(0, 1) {
            text(currentTicket.productStatus.toString(), 'textData')
        }
        processNextCell(0, 1) {
            date(currentTicket?.issueDate, 'dateData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.ticketNumber, 'textData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.segment, 'textData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.ticketPrice, 'numberData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.portbiletFee, 'numberData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.portbiletTotalPrice, 'numberData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.subAgentFee, 'numberData')
        }
        processNextCell(0, 1) {
            number(currentTicket?.totalPrice, 'numberData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.carrier, 'textData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.pnr, 'textData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.passengerName, 'textData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.gdsName, 'textData')
        }
        processNextCell(0, 1) {
            text(currentTicket?.paymentType, 'textData')
        }

        switch (currentTicket.productStatus)
        {
            case ProductStatus.SELL:
                String statusString = ProductStatus.SELL.toString()
                fillTotalMap(currentTicket, statusString)
                numberOfSoldTickets++
                numberOfSegmentsSoldTickets += currentTicket.segment
                break
            case ProductStatus.REFUND:
                String statusString = ProductStatus.REFUND.toString()
                fillTotalMap(currentTicket, statusString)
                numberOfRefundedTickets++
                numberOfSegmentsRefundedTickets += currentTicket.segment
                break
            case ProductStatus.VOID:
                String statusString = ProductStatus.VOID.toString()
                fillTotalMap(currentTicket, statusString)
                numberOfVoidedTickets++
                numberOfSegmentsVoidedTickets += currentTicket.segment
                break
            case ProductStatus.EXCHANGE:
                String statusString = ProductStatus.EXCHANGE.toString()
                fillTotalMap(currentTicket, statusString)
                numberOfExchangedTickets++
                numberOfSegmentsExchangedTickets += currentTicket.segment
                break
        }

        boolean isAgentChanged = !currentTicket?.agent?.equals(nextTicket?.agent)
        boolean isGroupChanged = !currentTicket?.group?.equals((nextTicket?.group))
        boolean isSubAgentChanged = !currentTicket?.subAgent?.equals(nextTicket?.subAgent)
        boolean isStatusChanged = !currentTicket?.productStatus?.equals(nextTicket?.productStatus)
        boolean needTotalForAgent = (isGroupedByAgent || !specifiedAgents.isEmpty()) &&
                ((!isAgentChanged && isStatusChanged) || isAgentChanged)
        boolean needTotalForSubAgent = isSubAgentChanged
        boolean needTotalForGroup = (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty()) && isGroupChanged

        if (needTotalForAgent)
        {
            if (isStatusChanged || isAgentChanged)
            {
                fillPreliminaryTotalRow(currentTicket.agent, currentTicket.productStatus.toString())
            }

            if (isAgentChanged)
            {
                fillPreliminaryTotalRow(currentTicket.agent)
            }
        }

        if (needTotalForGroup)
        {
            fillPreliminaryTotalRow(currentTicket.group, ProductStatus.SELL.toString())
            fillPreliminaryTotalRow(currentTicket.group, ProductStatus.REFUND.toString())
            fillPreliminaryTotalRow(currentTicket.group, ProductStatus.EXCHANGE.toString())
            fillPreliminaryTotalRow(currentTicket.group, ProductStatus.VOID.toString())
            fillPreliminaryTotalRow(currentTicket.group)
        }

        if (needTotalForSubAgent)
        {
            fillPreliminaryTotalRow(currentTicket.subAgent, ProductStatus.SELL.toString())
            fillPreliminaryTotalRow(currentTicket.subAgent, ProductStatus.REFUND.toString())
            fillPreliminaryTotalRow(currentTicket.subAgent, ProductStatus.EXCHANGE.toString())
            fillPreliminaryTotalRow(currentTicket.subAgent, ProductStatus.VOID.toString())
            fillPreliminaryTotalRow(currentTicket.subAgent)
        }
        currentTicket = nextTicket
        rowHeight(12)
    }
}

private void fillPreliminaryTotalRow(String targetObject, String status = '')
{
    BigDecimal fare = BigDecimal.ZERO
    BigDecimal prtbReward = BigDecimal.ZERO
    BigDecimal subAgentReward = BigDecimal.ZERO
    if (!status.equals(''))
    {
        Total fareKey = new Total(targetObject, FARE, status)
        fare = totalMap.get(fareKey) ?: BigDecimal.ZERO
        Total prtbRewardKey = new Total(targetObject, PRTB_REWARD, status)
        prtbReward = totalMap.get(prtbRewardKey) ?: BigDecimal.ZERO
        Total subAgentRewardKey = new Total(targetObject, SUB_AGENT_REWARD, status)
        subAgentReward = totalMap.get(subAgentRewardKey) ?: BigDecimal.ZERO
    }
    else
    {
        Total sellFareKey = new Total(targetObject, FARE, ProductStatus.SELL.toString())
        Total sellPrtbRewardKey = new Total(targetObject, PRTB_REWARD, ProductStatus.SELL.toString())
        Total sellSubAgentRewardKey = new Total(targetObject, SUB_AGENT_REWARD, ProductStatus.SELL.toString())
        Total refundFareKey = new Total(targetObject, FARE, ProductStatus.REFUND.toString())
        Total refundPrtbRewardKey = new Total(targetObject, PRTB_REWARD, ProductStatus.REFUND.toString())
        Total refundSubAgentRewardKey = new Total(targetObject, SUB_AGENT_REWARD, ProductStatus.REFUND.toString())
        Total exchangeFareKey = new Total(targetObject, FARE, ProductStatus.EXCHANGE.toString())
        Total exchangePrtbRewardKey = new Total(targetObject, PRTB_REWARD, ProductStatus.EXCHANGE.toString())
        Total exchangeSubAgentRewardKey = new Total(targetObject, SUB_AGENT_REWARD, ProductStatus.EXCHANGE.toString())
        Total voidFareKey = new Total(targetObject, FARE, ProductStatus.VOID.toString())
        Total voidPrtbRewardKey = new Total(targetObject, PRTB_REWARD, ProductStatus.VOID.toString())
        Total voidSubAgentRewardKey = new Total(targetObject, SUB_AGENT_REWARD, ProductStatus.VOID.toString())
        fare = (totalMap.get(sellFareKey) ?: BigDecimal.ZERO).add(totalMap.get(refundFareKey) ?: BigDecimal.ZERO).add(
                totalMap.get(exchangeFareKey) ?: BigDecimal.ZERO).add(totalMap.get(voidFareKey) ?: BigDecimal.ZERO)
        prtbReward = (totalMap.get(sellPrtbRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(refundPrtbRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(exchangePrtbRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(voidPrtbRewardKey) ?: BigDecimal.ZERO)
        subAgentReward = (totalMap.get(sellSubAgentRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(refundSubAgentRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(exchangeSubAgentRewardKey) ?: BigDecimal.ZERO).add(
                totalMap.get(voidSubAgentRewardKey) ?: BigDecimal.ZERO)
    }

    if (!(fare.equals(BigDecimal.ZERO) && prtbReward.equals(BigDecimal.ZERO) && subAgentReward.equals(BigDecimal.ZERO)))
    {
        processNextCell(1, 0) {
            text(status.equals('') ? String.format('Итого по %s:', targetObject) :
                         String.format('Итого операции %s по %s:', status, targetObject), 'preliminaryTotalText')
        }
        processNextCell(0, 1) {
            number(fare, 'totalNumber')
        }
        processNextCell(0, 1) {
            number(prtbReward, 'totalNumber')
        }
        processNextCell(0, 1) {
            number(fare.add(prtbReward), 'totalNumber')
        }
        processNextCell(0, 1) {
            number(subAgentReward, 'totalNumber')
        }
        processNextCell(0, 1) {
            number(fare.add(prtbReward).add(subAgentReward), 'totalNumber')
        }
        5.times {
            processNextCell(0, 1) {
                text('', 'data')
            }
        }
    }
}

private void fillTotalMap(AeroTicket currentTicket, String statusString)
{
    String subAgent = currentTicket.subAgent
    Total subAgentFareKey = new Total(subAgent, FARE, statusString)
    calculateTotalValueForObject(totalMap, subAgentFareKey, currentTicket.ticketPrice)
    Total subAgentPrtbRewardKey = new Total(subAgent, PRTB_REWARD, statusString)
    calculateTotalValueForObject(totalMap, subAgentPrtbRewardKey, currentTicket.portbiletFee)
    Total subAgentRewardKey = new Total(subAgent, SUB_AGENT_REWARD, statusString)
    calculateTotalValueForObject(totalMap, subAgentRewardKey, currentTicket.subAgentFee)

    if (isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty())
    {
        String group = currentTicket.group
        Total groupFareKey = new Total(group, FARE, statusString)
        calculateTotalValueForObject(totalMap, groupFareKey, currentTicket.ticketPrice)
        Total groupPrtbRewardKey = new Total(group, PRTB_REWARD, statusString)
        calculateTotalValueForObject(totalMap, groupPrtbRewardKey, currentTicket.portbiletFee)
        Total groupRewardKey = new Total(group, SUB_AGENT_REWARD, statusString)
        calculateTotalValueForObject(totalMap, groupRewardKey, currentTicket.subAgentFee)
    }

    if (isGroupedByAgent || !specifiedAgents.isEmpty())
    {
        String agent = currentTicket.agent
        Total agentFareKey = new Total(agent, FARE, statusString)
        calculateTotalValueForObject(totalMap, agentFareKey, currentTicket.ticketPrice)
        Total agentPrtbRewardKey = new Total(agent, PRTB_REWARD, statusString)
        calculateTotalValueForObject(totalMap, agentPrtbRewardKey, currentTicket.portbiletFee)
        Total groupRewardKey = new Total(agent, SUB_AGENT_REWARD, statusString)
        calculateTotalValueForObject(totalMap, groupRewardKey, currentTicket.subAgentFee)
    }

    String blankOwner = currentTicket.blankOwner
    blankOwners.add(blankOwner)
    Total blankOwnerFareKey = new Total(blankOwner, BLANK_OWNER_FARE, '')
    calculateTotalValueForObject(totalMap, blankOwnerFareKey, currentTicket.ticketPrice)
    Total blankOwnerOperationNumberKey = new Total(blankOwner, OPERATION_NUMBERS, '')
    calculateTotalValueForObject(totalMap, blankOwnerOperationNumberKey, BigDecimal.ONE)
}

private void calculateTotalValueForObject(Map<Total, BigDecimal> map, Total objectKey, BigDecimal valueToProcess)
{
    BigDecimal totalFareForSubAgent = map.get(objectKey)
    if (!totalFareForSubAgent)
    {
        map.put(objectKey, valueToProcess)
    }
    else
    {
        map.put(objectKey, totalFareForSubAgent.add(valueToProcess))
    }
}

private void fillTableFooter()
{
    processNextCell(1, 0) {}
    processNextCell(1, getColumnShiftTotal(2)) {
        text('Количество билетов', 'finalTotalHeaderText')
    }
    processNextCell(0, 1) {
        text('Сегменты', 'finalTotalHeaderText')
    }
    fillTotalRow(ProductStatus.SELL)
    fillTotalRow(ProductStatus.REFUND)
    fillTotalRow(ProductStatus.EXCHANGE)
    fillTotalRow(ProductStatus.VOID)
    fillTotalRow()
    fillFinalTable()
}

private void fillTotalRow(ProductStatus totalByStatus = null)
{
    String totalTicketCountString = ''
    int totalTicketCountValue = 0
    int totalTicketSegmentValue = 0
    String totalTicketSumString = ''
    BigDecimal totalTicketFareSumValue = BigDecimal.ZERO
    BigDecimal totalTicketPrtbRewardSumValue = BigDecimal.ZERO
    BigDecimal totalTicketSubAgentRewardSumValue = BigDecimal.ZERO
    switch (totalByStatus)
    {
        case ProductStatus.SELL:
            totalTicketCountString = 'ПРОДАНО БИЛЕТОВ:'
            totalTicketCountValue = numberOfSoldTickets
            totalTicketSegmentValue = numberOfSegmentsSoldTickets
            totalTicketSumString = 'ИТОГО ПО ПРОДАЖАМ:'
            break
        case ProductStatus.REFUND:
            totalTicketCountString = 'ВОЗВРАЩЕНО БИЛЕТОВ:'
            totalTicketCountValue = numberOfRefundedTickets
            totalTicketSegmentValue = numberOfSegmentsRefundedTickets
            totalTicketSumString = 'ИТОГО ПО ВОЗВРАТАМ:'
            break
        case ProductStatus.EXCHANGE:
            totalTicketCountString = 'ОБМЕНЯНО БИЛЕТОВ:'
            totalTicketCountValue = numberOfExchangedTickets
            totalTicketSegmentValue = numberOfSegmentsExchangedTickets
            totalTicketSumString = 'ИТОГО ПО ОБМЕНАМ:'
            break
        case ProductStatus.VOID:
            totalTicketCountString = 'АННУЛИРОВАНО БИЛЕТОВ:'
            totalTicketCountValue = numberOfVoidedTickets
            totalTicketSegmentValue = numberOfSegmentsVoidedTickets
            totalTicketSumString = 'ИТОГО ПО АННУЛЯЦИЯМ:'
            break
        case null:
            totalTicketCountString = 'ИТОГО:'
            totalTicketCountValue = numberOfSoldTickets + numberOfRefundedTickets + numberOfExchangedTickets +
                    numberOfVoidedTickets
            totalTicketSegmentValue = numberOfSegmentsSoldTickets + numberOfSegmentsRefundedTickets +
                    numberOfSegmentsExchangedTickets + numberOfSegmentsVoidedTickets
            totalTicketSumString = 'ИТОГО:'
    }

    totalMap.each { key, value ->
        if (key.getStatus().equals(totalByStatus.toString()) || totalByStatus.equals(null))
        {
            if (key.getValueId().equals(FARE))
            {
                totalTicketFareSumValue = totalTicketFareSumValue.add(value)
            }
            else if (key.getValueId().equals(PRTB_REWARD))
            {
                totalTicketPrtbRewardSumValue = totalTicketPrtbRewardSumValue.add(value)
            }
            else if (key.getValueId().equals(SUB_AGENT_REWARD))
            {
                totalTicketSubAgentRewardSumValue = totalTicketSubAgentRewardSumValue.add(value)
            }
        }
    }
    processNextCell(1, getColumnShiftTotal(0)) {
        text(totalTicketCountString, 'finalTotalText')
    }
    processNextCell(0, 1) {
        number(totalTicketCountValue, 'totalIntegerNumber')
    }
    processNextCell(0, 1) {
        number(totalTicketSegmentValue, 'totalIntegerNumber')
    }

    processNextCell(0, 1) {
        text(totalTicketSumString, 'finalTotalText')
    }
    processNextCell(0, 1) {
        number(totalTicketFareSumValue, 'totalNumber')
    }
    processNextCell(0, 1) {
        number(totalTicketPrtbRewardSumValue, 'totalNumber')
    }
    processNextCell(0, 1) {
        number(totalTicketFareSumValue.add(totalTicketPrtbRewardSumValue), 'totalNumber')
    }
    processNextCell(0, 1) {
        number(totalTicketSubAgentRewardSumValue, 'totalNumber')
    }
    processNextCell(0, 1) {
        number(totalTicketFareSumValue.add(totalTicketPrtbRewardSumValue).add(totalTicketSubAgentRewardSumValue),
               'totalNumber')
    }
}

private void fillFinalTable()
{
    processNextCell(1, 0) {}
    processNextCell(1, getColumnShiftTotal(2)) {
        text('Сумма', 'finalTotalHeaderText')
    }
    processNextCell(0, 1) {
        text('Количество операций', 'finalTotalHeaderText')
    }
    BigDecimal sum = BigDecimal.ZERO
    BigDecimal operationSum = BigDecimal.ZERO
    for (String blankOwner : blankOwners)
    {
        BigDecimal blankOwnerFare = totalMap.get(new Total(blankOwner, BLANK_OWNER_FARE, ''))
        BigDecimal blankOwnerOperationNumber = totalMap.get(new Total(blankOwner, OPERATION_NUMBERS, ''))
        fillFinalFareRow(blankOwner, blankOwnerFare, blankOwnerOperationNumber)
        sum = sum.add(blankOwnerFare)
        operationSum = operationSum.add(blankOwnerOperationNumber)
    }
    fillFinalFareRow('ИТОГО', sum, operationSum)
}

private void fillFinalFareRow(String rowString, BigDecimal fare, BigDecimal operationNumber)
{
    processNextCell(1, getColumnShiftTotal(0)) {
        text(rowString, 'finalTotalText')
    }
    processNextCell(0, 1) {
        number(fare ?: BigDecimal.ZERO, 'totalNumber')
    }
    processNextCell(0, 1) {
        number(operationNumber ?: BigDecimal.ZERO, 'totalIntegerNumber')
    }
}

private void processNextCell(int numberOfRowShifts, int numberOfColumnShifts, Closure action)
{
    numberOfRowShifts.times { nextRow() }
    numberOfColumnShifts.times { nextColumn() }
    action()
}

private int getColumnShiftTotal(int baseShift)
{
    if ((isGroupedByProfileGroup || !specifiedProfileGroup.isEmpty()))
    {
        baseShift++
    }
    if (isGroupedByAgent || !specifiedAgents.isEmpty())
    {
        baseShift++
    }
    if (showClient)
    {
        baseShift++
    }
    return baseShift
}

private void populateSubAgent(ProductIndex index, EntityStorage entityStorage, AeroTicket ticket)
{
    String subAgent = ''
    if (index.getSubagency() != null)
    {
        EntityContainer<Organization> cont = entityStorage.resolve(index.getSubagency());
        if (cont != null)
        {
            Organization org = cont.getEntity();
            if (org != null)
            {
                subAgent = L10nStringHelper.getValue(org.getShortName(), LocaleHelper.getCurrentLocale(), false);
            }
        }
    }
    ticket.setSubAgent(subAgent);
}

private void populateTotalFare(AeroTicket ticket)
{
    ticket.setTotalPrice(ticket.getTicketPrice().add(ticket.getPortbiletFee()).add(ticket.getSubAgentFee()))
}

private void populateFare(ProductIndex index, Product product, boolean negate, AeroTicket ticket, boolean first)
{
    BigDecimal price = AirProductHelper.getEquivalentFare(product);
    if (price == null)
    {
        price = BigDecimal.ZERO;
    }
    if (index.getStatus() == ProductStatus.VOID || !first)
    {
        price = BigDecimal.ZERO;
    }
    else
    {
        price = price.add(AirProductTaxHelper.getEquivalentTaxesAmount(product));
        if (product.getPenalty() != null)
        {
            BigDecimal penalty = product.getPenalty();
            if (((index.getStatus() == ProductStatus.REFUND) || (index.getStatus() == ProductStatus.EXCHANGE)) &&
                    (penalty != null) && (penalty.doubleValue() > 0))
            {
                Product penaltyMco = null;
                for (Product prod : AirProductHelper.getMcoByRelatedProduct(product))
                {
                    if ((prod.getMcoCategory() == MCOCategory.PENALTY) ||
                            (prod.getMcoCategory() == MCOCategory.REBOOKING))
                    {
                        penaltyMco = prod;
                        break;
                    }
                }
                if (penaltyMco == null)
                {
                    price = price.subtract(penalty);
                }
            }
            else if ((index.getStatus() == ProductStatus.SELL) && (product.getPreviousProduct() != null) &&
                    (product.getPreviousProduct().getStatus() == ProductStatus.EXCHANGE) && (penalty != null) &&
                    (penalty.doubleValue() > 0))
            {
                price = price.add(penalty);
            }
            else if (product.getMcoCategory() == MCOCategory.REBOOKING)
            {
                price = price.add(penalty);
            }
        }
        if (negate)
        {
            price = price.negate();
        }
    }
    ticket.setTicketPrice(price)
}

private void populateIssueDate(ProductIndex index, AeroTicket ticket)
{
    if (index.getIssueDate() != null)
    {
        ticket.setIssueDate(index.getIssueDate())
    }
}

private void populateProductStatus(Product product, AeroTicket ticket)
{
    if (product.getStatus() != null)
    {
        ticket.setProductStatus(product.getStatus())
    }
}

private void populateAgent(ProductIndex index, Reservation reservation, AeroTicket ticket)
{
    String agent = '';
    if (index.agent != null)
    {
        agent = index.getAgent().toString()
    }
    else if (index.getSellAgent() != null)
    {
        agent = index.getSellAgent().toString()
    }
    else if (reservation.getBookingAgent() != null)
    {
        agent = reservation.getBookingAgent().toString()
    }
    ticket.setAgent(agent)
}

private void populateBlankOwner(ProductIndex index, EntityStorage entityStorage, AeroTicket ticket)
{
    String blankOwnerStr = index.getBlankOwnerNumber();
    if (index.getBlankOwner() != null)
    {
        EntityContainer<Organization> cont = entityStorage.resolve(index.getBlankOwner());
        if (cont != null)
        {
            Organization org = cont.getEntity();
            blankOwnerStr = L10nStringHelper.getValue(org.getShortName(), LocaleHelper.getCurrentLocale(), false);
        }
    }
    ticket.setBlankOwner(blankOwnerStr);
}

private void populateTicketNumber(String ticketNumber, AeroTicket ticket)
{
    if (ticketNumber != null)
    {
        ticket.setTicketNumber(ticketNumber)
    }
    else
    {
        ticket.setTicketNumber("")
    }
}

private void populateGDS(ProductIndex index, AeroTicket ticket)
{
    if (index.getDisplayedGdsName() != null) {
        ticket.setGdsName(index.getDisplayedGdsName().toString())
    } else if (index.getGdsName() != null) {
        ticket.setGdsName(index.getGdsName().toString())
    } else {
        ticket.setGdsName("")
    }
}

private void populatePassengerName(Product product, AeroTicket ticket)
{
    ticket.setPassengerName(product.getTraveller() != null ? product.getTraveller().getName() : '')
}

private void populateAgencyReward(ProductIndex index, Product product, AeroTicket ticket, boolean negate, boolean first)
{
    BigDecimal agencyReward = BigDecimal.ZERO
    if (product.getStatus() == ProductStatus.VOID)
    {
        agencyReward = BigDecimal.ZERO;
    }
    else
    {
        agencyReward = GeneralProductHelper.calculateCommissions(product, ContractType.SUBAGENCY,
                                                                 GeneralProductHelper.feePropertyTypes);
        if (negate && (agencyReward != null))
        {
            agencyReward = agencyReward.negate();
        }
        if ((agencyReward == null) || (index.getStatus() == ProductStatus.VOID) || !first)
        {
            agencyReward = BigDecimal.ZERO;
        }
    }

    ticket.setPortbiletFee(agencyReward)
}

private void populateSubAgentReward(Product product, AeroTicket ticket, boolean negate, boolean first)
{
    BigDecimal subAgentFee = BigDecimal.ZERO
    if (product.getStatus() == ProductStatus.VOID || !first)
    {
        agencyReward = BigDecimal.ZERO;
    }
    else
    {
        subAgentFee = GeneralProductHelper.calculateCommissions(product, ContractType.CLIENT,
                                                                GeneralProductHelper.feePropertyTypes);
        if ((subAgentFee != null) && negate)
        {
            subAgentFee = subAgentFee.negate();
        }
        if ((subAgentFee != null) && (ticket.getPortbiletFee() != null))
        {
            subAgentFee = subAgentFee.subtract(ticket.getPortbiletFee());
        }
    }
    ticket.setSubAgentFee(subAgentFee);
}

private void populateCarrier(ProductIndex index, AeroTicket ticket)
{
    String carrierStr = "";
    Locale ruLocale = new Locale("RU");
    DictionaryCache dictCache = Environment.getPublished(DictionaryCache.class);
    if (index.getCarrier() != null)
    {
        Airline carrier = dictCache.resolveReference(index.getCarrier());
        if (carrier != null)
        {
            carrierStr = !TextUtil.isBlank(carrier.getTranslations().get(ruLocale)) ?
                    carrier.getTranslations().get(ruLocale) : carrier.getTranslations().get(Locale.ENGLISH);
            if (TextUtil.isBlank(carrierStr))
            {
                Iterator<Map.Entry<Locale, String>> iter = carrier.getTranslations().entrySet().iterator();
                if (iter.hasNext())
                {
                    carrierStr = iter.next().getValue();
                }
            }
        }
    }
    ticket.setCarrier(carrierStr);
}

private void populateProfileGroup(ProductIndex index, AeroTicket ticket)
{
    ticket.setGroup(index.getProfileGroup() == null ? '' : index.getProfileGroup().toString())
}

private void populatePaymentType(Product product, AeroTicket ticket)
{
    ticket.setPaymentType(getPaymentType(product)?.toString())
}

private void populatePnr(ProductIndex index, AeroTicket ticket)
{
    if (TextUtil.nonBlank(index.getDisplayedRecordLocator())) {
        ticket.setPnr(index.getDisplayedRecordLocator())
    } else {
        ticket.setPnr(index.getRecordLocator())
    }
}

private void populateSegment(Product product, AeroTicket ticket)
{
    int segCount = 0;
    for (SegmentTariff t : product.getSegmentTariffs())
    {
        segCount += t.getSegments().size();
    }
    ticket.setSegment(segCount);
}

private void populatePrtbTotalFare(AeroTicket ticket)
{
    ticket.setPortbiletTotalPrice(ticket.getTicketPrice().add(ticket.getPortbiletFee()))
}

private PaymentType getPaymentType(Product product)
{
    return GeneralProductHelper.findFop(product)?.type
}

private void populateClient(ProductIndex index, AeroTicket ticket)
{
    String client = ''
    if (index.client != null)
    {
        client = index.client.toString()
    }
    ticket.setClient(client)
}