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

from sql import Null

from trytond import backend
from trytond.model import (
    DeactivableMixin, MatchMixin, ModelSQL, ModelView, fields,
    sequence_ordered)
from trytond.modules.product import price_digits
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, If
from trytond.transaction import Transaction


class CountryMatchMixin(MatchMixin):
    country = fields.Many2One(
        'country.country', "Country",
        states={
            'invisible': Bool(Eval('organization')),
            })
    organization = fields.Many2One(
        'country.organization', "Organization",
        states={
            'invisible': Bool(Eval('country')),
            })

    def match(self, pattern, match_none=False):
        if 'country' in pattern:
            pattern = pattern.copy()
            country = pattern.pop('country')
            if country is not None or match_none:
                if self.country and self.country.id != country:
                    return False
                if (self.organization
                        and country not in [
                            c.id for c in self.organization.countries]):
                    return False
        return super().match(pattern, match_none=match_none)


class TariffCode(DeactivableMixin, CountryMatchMixin, ModelSQL, ModelView):
    __name__ = 'customs.tariff.code'
    _rec_name = 'code'
    code = fields.Char('Code', required=True,
        help='The code from Harmonized System of Nomenclature.')
    description = fields.Char('Description', translate=True)
    start_month = fields.Many2One('ir.calendar.month', "Start Month",
        states={
            'required': Eval('end_month') | Eval('start_day'),
            })
    start_day = fields.Integer('Start Day',
        domain=['OR',
            ('start_day', '=', None),
            [('start_day', '>=', 1), ('start_day', '<=', 31)],
            ],
        states={
            'required': Bool(Eval('start_month')),
            })
    end_month = fields.Many2One('ir.calendar.month', "End Month",
        states={
            'required': Eval('start_month') | Eval('end_day'),
            })
    end_day = fields.Integer('End Day',
        domain=['OR',
            ('end_day', '=', None),
            [('end_day', '>=', 1), ('end_day', '<=', 31)],
            ],
        states={
            'required': Bool(Eval('end_month')),
            })
    duty_rates = fields.One2Many('customs.duty.rate', 'tariff_code',
        'Duty Rates')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('code', 'ASC'))

    @classmethod
    def __register__(cls, module_name):
        transaction = Transaction()
        cursor = transaction.connection.cursor()
        pool = Pool()
        Month = pool.get('ir.calendar.month')
        sql_table = cls.__table__()
        month = Month.__table__()
        table_h = cls.__table_handler__(module_name)

        # Migration from 6.6: use ir.calendar
        migrate_calendar = False
        if (backend.TableHandler.table_exist(cls._table)
                and table_h.column_exist('start_month')
                and table_h.column_exist('end_month')):
            migrate_calendar = (
                table_h.column_is_type('start_month', 'VARCHAR')
                or table_h.column_is_type('end_month', 'VARCHAR'))
            if migrate_calendar:
                table_h.column_rename('start_month', '_temp_start_month')
                table_h.column_rename('end_month', '_temp_end_month')

        super().__register__(module_name)

        table_h = cls.__table_handler__(module_name)

        # Migration from 6.6: use ir.calendar
        if migrate_calendar:
            update = transaction.connection.cursor()
            cursor.execute(*month.select(month.id, month.index))
            for month_id, index in cursor:
                str_index = f'{index:02d}'
                update.execute(*sql_table.update(
                        [sql_table.start_month], [month_id],
                        where=sql_table._temp_start_month == str_index))
                update.execute(*sql_table.update(
                        [sql_table.end_month], [month_id],
                        where=sql_table._temp_end_month == str_index))
            table_h.drop_column('_temp_start_month')
            table_h.drop_column('_temp_end_month')

    def match(self, pattern, match_none=False):
        if 'date' in pattern:
            pattern = pattern.copy()
            date = pattern.pop('date')
            if self.start_month and self.end_month:
                start = (self.start_month.index, self.start_day)
                end = (self.end_month.index, self.end_day)
                date = (date.month, date.day)
                if start <= end:
                    if not (start <= date <= end):
                        return False
                else:
                    if end <= date <= start:
                        return False
        return super().match(pattern, match_none=match_none)

    def get_duty_rate(self, pattern):
        for rate in self.duty_rates:
            if rate.match(pattern):
                return rate


