# 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 sql import Literal
from sql.aggregate import Count

from trytond.i18n import gettext
from trytond.model import (
    ModelView, ModelSQL, MatchMixin, DeactivableMixin, sequence_ordered,
    fields)
from trytond.pyson import Eval, If, Bool
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.tools import lstrip_wildcard

from trytond.modules.product import price_digits

from .exceptions import PurchaseUOMWarning


class Template(metaclass=PoolMeta):
    __name__ = "product.template"
    purchasable = fields.Boolean("Purchasable")
    product_suppliers = fields.One2Many(
        'purchase.product_supplier', 'template', "Suppliers",
        states={
            'invisible': (~Eval('purchasable', False)
                | ~Eval('context', {}).get('company')),
            },
        domain=[
            If(~Eval('active'), ('active', '=', False), ()),
            ],
        depends=['purchasable', 'active'])
    purchase_uom = fields.Many2One('product.uom', 'Purchase UOM', states={
            'invisible': ~Eval('purchasable'),
            'required': Eval('purchasable', False),
            },
        domain=[('category', '=', Eval('default_uom_category'))],
        depends=['purchasable', 'default_uom_category'])

    @fields.depends('default_uom', 'purchase_uom', 'purchasable')
    def on_change_default_uom(self):
        try:
            super(Template, self).on_change_default_uom()
        except AttributeError:
            pass
        if self.default_uom:
            if self.purchase_uom:
                if self.default_uom.category != self.purchase_uom.category:
                    self.purchase_uom = self.default_uom
            else:
                self.purchase_uom = self.default_uom

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

    def product_suppliers_used(self, **pattern):
        # Skip rules to test pattern on all records
        with Transaction().set_user(0):
            template = self.__class__(self)
        for product_supplier in template.product_suppliers:
            if product_supplier.match(pattern):
                yield product_supplier

    @classmethod
    def write(cls, *args):
        pool = Pool()
        Warning = pool.get('res.user.warning')
        actions = iter(args)
        for templates, values in zip(actions, actions):
            if not values.get("purchase_uom"):
                continue
            for template in templates:
                if not template.purchase_uom:
                    continue
                if template.purchase_uom.id == values["purchase_uom"]:
                    continue
                for product in template.products:
                    if not product.product_suppliers:
                        continue
                    name = '%s@product_template' % template.id
                    if Warning.check(name):
                        raise PurchaseUOMWarning(
                            name, gettext('purchase.msg_change_purchase_uom'))
        super(Template, cls).write(*args)

    @classmethod
    def copy(cls, templates, default=None):
        pool = Pool()
        ProductSupplier = pool.get('purchase.product_supplier')
        if default is None:
            default = {}
        else:
            default = default.copy()

        copy_suppliers = 'product_suppliers' not in default
        default.setdefault('product_suppliers', None)
        new_templates = super().copy(templates, default)
        if copy_suppliers:
            old2new = {}
            to_copy = []
            for template, new_template in zip(templates, new_templates):
                to_copy.extend(
                    ps for ps in template.product_suppliers if not ps.product)
                old2new[template.id] = new_template.id
            if to_copy:
                ProductSupplier.copy(to_copy, {
                        'template': lambda d: old2new[d['template']],
                        })
        return new_templates


