# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal

from sql import Null

from trytond.model import ModelView, Workflow, fields
from trytond.modules.product import price_digits, round_price
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval
from trytond.transaction import Transaction


class Move(metaclass=PoolMeta):
    __name__ = 'stock.move'

    shipment_out_cost_price = fields.Numeric(
        "Shipment Cost Price", digits=price_digits, readonly=True,
        states={
            'invisible': ~Eval('shipment_out_cost_price'),
            },
        )

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._allow_modify_closed_period.add('shipment_out_cost_price')


class ShipmentCostMixin:
    __slots__ = ()

    carrier = fields.Many2One(
        'carrier', "Carrier",
        states={
            'readonly': Eval('shipment_cost_readonly', True),
            },
        context={
            'company': Eval('company', -1),
            },
        depends=['company'])

    cost_currency_used = fields.Function(fields.Many2One(
            'currency.currency', "Cost Currency",
            states={
                'readonly': (
                    Eval('shipment_cost_readonly', True)
                    | ~Eval('cost_edit', False)),
                }),
        'on_change_with_cost_currency_used', setter='set_cost')
    cost_currency = fields.Many2One(
        'currency.currency', "Cost Currency",
        states={
            'required': Bool(Eval('cost')),
            'readonly': Eval('shipment_cost_readonly', True),
            })

    cost_used = fields.Function(fields.Numeric(
            "Cost", digits=price_digits,
            states={
                'readonly': (
                    Eval('shipment_cost_readonly', True)
                    | ~Eval('cost_edit', False)),
                }),
        'on_change_with_cost_used', setter='set_cost')
    cost = fields.Numeric("Cost", digits=price_digits, readonly=True)
    cost_edit = fields.Boolean(
        "Edit Cost",
        states={
            'readonly': Eval('shipment_cost_readonly', True),
            'invisible': Eval('shipment_cost_readonly', True),
            },
        help="Check to edit the cost.")

    shipment_cost_readonly = fields.Function(
        fields.Boolean("Shipment Cost Read Only"),
        'on_change_with_shipment_cost_readonly')

    def _get_carrier_context(self):
        return {}

    def get_carrier_context(self):
        return self._get_carrier_context()

    @fields.depends('carrier', 'company', methods=['_get_carrier_context'])
    def _compute_costs(self):
        costs = {
            'cost': None,
            'cost_currency': None,
            }
        if self.carrier:
            with Transaction().set_context(self._get_carrier_context()):
                cost, currency_id = self.carrier.get_purchase_price()
            if cost is not None:
                costs['cost'] = round_price(cost)
                costs['cost_currency'] = currency_id
        return costs

    @fields.depends(
        'cost_currency', 'cost_edit',
        methods=['_compute_costs', 'on_change_with_shipment_cost_readonly'])
    def on_change_with_cost_currency_used(self, name=None):
        readonly = self.on_change_with_shipment_cost_readonly()
        if not self.cost_edit and not readonly:
            return self._compute_costs()['cost_currency']
        elif self.cost_currency:
            return self.cost_currency

    @fields.depends(
        'cost', 'cost_edit',
        methods=['_compute_costs', 'on_change_with_shipment_cost_readonly'])
    def on_change_with_cost_used(self, name=None):
        cost = self.cost
        readonly = self.on_change_with_shipment_cost_readonly()
        if not self.cost_edit and not readonly:
            cost = self._compute_costs()['cost']
        return cost

    @classmethod
    def set_cost(cls, lines, name, value):
        if name.endswith('_used'):
            name = name[:-len('_used')]
        cls.write([l for l in lines if l.cost_edit], {
                name: value,
                })

    def on_change_with_shipment_cost_readonly(self, name=None):
        raise NotImplementedError

    @classmethod
    def index_set_field(cls, name):
        index = super().index_set_field(name)
        if name == 'cost_used':
            index = cls.index_set_field('cost_currency_used') + 1
        return index

    @property
    def shipment_cost_moves(self):
        raise NotImplementedError

    def _get_shipment_cost(self):
        "Return the cost for the shipment in the company currency"
        pool = Pool()
        Currency = pool.get('currency.currency')
        if self.cost_used:
            return Currency.compute(
                self.cost_currency_used, self.cost_used,
                self.company.currency, round=False)
        else:
            return Decimal(0)


class ShipmentOutCostMixin(ShipmentCostMixin):

    @classmethod
    def __register__(cls, module):
        pool = Pool()
        Company = pool.get('company.company')
        table = cls.__table__()
        table_h = cls.__table_handler__(module)
        company = Company.__table__()
        cursor = Transaction().connection.cursor()

        cost_currency_exists = table_h.column_exist('cost_currency')

        super().__register__(module)

        # Migration from 6.4: fill cost_currency
        if not cost_currency_exists:
            cursor.execute(*table.update(
                    columns=[table.cost_currency],
                    values=[company.select(
                            company.currency,
                            where=company.id == table.company)],
                    where=table.cost != Null))

    @fields.depends('state')
    def on_change_with_shipment_cost_readonly(self, name=None):
        return self.state in {'done', 'cancelled'}

    @classmethod
    def set_shipment_cost(cls, shipments):
        pool = Pool()
        Move = pool.get('stock.move')
        moves = []
        for shipment in shipments:
            cost = shipment._get_shipment_cost()
            if cost is None:
                continue
            factors = getattr(shipment,
                '_get_allocation_shipment_cost_factors_by_%s' %
                shipment.allocation_shipment_cost_method)()
            moves.extend(shipment._allocate_shipment_cost(cost, factors))
        Move.save(moves)

    @property
    def allocation_shipment_cost_method(self):
        if self.carrier:
            return self.carrier.shipment_cost_allocation_method
        return 'cost'

    def _get_allocation_shipment_cost_factors_by_cost(self):
        sum_ = Decimal(0)
        costs = {}
        for move in self.shipment_cost_moves:
            move_cost = (
                (move.cost_price or 0) * Decimal(str(move.internal_quantity)))
            costs[move] = move_cost
            sum_ += move_cost

        factors = {}
        for move in self.shipment_cost_moves:
            if sum_:
                ratio = costs[move] / sum_
            else:
                ratio = Decimal(1) / len(self.shipment_cost_moves)
            factors[move.id] = ratio
        return factors

    def _allocate_shipment_cost(self, cost, factors):
        moves = []
        for move in self.shipment_cost_moves:
            ratio = factors[move.id]
            quantity = Decimal(str(move.internal_quantity))
            if quantity and ratio:
                cost_price = round_price(cost * ratio / quantity)
            else:
                cost_price = Decimal(0)
            if move.shipment_out_cost_price != cost_price:
                move.shipment_out_cost_price = cost_price
                moves.append(move)
        return moves

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def do(cls, shipments):
        for shipment in shipments:
            shipment.cost = shipment.cost_used
            shipment.cost_currency = shipment.cost_currency_used
        cls.save(shipments)
        super().do(shipments)
        cls.set_shipment_cost(shipments)


class ShipmentOut(ShipmentOutCostMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.out'

    @property
    def shipment_cost_moves(self):
        return self.outgoing_moves


class ShipmentOutReturn(ShipmentOutCostMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.out.return'

    @property
    def shipment_cost_moves(self):
        return self.incoming_moves
