# 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 collections import defaultdict

from sql import Literal
from sql.operators import Equal

from trytond.cache import Cache
from trytond.model import (
    DeactivableMixin, Exclude, ModelSQL, ModelView, fields)
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction


class Many2ManyInactive(fields.Many2Many):

    def get(self, ids, model, name, values=None):
        with Transaction().set_context(inactive_test=True):
            return super().get(ids, model, name, values=values)

    def set(self, Model, name, ids, values, *args):
        with Transaction().set_context(inactive_test=True):
            return super().set(Model, name, ids, values, *args)


class Inactivate(ModelSQL, DeactivableMixin):

    @classmethod
    def search_domain(cls, domain, active_test=None, tables=None):
        context = Transaction().context
        if context.get('inactive_test'):
            domain = [domain, ('active', '=', False)]
        return super().search_domain(
            domain, active_test=active_test, tables=tables)

    @classmethod
    def on_modification(cls, mode, records, field_names=None):
        super().on_modification(mode, records, field_names=field_names)
        if mode == 'delete':
            cls.copy([r for r in records if r.active and r.shop.to_sync],
                default={
                    'active': False,
                    })


class Shop(DeactivableMixin, ModelSQL, ModelView):
    __name__ = 'web.shop'

    name = fields.Char("Name", required=True)
    company = fields.Many2One('company.company', "Company", required=True)
    currency = fields.Many2One('currency.currency', "Currency", required=True)
    language = fields.Many2One(
        'ir.lang', "Language",
        domain=[
            ('translatable', '=', True),
            ])
    type = fields.Selection([
            (None, ""),
            ], "Type",
        help="The front-end used for the web shop.")
    warehouses = fields.Many2Many(
        'web.shop-stock.location', 'shop', 'warehouse', "Warehouses",
        domain=[
            ('type', '=', 'warehouse'),
            ])
    guest_party = fields.Many2One(
        'party.party', "Guest Party",
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    countries = fields.Many2Many(
        'web.shop-country.country', 'shop', 'country', "Countries")

    products = fields.Many2Many(
        'web.shop-product.product', 'shop', 'product', "Products",
        domain=[
            ('salable', '=', True),
            ],
        context={
            'company': Eval('company', -1),
            },
        depends={'company'},
        help="The list of products to publish.")
    products_removed = Many2ManyInactive(
        'web.shop-product.product', 'shop', 'product', "Products Removed",
        context={
            'company': Eval('company', -1),
            },
        depends={'company'},
        help="The list of products to unpublish.")

    categories = fields.Many2Many(
        'web.shop-product.category', 'shop', 'category', "Categories",
        context={
            'company': Eval('company', -1),
            },
        depends={'company'},
        help="The list of categories to publish.")
    categories_removed = Many2ManyInactive(
        'web.shop-product.category', 'shop', 'category', "Categories Removed",
        context={
            'company': Eval('company', -1),
            },
        depends={'company'},
        help="The list of categories to unpublish.")

    _name_cache = Cache('web.shop.name', context=False)

    @property
    def warehouse(self):
        if self.warehouses:
            return self.warehouses[0]

    @classmethod
    def __setup__(cls):
        super().__setup__()

        t = cls.__table__()
        cls._sql_constraints = [
            ('name_unique', Exclude(t, (t.name, Equal),
                    where=t.active == Literal(True)),
                'web_shop.msg_shop_name_unique'),
            ]

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

    @classmethod
    def default_currency(cls):
        pool = Pool()
        Company = pool.get('company.company')
        company_id = cls.default_company()
        if company_id is not None and company_id >= 0:
            company = Company(company_id)
            return company.currency.id

    @classmethod
    def get(cls, name):
        shop_id = cls._name_cache.get(name)
        if not shop_id:
            shop, = cls.search([('name', '=', name)])
            cls._name_cache.set(name, shop.id)
        else:
            shop = cls(shop_id)
        return shop

    @classmethod
    def copy(cls, shops, default=None):
        default = default.copy() if default is not None else {}
        default.setdefault('warehouses')
        default.setdefault('products')
        default.setdefault('products_removed')
        default.setdefault('categories')
        default.setdefault('categories_removed')
        return super().copy(shops, default=default)

    @classmethod
    def on_modification(cls, mode, shops, field_names=None):
        super().on_modification(mode, shops, field_names=field_names)
        if mode == 'delete' or (mode == 'write' and 'name' in field_names):
            cls._name_cache.clear()

    @property
    def to_sync(self):
        return False

    def _customer_taxe_rule(self):
        pool = Pool()
        Configuration = pool.get('account.configuration')
        config = Configuration(1)
        return config.get_multivalue(
            'default_customer_tax_rule', company=self.company.id)

    def get_context(self):
        pool = Pool()
        Date = pool.get('ir.date')
        with Transaction().set_context(company=self.company.id):
            today = Date.today()
        return {
            'language': self.language.code if self.language else None,
            'company': self.company.id,
            'currency': self.currency.id,
            'locations': [w.id for w in self.warehouses],
            'stock_date_end': today,
            'stock_assign': True,
            }

    def get_products(self, pattern=None, key=None):
        """Return a list of products with their corresponding dictionaries of
        prices and taxes according to the tax pattern.
        If a key function is supplied, the products will be sorted in ascending
        order based on the key applied to each product."""
        pool = Pool()
        Date = pool.get('ir.date')
        Product = pool.get('product.product')
        Tax = pool.get('account.tax')
        if pattern is None:
            pattern = {}

        with Transaction().set_context(**self.get_context()):
            if key is not None:
                all_products = sorted(self.products, key=key)
            else:
                all_products = self.products
            all_products = Product.browse(all_products)
            today = Date.today()

        customer_tax_rule = self._customer_taxe_rule()
        taxes2products = defaultdict(list)
        for product in all_products:
            taxes = set()
            for tax in product.customer_taxes_used:
                if customer_tax_rule:
                    tax_ids = customer_tax_rule.apply(tax, pattern)
                    if tax_ids:
                        taxes.update(tax_ids)
                    continue
                taxes.add(tax.id)
            if customer_tax_rule:
                tax_ids = customer_tax_rule.apply(None, pattern)
                if tax_ids:
                    taxes.update(tax_ids)
            taxes2products[tuple(sorted(taxes))].append(product)

        prices, taxes = {}, {}
        for tax_ids, products in taxes2products.items():
            with Transaction().set_context(**self.get_context()):
                products = Product.browse(products)
                taxes_ = Tax.browse(tax_ids)
                with Transaction().set_context(taxes=tax_ids):
                    prices.update(Product.get_sale_price(products))
            for product in products:
                price = prices[product.id]
                if price is not None:
                    taxes[product.id] = sum(
                        t['amount'] for t in Tax.compute(
                            taxes_, price, 1, today))
                else:
                    taxes[product.id] = None

        return all_products, prices, taxes

    def get_categories(self):
        "Return the list of categories"
        pool = Pool()
        Category = pool.get('product.category')
        with Transaction().set_context(**self.get_context()):
            return Category.browse(self.categories)

    def get_sale(self, party=None):
        pool = Pool()
        Sale = pool.get('sale.sale')
        if not party:
            party = self.guest_party
        sale = Sale()
        sale.company = self.company
        sale.warehouse = self.warehouse
        sale.web_shop = self
        if party:
            sale.party = party
            sale.invoice_address = party.address_get(type='invoice')
            with Transaction().set_context(
                    warehouse=self.warehouse.id if self.warehouse else None):
                sale.shipment_address = party.address_get(type='delivery')
        sale.on_change_web_shop()
        sale.currency = self.currency
        sale.invoice_method = 'order'
        sale.shipment_method = 'order'
        return sale

    def update_sale_ids(self, sale_ids):
        pool = Pool()
        Sale = pool.get('sale.sale')
        return self.update_sales(Sale.browse(sale_ids))

    def update_sales(self, sales):
        assert all(s.web_shop == self for s in sales)

    @property
    def is_managing_gift_card(self):
        "Return if the webshop manages gift cards"
        pass


class Shop_PriceList(metaclass=PoolMeta):
    __name__ = 'web.shop'

    sale_price_list = fields.Many2One(
        'product.price_list', "Sale Price List",
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        help="The price list to compute sale price of products.")
    non_sale_price_list = fields.Many2One(
        'product.price_list', "Non-Sale Price List",
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        help="The price list to compute the price of products "
        "when it is not on sale.")

    def get_context(self):
        context = super().get_context()
        if self.sale_price_list:
            context['price_list'] = self.sale_price_list.id
        if (self.non_sale_price_list
                and Transaction().context.get('_non_sale_price', False)):
            context['price_list'] = self.non_sale_price_list.id
        return context

    def get_sale(self, party=None):
        sale = super().get_sale(party=party)
        if self.sale_price_list:
            sale.price_list = self.sale_price_list
        return sale


class Shop_ShipmentCost(metaclass=PoolMeta):
    __name__ = 'web.shop'

    def get_sale(self, party=None):
        sale = super().get_sale(party=party)
        sale.shipment_cost_method = 'order'
        return sale


class Shop_TaxRuleCountry(metaclass=PoolMeta):
    __name__ = 'web.shop'

    def get_products(self, pattern=None, key=None):
        pattern = pattern.copy() if pattern is not None else {}
        if (self.warehouse
                and self.warehouse.address
                and self.warehouse.address.country):
            pattern.setdefault(
                'from_country', self.warehouse.address.country.id)
        else:
            pattern.setdefault('from_country')
        pattern.setdefault('to_country')
        return super().get_products(pattern=pattern, key=key)


class Shop_Warehouse(ModelSQL):
    __name__ = 'web.shop-stock.location'

    shop = fields.Many2One(
        'web.shop', "Shop", ondelete='CASCADE', required=True)
    warehouse = fields.Many2One(
        'stock.location', "Warehouse", ondelete='CASCADE', required=True,
        domain=[
            ('type', '=', 'warehouse'),
            ])


class Shop_Country(ModelSQL):
    __name__ = 'web.shop-country.country'

    shop = fields.Many2One(
        'web.shop', "Shop", ondelete='CASCADE', required=True)
    country = fields.Many2One(
        'country.country', "Country", ondelete='CASCADE', required=True)


class Shop_Product(Inactivate):
    __name__ = 'web.shop-product.product'

    shop = fields.Many2One(
        'web.shop', "Shop", ondelete='CASCADE', required=True)
    product = fields.Many2One(
        'product.product', "Product", ondelete='RESTRICT', required=True)


class Shop_ProductCategory(Inactivate):
    __name__ = 'web.shop-product.category'

    shop = fields.Many2One(
        'web.shop', "Shop", ondelete='CASCADE', required=True)
    category = fields.Many2One(
        'product.category', "Category", ondelete='RESTRICT', required=True)


class ShopAttribute(metaclass=PoolMeta):
    __name__ = 'web.shop'

    attributes = fields.Many2Many(
        'web.shop-product.attribute', 'shop', 'attribute', "Attributes",
        help="The list of attributes to publish.")
    attributes_removed = Many2ManyInactive(
        'web.shop-product.attribute', 'shop', 'attribute',
        "Attributes Removed",
        help="The list of attributes to unpublish.")

    def get_attributes(self):
        "Return the list of attributes"
        pool = Pool()
        Attribute = pool.get('product.attribute')
        with Transaction().set_context(**self.get_context()):
            return Attribute.browse(self.attributes)


class Shop_Attribute(Inactivate):
    __name__ = 'web.shop-product.attribute'

    shop = fields.Many2One(
        'web.shop', "Shop", ondelete='CASCADE', required=True)
    attribute = fields.Many2One(
        'product.attribute', "Attribute", ondelete='RESTRICT', required=True)


class User(metaclass=PoolMeta):
    __name__ = 'web.user'

    invoice_address = fields.Many2One(
        'party.address', "Invoice Address",
        domain=['OR',
            ('party', '=', Eval('party', -1)),
            ('party', 'in', Eval('secondary_parties', [])),
            ])
    shipment_address = fields.Many2One(
        'party.address', "Shipment Address",
        domain=['OR',
            ('party', '=', Eval('party', -1)),
            ('party', 'in', Eval('secondary_parties', [])),
            ])