class Product(metaclass=PoolMeta):
    __name__ = 'product.product'

    product_suppliers = fields.One2Many(
        'purchase.product_supplier', 'product', "Suppliers",
        domain=[
            ('template', '=', Eval('template')),
            If(~Eval('active'), ('active', '=', False), ()),
            ],
        states={
            'invisible': (~Eval('purchasable', False)
                | ~Eval('context', {}).get('company')),
            },
        depends=['template', 'purchasable', 'active'])
    purchase_price_uom = fields.Function(fields.Numeric(
            "Purchase Price", digits=price_digits), 'get_purchase_price_uom')

    @classmethod
    def get_purchase_price_uom(cls, products, name):
        quantity = Transaction().context.get('quantity') or 0
        return cls.get_purchase_price(products, quantity=quantity)

    def product_suppliers_used(self, **pattern):
        # Skip rules to test pattern on all records
        with Transaction().set_user(0):
            product = self.__class__(self)
        for product_supplier in product.product_suppliers:
            if product_supplier.match(pattern):
                yield product_supplier
        pattern['product'] = None
        yield from self.template.product_suppliers_used(**pattern)

    def _get_purchase_unit_price(self, quantity=0):
        return self.get_multivalue('cost_price')

    @classmethod
    def get_purchase_price(cls, products, quantity=0):
        '''
        Return purchase price for product ids.
        The context that can have as keys:
            uom: the unit of measure
            supplier: the supplier party id
            currency: the currency id for the returned price
        '''
        pool = Pool()
        Uom = pool.get('product.uom')
        User = pool.get('res.user')
        Currency = pool.get('currency.currency')
        Date = pool.get('ir.date')
        ProductSupplier = pool.get('purchase.product_supplier')
        ProductSupplierPrice = pool.get('purchase.product_supplier.price')

        today = Date.today()
        context = Transaction().context
        prices = {}

        assert len(products) == len(set(products)), "Duplicate products"

        uom = None
        if context.get('uom'):
            uom = Uom(context['uom'])

        currency = None
        if context.get('currency'):
            currency = Currency(context['currency'])

        user = User(Transaction().user)

        for product in products:
            prices[product.id] = product._get_purchase_unit_price(
                quantity=quantity)
            default_uom = product.default_uom
            default_currency = (user.company.currency if user.company
                else None)
            if not uom or default_uom.category != uom.category:
                product_uom = default_uom
            else:
                product_uom = uom
            pattern = ProductSupplier.get_pattern()
            product_suppliers = product.product_suppliers_used(**pattern)
            try:
                product_supplier = next(product_suppliers)
            except StopIteration:
                pass
            else:
                pattern = ProductSupplierPrice.get_pattern()
                for price in product_supplier.prices:
                    if price.match(quantity, product_uom, pattern):
                        prices[product.id] = price.unit_price
                        default_uom = product_supplier.uom
                        default_currency = product_supplier.currency
            prices[product.id] = Uom.compute_price(
                default_uom, prices[product.id], product_uom)
            if currency and default_currency:
                date = context.get('purchase_date') or today
                with Transaction().set_context(date=date):
                    prices[product.id] = Currency.compute(default_currency,
                        prices[product.id], currency, round=False)
        return prices

    @classmethod
    def copy(cls, products, default=None):
        pool = Pool()
        ProductSupplier = pool.get('purchase.product_supplier')
        if default is None:
            default = {}
        else:
            default = default.copy()

        copy_suppliers = 'product_suppliers' not in default
        if 'template' in default:
            default.setdefault('product_suppliers', None)
        new_products = super().copy(products, default)
        if 'template' in default and copy_suppliers:
            template2new = {}
            product2new = {}
            to_copy = []
            for product, new_product in zip(products, new_products):
                if product.product_suppliers:
                    to_copy.extend(product.product_suppliers)
                    template2new[product.template.id] = new_product.template.id
                    product2new[product.id] = new_product.id
            if to_copy:
                ProductSupplier.copy(to_copy, {
                        'product': lambda d: product2new[d['product']],
                        'template': lambda d: template2new[d['template']],
                        })
        return new_products


