# 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.i18n import gettext
from trytond.model import Index, ModelView, Workflow, fields
from trytond.modules.account.exceptions import FiscalYearNotFoundError
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction, without_check_access

from .exceptions import CounterPartyNotFound, CountryNotFound


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

    _states = {
        'required': Eval('intrastat_type') & (Eval('state') == 'done'),
        'invisible': ~Eval('intrastat_type'),
        }
    _states_dispatch = {
        'required': (
            (Eval('intrastat_type') == 'dispatch')
            & (Eval('state') == 'done')),
        'invisible': Eval('intrastat_type') != 'dispatch',
        }

    intrastat_type = fields.Selection([
            (None, ""),
            ('arrival', "Arrival"),
            ('dispatch', "Dispatch"),
            ], "Intrastat Type", sort=False, readonly=True)
    intrastat_warehouse_country = fields.Many2One(
        'country.country', "Intrastat Warehouse Country",
        ondelete='RESTRICT', states=_states)
    intrastat_country = fields.Many2One(
        'country.country', "Intrastat Country",
        ondelete='RESTRICT', states=_states)
    intrastat_subdivision = fields.Many2One(
        'country.subdivision', "Intrastat Subdivision",
        ondelete='RESTRICT',
        domain=[
            ('country', '=', Eval('intrastat_warehouse_country', -1)),
            ('intrastat_code', '!=', None),
            ],
        states=_states)
    intrastat_tariff_code = fields.Many2One(
        'customs.tariff.code', "Intrastat Tariff Code",
        ondelete='RESTRICT', states=_states)
    intrastat_value = fields.Numeric(
        "Intrastat Value", digits=(16, 2), readonly=True, states=_states)
    intrastat_transaction = fields.Many2One(
        'account.stock.eu.intrastat.transaction', "Intrastat Transaction",
        ondelete='RESTRICT', states=_states)
    intrastat_additional_unit = fields.Float(
        "Intrastat Additional Unit", digits=(16, 3),
        states={
            'required': (
                _states['required'] & Eval('intrastat_tariff_code_uom')),
            'invisible': _states['invisible'],
            })
    intrastat_country_of_origin = fields.Many2One(
        'country.country', "Intrastat Country of Origin",
        ondelete='RESTRICT', states=_states_dispatch)
    intrastat_vat = fields.Many2One(
        'party.identifier', "Intrastat VAT",
        ondelete='RESTRICT',
        domain=[
            ('type', '=', 'eu_vat'),
            ],
        states={
            'invisible': _states_dispatch['invisible'],
            })
    intrastat_declaration = fields.Many2One(
        'account.stock.eu.intrastat.declaration', "Intrastat Declaration",
        readonly=True, states=_states, ondelete='RESTRICT',
        domain=[
            ('company', '=', Eval('company', -1)),
            ('country', '=', Eval('intrastat_warehouse_country', -1)),
            ])

    intrastat_tariff_code_uom = fields.Function(
        fields.Many2One('product.uom', "Intrastat Tariff Code Unit"),
        'on_change_with_intrastat_tariff_code_uom')

    del _states, _states_dispatch

    @classmethod
    def __setup__(cls):
        super().__setup__()
        intrastat_required = Eval('intrastat_type') & (Eval('state') == 'done')
        weight_required = cls.internal_weight.states.get('required')
        if weight_required:
            weight_required |= intrastat_required
        else:
            weight_required = intrastat_required
        cls.internal_weight.states['required'] = weight_required

        t = cls.__table__()
        cls._sql_indexes.add(
            Index(
                t,
                (t.intrastat_declaration, Index.Equality()),
                (t.company, Index.Equality()),
                where=(t.intrastat_type != Null) & (t.state == 'done')))

    @fields.depends(
        'effective_date', 'planned_date',
        'from_location', 'to_location',
        methods=['intrastat_from_country', 'intrastat_to_country'])
    def on_change_with_intrastat_type(self):
        from_country = self.intrastat_from_country
        to_country = self.intrastat_to_country
        if (from_country != to_country
                and from_country and from_country.in_intrastat(
                    date=self.effective_date or self.planned_date)
                and to_country and to_country.in_intrastat(
                    date=self.effective_date or self.planned_date)):
            if self.from_location.type == 'storage' and self.from_warehouse:
                return 'dispatch'
            elif self.to_location.type == 'storage' and self.to_warehouse:
                return 'arrival'

    @fields.depends('intrastat_tariff_code')
    def on_change_with_intrastat_tariff_code_uom(self, name=None):
        if self.intrastat_tariff_code:
            return self.intrastat_tariff_code.intrastat_uom

    @property
    @fields.depends('from_location', 'shipment')
    def intrastat_from_country(self):
        if self.from_location:
            if self.from_warehouse and self.from_warehouse.address:
                return self.from_warehouse.address.country
            elif (self.from_location.type in {'supplier', 'customer'}
                    and hasattr(self.shipment, 'intrastat_from_country')):
                return self.shipment.intrastat_from_country

    @property
    @fields.depends('to_location', 'shipment')
    def intrastat_to_country(self):
        if self.to_location:
            if self.to_warehouse and self.to_warehouse.address:
                return self.to_warehouse.address.country
            elif (self.to_location.type in {'supplier', 'customer'}
                    and hasattr(self.shipment, 'intrastat_to_country')):
                return self.shipment.intrastat_to_country

    @classmethod
    @without_check_access
    def _update_intrastat(cls, moves):
        moves = cls.browse(moves)
        for move in moves:
            if move.state not in {'done', 'cancelled'}:
                intrastat_type = move.on_change_with_intrastat_type()
                if move.intrastat_type != intrastat_type:
                    move.intrastat_type = intrastat_type
            if move.state == 'done':
                intrastat_value = move._intrastat_value()
                if move.intrastat_value != intrastat_value:
                    move.intrastat_value = intrastat_value
        cls.save(moves)

    @classmethod
    @without_check_access
    def _reopen_intrastat(cls, *args):
        pool = Pool()
        IntrastatDeclaration = pool.get(
            'account.stock.eu.intrastat.declaration')
        declarations = set()
        actions = iter(args)
        for moves, values in zip(actions, actions):
            moves = cls.browse(moves)
            if any(k.startswith('intrastat_') for k in values.keys()):
                declarations.update(
                    m.intrastat_declaration for m in moves
                    if m.intrastat_declaration)
        if declarations:
            IntrastatDeclaration.open(
                IntrastatDeclaration.browse(declarations))

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('//page[@id="intrastat"]', 'states', {
                    'invisible': ~Eval('intrastat_type'),
                    }),
            ]

    @classmethod
    def create(cls, vlist):
        moves = super().create(vlist)
        cls._update_intrastat(moves)
        return moves

    @classmethod
    def write(cls, *args):
        cls._reopen_intrastat(*args)
        moves = sum(args[0:None:2], [])
        super().write(*args)
        cls._update_intrastat(moves)
        cls._reopen_intrastat(*args)

    @classmethod
    def copy(cls, moves, default=None):
        default = default.copy() if default else {}
        default.setdefault('intrastat_type')
        default.setdefault('intrastat_warehouse_country')
        default.setdefault('intrastat_country')
        default.setdefault('intrastat_subdivision')
        default.setdefault('intrastat_tariff_code')
        default.setdefault('intrastat_value')
        default.setdefault('intrastat_transaction')
        default.setdefault('intrastat_additional_unit')
        default.setdefault('intrastat_country_of_origin')
        default.setdefault('intrastat_vat')
        default.setdefault('intrastat_declaration')
        return super().copy(moves, default=default)

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, moves):
        pool = Pool()
        IntrastatDeclaration = pool.get(
            'account.stock.eu.intrastat.declaration')
        super().cancel(moves)
        declarations = {
            m.intrastat_declaration for m in moves if m.intrastat_declaration}
        if declarations:
            IntrastatDeclaration.open(
                IntrastatDeclaration.browse(declarations))

    @classmethod
    @ModelView.button
    @Workflow.transition('done')
    def do(cls, moves):
        pool = Pool()
        Warning = pool.get('res.user.warning')
        unknown_country = []
        for move in moves:
            move._set_intrastat()
            if (move.intrastat_type
                    and (not move.intrastat_from_country
                        or not move.intrastat_to_country)):
                unknown_country.append(move)
        if unknown_country:
            warning_name = Warning.format(
                'intrastat_country', unknown_country)
            if Warning.check(warning_name):
                names = ', '.join(m.rec_name for m in unknown_country[:5])
                if len(unknown_country) > 5:
                    names + '...'
                raise CountryNotFound(warning_name,
                    gettext('account_stock_eu.msg_move_country_not_found',
                        moves=names))
        cls.save(moves)
        super().do(moves)

    def _set_intrastat(self):
        pool = Pool()
        IntrastatTransaction = pool.get(
            'account.stock.eu.intrastat.transaction')
        IntrastatDeclaration = pool.get(
            'account.stock.eu.intrastat.declaration')
        Warning = pool.get('res.user.warning')
        if not self.intrastat_type:
            return
        self.set_effective_date()
        self.intrastat_value = self._intrastat_value()
        if self.intrastat_type == 'arrival':
            if not self.intrastat_warehouse_country:
                self.intrastat_warehouse_country = self.intrastat_to_country
            if not self.intrastat_country:
                self.intrastat_country = self.intrastat_from_country
            if not self.intrastat_subdivision:
                if (self.to_warehouse
                        and self.to_warehouse.address
                        and self.to_warehouse.address.subdivision):
                    subdivision = self.to_warehouse.address.subdivision
                    self.intrastat_subdivision = subdivision.get_intrastat()
            if self.intrastat_country_of_origin:
                self.intrastat_country_of_origin = None
            if self.intrastat_vat:
                self.intrastat_vat = None
        elif self.intrastat_type == 'dispatch':
            if not self.intrastat_warehouse_country:
                self.intrastat_warehouse_country = self.intrastat_from_country
            if not self.intrastat_country:
                self.intrastat_country = self.intrastat_to_country
            if not self.intrastat_subdivision:
                if (self.from_warehouse
                        and self.from_warehouse.address
                        and self.from_warehouse.address.subdivision):
                    subdivision = self.from_warehouse.address.subdivision
                    self.intrastat_subdivision = subdivision.get_intrastat()
            if not self.intrastat_country_of_origin:
                self.intrastat_country_of_origin = (
                    self.product.country_of_origin)
            if not self.intrastat_vat:
                counterparty = self._intrastat_counterparty()
                if not counterparty:
                    warning_name = Warning.format(
                        'intrastat_counterparty', [self])
                    if Warning.check(warning_name):
                        raise CounterPartyNotFound(warning_name,
                            gettext('account_stock_eu'
                                '.msg_move_counterparty_not_found',
                                move=self.rec_name))
                else:
                    fallback = None
                    for identifier in counterparty.identifiers:
                        if identifier.type == 'eu_vat':
                            if not fallback:
                                fallback = identifier
                            if (self.intrastat_country
                                    and identifier.code.startswith(
                                        self.intrastat_country.code)):
                                break
                    else:
                        identifier = fallback
                    self.intrastat_vat = identifier
        if self.intrastat_warehouse_country:
            self.intrastat_declaration = IntrastatDeclaration.get(
                self.company,
                self.intrastat_warehouse_country,
                self.effective_date or self.planned_date)
        if not self.intrastat_tariff_code:
            self.intrastat_tariff_code = self.product.get_tariff_code(
                self._intrastat_tariff_code_pattern())
        if not self.intrastat_transaction:
            self.intrastat_transaction = IntrastatTransaction.get(
                self._intrastat_transaction_code())
        if (not self.intrastat_additional_unit
                and self.intrastat_tariff_code
                and self.intrastat_tariff_code.intrastat_uom):
            quantity = self._intrastat_quantity(
                self.intrastat_tariff_code.intrastat_uom)
            if quantity is not None:
                ndigits = self.__class__.intrastat_additional_unit.digits[1]
                self.intrastat_additional_unit = round(quantity, ndigits)

    def _intrastat_tariff_code_pattern(self):
        return {
            'date': self.effective_date,
            'country': (
                self.intrastat_country.id if self.intrastat_country else None),
            }

    def _intrastat_transaction_code(self):
        pool = Pool()
        ShipmentIn = pool.get('stock.shipment.in')
        ShipmentInReturn = pool.get('stock.shipment.in.return')
        ShipmentOut = pool.get('stock.shipment.out')
        ShipmentOutReturn = pool.get('stock.shipment.out.return')
        ShipmentInternal = pool.get('stock.shipment.internal')

        if isinstance(self.shipment, ShipmentInternal):
            return '31'

        try:
            SaleLine = pool.get('sale.line')
        except KeyError:
            pass
        else:
            if isinstance(self.origin, SaleLine):
                sale = self.origin.sale
                party = sale.invoice_party or sale.party
                if self.quantity >= 0:
                    if party.tax_identifier:
                        return '11'
                    else:
                        return '12'
                else:
                    return '21'
        try:
            PurchaseLine = pool.get('purchase.line')
        except KeyError:
            pass
        else:
            if isinstance(self.origin, PurchaseLine):
                purchase = self.origin.purchase
                party = purchase.invoice_party or purchase.party
                if self.quantity >= 0:
                    if party.tax_identifier:
                        return '11'
                    else:
                        return '12'
                else:
                    return '21'

        if isinstance(self.shipment, ShipmentIn):
            if self.shipment.supplier.tax_identifier:
                return '11'
            else:
                return '12'
        elif isinstance(self.shipment, ShipmentInReturn):
            return '21'
        elif isinstance(self.shipment, ShipmentOut):
            if self.shipment.customer.tax_identifier:
                return '11'
            else:
                return '12'
        elif isinstance(self.shipment, ShipmentOutReturn):
            return '21'

    def _intrastat_value(self):
        pool = Pool()
        Currency = pool.get('currency.currency')
        if self.unit_price is not None:
            ndigits = self.__class__.intrastat_value.digits[1]
            with Transaction().set_context(
                    date=self.effective_date or self.planned_date):
                return round(Currency.compute(
                        self.currency,
                        self.unit_price * Decimal(str(self.quantity)),
                        self.company.intrastat_currency or self.currency,
                        round=False), ndigits)

    def _intrastat_quantity(self, unit):
        pool = Pool()
        UoM = pool.get('product.uom')
        if self.unit.category == unit.category:
            return UoM.compute_qty(self.unit, self.quantity, unit, round=False)
        elif (getattr(self, 'secondary_unit', None)
                and self.secondary_unit.category == unit.category):
            return UoM.compute_qty(
                self.secondary_unit, self.secondary_quantity, unit,
                round=False)
        if (self.product.volume
                and self.product.volume_uom.category == unit.category):
            return UoM.compute_qty(
                self.product.volume_uom,
                self.internal_quantity * self.product.volume,
                unit, round=False)

    def _intrastat_counterparty(self):
        pool = Pool()
        ShipmentIn = pool.get('stock.shipment.in')
        ShipmentInReturn = pool.get('stock.shipment.in.return')
        ShipmentOut = pool.get('stock.shipment.out')
        ShipmentOutReturn = pool.get('stock.shipment.out.return')
        ShipmentInternal = pool.get('stock.shipment.internal')

        if isinstance(self.shipment, ShipmentInternal):
            return self.company.party

        try:
            SaleLine = pool.get('sale.line')
        except KeyError:
            pass
        else:
            if isinstance(self.origin, SaleLine):
                sale = self.origin.sale
                return sale.invoice_party or sale.party

        try:
            PurchaseLine = pool.get('purchase.line')
        except KeyError:
            pass
        else:
            if isinstance(self.origin, PurchaseLine):
                purchase = self.origin.purchase
                return purchase.invoice_party or purchase.party

        if isinstance(self.shipment, (ShipmentIn, ShipmentInReturn)):
            return self.shipment.supplier
        elif isinstance(self.shipment, (ShipmentOut, ShipmentOutReturn)):
            return self.shipment.customer


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

    _states = {
        'required': (
            Eval('intrastat_type') & Eval('intrastat_extended')
            & (Eval('state') == 'done')),
        'invisible': ~Eval('intrastat_type') | ~Eval('intrastat_extended'),
        }

    intrastat_transport = fields.Many2One(
        'account.stock.eu.intrastat.transport', "Intrastat Transport",
        ondelete='RESTRICT', states=_states)
    intrastat_incoterm = fields.Many2One(
        'incoterm.incoterm', "Intrastat Incoterm",
        ondelete='RESTRICT', states=_states)

    intrastat_extended = fields.Function(
        fields.Boolean("Intrastat Extended"),
        'on_change_with_intrastat_extended')

    del _states

    @fields.depends('company', 'effective_date', 'planned_date')
    def on_change_with_intrastat_extended(self, name=None):
        pool = Pool()
        FiscalYear = pool.get('account.fiscalyear')
        if self.company:
            try:
                fiscalyear = FiscalYear.find(
                    self.company.id,
                    date=self.effective_date or self.planned_date)
            except FiscalYearNotFoundError:
                pass
            else:
                return fiscalyear.intrastat_extended

    def _set_intrastat(self):
        from trytond.modules.incoterm.common import IncotermMixin
        super()._set_intrastat()

        if not self.intrastat_transport:
            carrier = self._intrastat_carrier()
            if carrier:
                self.intrastat_transport = carrier.intrastat_transport

        if not self.intrastat_incoterm:
            if isinstance(self.shipment, IncotermMixin):
                self.intrastat_incoterm = self.shipment.incoterm
            elif isinstance(self.origin, IncotermMixin):
                self.intrastat_incoterm = self.origin.incoterm

    def _intrastat_carrier(self):
        if (hasattr(self.shipment, 'carrier')
                and not getattr(self.shipment, 'carriages', None)):
            return self.shipment.carrier

    @classmethod
    def copy(cls, moves, default=None):
        default = default.copy() if default else {}
        default.setdefault('intrastat_transport')
        default.setdefault('intrastat_incoterm')
        return super().copy(moves, default=default)


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

    def _intrastat_transaction_code(self):
        code = super()._intrastat_transaction_code()
        if self.is_supplier_consignment or self.is_customer_consignment:
            code = '32'
        return code