class DutyRate(CountryMatchMixin, ModelSQL, ModelView):
    __name__ = 'customs.duty.rate'
    tariff_code = fields.Many2One(
        'customs.tariff.code', "Tariff Code", required=True)
    type = fields.Selection([
            ('import', 'Import'),
            ('export', 'Export'),
            ], 'Type')
    start_date = fields.Date('Start Date',
        domain=['OR',
            ('start_date', '<=', If(Bool(Eval('end_date')),
                    Eval('end_date', datetime.date.max), datetime.date.max)),
            ('start_date', '=', None),
            ])
    end_date = fields.Date('End Date',
        domain=['OR',
            ('end_date', '>=', If(Bool(Eval('start_date')),
                    Eval('start_date', datetime.date.min), datetime.date.min)),
            ('end_date', '=', None),
            ])
    computation_type = fields.Selection([
            ('amount', 'Amount'),
            ('quantity', 'Quantity'),
            ], 'Computation Type')
    amount = fields.Numeric('Amount', digits=price_digits,
        states={
            'required': Eval('computation_type').in_(['amount', 'quantity']),
            'invisible': ~Eval('computation_type').in_(['amount', 'quantity']),
            })
    currency = fields.Many2One('currency.currency', 'Currency',
        states={
            'required': Eval('computation_type').in_(['amount', 'quantity']),
            'invisible': ~Eval('computation_type').in_(['amount', 'quantity']),
            })
    uom = fields.Many2One(
        'product.uom', "UoM",
        states={
            'required': Eval('computation_type') == 'quantity',
            'invisible': Eval('computation_type') != 'quantity',
            },
        help="The Unit of Measure.")

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('start_date', 'ASC'))
        cls._order.insert(0, ('end_date', 'ASC'))

    @classmethod
    def default_type(cls):
        return 'import'

    @staticmethod
    def order_start_date(tables):
        table, _ = tables[None]
        return [table.start_date == Null, table.start_date]

    @staticmethod
    def order_end_date(tables):
        table, _ = tables[None]
        return [table.end_date == Null, table.end_date]

    def match(self, pattern):
        if 'date' in pattern:
            pattern = pattern.copy()
            start = self.start_date or datetime.date.min
            end = self.end_date or datetime.date.max
            if not (start <= pattern.pop('date') <= end):
                return False
        return super().match(pattern)

    def compute(self, currency, quantity, uom, **kwargs):
        return getattr(self, 'compute_%s' % self.computation_type)(
            currency, quantity, uom, **kwargs)

    def compute_amount(self, currency, quantity, uom, **kwargs):
        pool = Pool()
        Currency = pool.get('currency.currency')
        return Currency.compute(self.currency, self.amount, currency)

    def compute_quantity(self, currency, quantity, uom, **kwargs):
        pool = Pool()
        Currency = pool.get('currency.currency')
        Uom = pool.get('product.uom')

        amount = Uom.compute_price(self.uom, self.amount, uom)
        amount *= Decimal(str(quantity))
        return Currency.compute(self.currency, amount, currency)


class Agent(DeactivableMixin, ModelSQL, ModelView):
    __name__ = 'customs.agent'

    party = fields.Many2One(
        'party.party', "Party", required=True, ondelete='CASCADE',
        help="The party which represents the import/export agent.")
    address = fields.Many2One(
        'party.address', "Address", required=True, ondelete='RESTRICT',
        domain=[
            ('party', '=', Eval('party', -1)),
            ],
        help="The address of the agent.")
    tax_identifier = fields.Many2One(
        'party.identifier', "Tax Identifier",
        required=True, ondelete='RESTRICT',
        help="The identifier of the agent for tax report.")

    @classmethod
    def __setup__(cls):
        pool = Pool()
        Party = pool.get('party.party')
        super().__setup__()
        tax_identifier_types = Party.tax_identifier_types()
        cls.tax_identifier.domain = [
            ('party', '=', Eval('party', -1)),
            ('type', 'in', tax_identifier_types),
            ]

    @fields.depends('party', 'address', 'tax_identifier')
    def on_change_party(self):
        if self.party:
            if not self.address or self.address.party != self.party:
                self.address = self.party.address_get()
            if (not self.tax_identifier
                    or self.tax_identifier.party != self.party):
                self.tax_identifier = self.party.tax_identifier
        else:
            self.address = self.tax_identifier = None

    def get_rec_name(self, name):
        return self.party.rec_name

    @classmethod
    def search_rec_name(cls, name, clause):
        return [('party.rec_name', *clause[1:])]


class AgentSelection(
        DeactivableMixin, sequence_ordered(), MatchMixin, ModelSQL, ModelView):
    __name__ = 'customs.agent.selection'

    company = fields.Many2One(
        'company.company', "Company", required=True)
    from_country = fields.Many2One(
        'country.country', "From Country", ondelete='RESTRICT',
        help="Apply only when shipping from this country.\n"
        "Leave empty for any countries.")
    to_country = fields.Many2One(
        'country.country', "To Country", ondelete='RESTRICT',
        help="Apply only when shipping to this country.\n"
        "Leave empty for any countries.")
    agent = fields.Many2One(
        'customs.agent', "Agent", required=True, ondelete='CASCADE',
        help="The selected agent.")

    @classmethod
    def default_company(cls):
        return Transaction().context.get('company')

    @classmethod
    def get_agent(cls, company, pattern):
        for selection in cls.search([
                    ('company', '=', company),
                    ]):
            if selection.match(pattern):
                return selection.agent