class ProductSupplier(
        sequence_ordered(), DeactivableMixin, ModelSQL, ModelView, MatchMixin):
    'Product Supplier'
    __name__ = 'purchase.product_supplier'
    template = fields.Many2One(
        'product.template', "Product",
        required=True, ondelete='CASCADE', select=True,
        domain=[
            If(Bool(Eval('product')),
                ('products', '=', Eval('product')),
                ()),
            If(Eval('active'), ('active', '=', True), ()),
            ],
        context={
            'company': Eval('company', -1),
            },
        depends=['product', 'active', 'company'])
    product = fields.Many2One(
        'product.product', "Variant", select=True,
        domain=[
            If(Bool(Eval('template')),
                ('template', '=', Eval('template')),
                ()),
            If(Eval('active'), ('active', '=', True), ()),
            ],
        context={
            'company': Eval('company', -1),
            },
        depends=['template', 'active', 'company'])
    party = fields.Many2One(
        'party.party', 'Supplier', required=True, ondelete='CASCADE',
        select=True,
        context={
            'company': Eval('company', -1),
            },
        depends=['company'])
    name = fields.Char('Name', size=None, translate=True, select=True)
    code = fields.Char('Code', size=None, select=True)
    prices = fields.One2Many('purchase.product_supplier.price',
            'product_supplier', 'Prices')
    company = fields.Many2One(
        'company.company', "Company",
        required=True, ondelete='CASCADE', select=True)
    lead_time = fields.TimeDelta('Lead Time',
        help="The time from confirming the purchase order to receiving the "
        "products.\n"
        "If empty the lead time of the supplier is used.")
    currency = fields.Many2One('currency.currency', 'Currency', required=True,
        ondelete='RESTRICT')
    uom = fields.Function(
        fields.Many2One('product.uom', "UOM"), 'on_change_with_uom')

    @classmethod
    def __register__(cls, module_name):
        transaction = Transaction()
        cursor = transaction.connection.cursor()
        table = cls.__table_handler__(module_name)
        sql_table = cls.__table__()

        # Migration from 5.0: add product/template
        if (table.column_exist('product')
                and not table.column_exist('template')):
            table.column_rename('product', 'template')

        super(ProductSupplier, cls).__register__(module_name)

        # Migration from 3.8: change delivery_time inte timedelta lead_time
        if table.column_exist('delivery_time'):
            cursor.execute(*sql_table.select(
                    sql_table.id, sql_table.delivery_time))
            for id_, delivery_time in cursor:
                if delivery_time is None:
                    continue
                lead_time = datetime.timedelta(days=delivery_time)
                cursor.execute(*sql_table.update(
                        [sql_table.lead_time],
                        [lead_time],
                        where=sql_table.id == id_))
            table.drop_column('delivery_time')

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

    @staticmethod
    def default_currency():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.id

    @fields.depends(
        'product', '_parent_product.template')
    def on_change_product(self):
        if self.product:
            self.template = self.product.template

    @fields.depends('party')
    def on_change_party(self):
        cursor = Transaction().connection.cursor()
        self.currency = self.default_currency()
        if self.party:
            table = self.__table__()
            cursor.execute(*table.select(table.currency,
                    where=table.party == self.party.id,
                    group_by=table.currency,
                    order_by=Count(Literal(1)).desc))
            row = cursor.fetchone()
            if row:
                self.currency, = row

    def get_rec_name(self, name):
        if self.name:
            name = self.name
        elif self.product:
            name = self.product.name
        else:
            name = self.template.name
        if self.code:
            name = '[' + self.code + '] ' + name
        return name

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        code_value = clause[2]
        if clause[1].endswith('like'):
            code_value = lstrip_wildcard(clause[2])
        domain = [bool_op,
            ('template',) + tuple(clause[1:]),
            ('product',) + tuple(clause[1:]),
            ('party',) + tuple(clause[1:]),
            ('code', clause[1], code_value) + tuple(clause[3:]),
            ('name',) + tuple(clause[1:]),
            ]
        return domain

    @fields.depends(
        'product', '_parent_product.purchase_uom',
        'template', '_parent_template.purchase_uom')
    def on_change_with_uom(self, name=None):
        if self.product:
            if self.product.purchase_uom:
                return self.product.purchase_uom.id
        elif self.template:
            if self.template.purchase_uom:
                return self.template.purchase_uom.id

    @property
    def lead_time_used(self):
        # Use getattr because it can be called with an unsaved instance
        lead_time = getattr(self, 'lead_time', None)
        party = getattr(self, 'party', None)
        if lead_time is None and party:
            company = getattr(self, 'company', None)
            lead_time = party.get_multivalue(
                'supplier_lead_time', company=company.id if company else None)
        return lead_time

    def compute_supply_date(self, date=None):
        '''
        Compute the supply date for the Product Supplier at the given date
        '''
        Date = Pool().get('ir.date')

        if not date:
            date = Date.today()
        if self.lead_time_used is None:
            return datetime.date.max
        return date + self.lead_time_used

    def compute_purchase_date(self, date):
        '''
        Compute the purchase date for the Product Supplier at the given date
        '''
        Date = Pool().get('ir.date')

        if self.lead_time_used is None or date is None:
            return Date.today()
        return date - self.lead_time_used

    @staticmethod
    def get_pattern():
        context = Transaction().context
        pattern = {'party': context.get('supplier')}
        if 'product_supplier' in context:
            pattern['id'] = context['product_supplier']
        return pattern


class ProductSupplierPrice(
        sequence_ordered(), ModelSQL, ModelView, MatchMixin):
    'Product Supplier Price'
    __name__ = 'purchase.product_supplier.price'
    product_supplier = fields.Many2One('purchase.product_supplier',
            'Supplier', required=True, ondelete='CASCADE')
    quantity = fields.Float(
        "Quantity",
        required=True,
        domain=[('quantity', '>=', 0)],
        help='Minimal quantity.')
    unit_price = fields.Numeric('Unit Price', required=True,
        digits=price_digits)

    uom = fields.Function(fields.Many2One('product.uom', 'UOM',
        help="The unit in which the quantity is specified."),
        'on_change_with_uom')
    currency = fields.Function(
        fields.Many2One('currency.currency', 'Currency'),
        'on_change_with_currency')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('product_supplier')

    @classmethod
    def __register__(cls, module_name):
        cursor = Transaction().connection.cursor()
        table = cls.__table_handler__(module_name)
        sql_table = cls.__table__()

        fill_sequence = not table.column_exist('sequence')

        super(ProductSupplierPrice, cls).__register__(module_name)

        # Migration from 3.2: replace quantity by sequence for order
        if fill_sequence:
            cursor.execute(*sql_table.update(
                    [sql_table.sequence], [sql_table.quantity]))

    @staticmethod
    def default_quantity():
        return 0.0

    @fields.depends('product_supplier', '_parent_product_supplier.uom')
    def on_change_with_uom(self, name=None):
        if self.product_supplier and self.product_supplier.uom:
            return self.product_supplier.uom.id

    @fields.depends('product_supplier', '_parent_product_supplier.currency')
    def on_change_with_currency(self, name=None):
        if self.product_supplier and self.product_supplier.currency:
            return self.product_supplier.currency.id

    @staticmethod
    def get_pattern():
        return {}

    def match(self, quantity, uom, pattern):
        pool = Pool()
        Uom = pool.get('product.uom')
        test_quantity = Uom.compute_qty(
            self.product_supplier.uom, self.quantity, uom)
        if test_quantity > abs(quantity):
            return False
        return super(ProductSupplierPrice, self).match(pattern)