class ShipmentMixin:
    __slots__ = ()

    @property
    def intrastat_from_country(self):
        raise NotImplementedError

    @property
    def intrastat_to_country(self):
        raise NotImplementedError


class ShipmentIn(ShipmentMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.in'

    intrastat_from_country = fields.Many2One(
        'country.country', "From Country")
    intrastat_to_country = fields.Function(
        fields.Many2One('country.country', "To Country"),
        'on_change_with_intrastat_to_country')

    @fields.depends('supplier')
    def on_change_supplier(self):
        try:
            super().on_change_supplier()
        except AttributeError:
            pass
        if self.supplier:
            address = self.supplier.address_get(type='delivery')
            if address:
                self.intrastat_from_country = address.country

    @fields.depends('warehouse')
    def on_change_with_intrastat_to_country(self, name=None):
        if self.warehouse and self.warehouse.address:
            return self.warehouse.address.country


class ShipmentInReturn(ShipmentMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.in.return'

    intrastat_from_country = fields.Function(
        fields.Many2One('country.country', "From Country"),
        'on_change_with_intrastat_from_country')
    intrastat_to_country = fields.Function(
        fields.Many2One('country.country', "To Country"),
        'on_change_with_intrastat_to_country')

    @fields.depends('warehouse')
    def on_change_with_intrastat_from_country(self, name=None):
        if self.warehouse and self.warehouse.address:
            return self.warehouse.address.country

    @fields.depends('delivery_address')
    def on_change_with_intrastat_to_country(self, name=None):
        if self.delivery_address:
            return self.delivery_address.country


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

    intrastat_from_country = fields.Function(
        fields.Many2One('country.country', "From Country"),
        'on_change_with_intrastat_from_country')
    intrastat_to_country = fields.Function(
        fields.Many2One('country.country', "To Country"),
        'on_change_with_intrastat_to_country')

    @fields.depends('warehouse')
    def on_change_with_intrastat_from_country(self, name=None):
        if self.warehouse and self.warehouse.address:
            return self.warehouse.address.country

    @fields.depends('delivery_address')
    def on_change_with_intrastat_to_country(self, name=None):
        if self.delivery_address:
            return self.delivery_address.country


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

    intrastat_from_country = fields.Many2One('country.country', "From Country")
    intrastat_to_country = fields.Function(
        fields.Many2One('country.country', "To Country"),
        'on_change_with_intrastat_to_country')

    @fields.depends('customer')
    def on_change_customer(self):
        try:
            super().on_change_customer()
        except AttributeError:
            pass
        if self.customer:
            address = self.customer.address_get(type='delivery')
            if address:
                self.intrastat_from_country = address.country

    @fields.depends('warehouse')
    def on_change_with_intrastat_to_country(self, name=None):
        if self.warehouse and self.warehouse.address:
            return self.warehouse.address.country


class ShipmentInternal(ShipmentMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.internal'

    intrastat_from_country = fields.Function(
        fields.Many2One('country.country', "From Country"),
        'on_change_with_intrastat_from_country')
    intrastat_to_country = fields.Function(
        fields.Many2One('country.country', "To Country"),
        'on_change_with_intrastat_to_country')

    @fields.depends('from_location')
    def on_change_with_intrastat_from_country(self, name=None):
        if (self.from_location
                and self.from_location.warehouse
                and self.from_location.warehouse.address):
            return self.from_location.warehouse.address.country

    @fields.depends('to_location')
    def on_change_with_intrastat_to_country(self, name=None):
        if (self.to_location
                and self.to_location.warehouse
                and self.to_location.warehouse.address):
            return self.to_location.warehouse.address.country


class ShipmentDrop(ShipmentMixin, metaclass=PoolMeta):
    __name__ = 'stock.shipment.drop'

    intrastat_from_country = None
    intrastat_to_country = None
