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

from trytond.i18n import gettext
from trytond.model import (
    DeactivableMixin, ModelSQL, ModelView, Workflow, fields)
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.tools import grouped_slice, reduce_ids
from trytond.transaction import Transaction

from .exceptions import DuplicateError


class Promotion(metaclass=PoolMeta):
    __name__ = 'sale.promotion'

    coupons = fields.One2Many('sale.promotion.coupon', 'promotion', "Coupons")

    def get_pattern(self, sale):
        pattern = super(Promotion, self).get_pattern(sale)
        if sale.coupons:
            pattern['coupons'] = [c.coupon.id for c in sale.coupons]
        return pattern

    def match(self, pattern):
        if self.coupons:
            pattern = pattern.copy()
            coupons = pattern.pop('coupons', [])
            if not set(coupons).intersection({c.id for c in self.coupons}):
                return False
        return super(Promotion, self).match(pattern)


class PromotionCoupon(ModelSQL, ModelView):
    "Promotion Coupon"
    __name__ = 'sale.promotion.coupon'

    name = fields.Char("Name", required=True)
    number_of_use = fields.Integer(
        "Number of Use", required=True,
        help="How much times a coupon number can be used.\n"
        "0 or below for no limit.")
    per_party = fields.Boolean(
        "Per Party", help="Check to count usage per party.")
    numbers = fields.One2Many(
        'sale.promotion.coupon.number', 'coupon', "Numbers")
    promotion = fields.Many2One('sale.promotion', "Promotion", required=True)

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

    @classmethod
    def default_number_of_use(cls):
        return 0

    @classmethod
    def default_per_party(cls):
        return False


class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
    "Promotion Coupon Number"
    __name__ = 'sale.promotion.coupon.number'
    _rec_name = 'number'

    number = fields.Char("Number", required=True)
    coupon = fields.Many2One(
        'sale.promotion.coupon', "Coupon", required=True)

    @classmethod
    def __setup__(cls):
        super(PromotionCouponNumber, cls).__setup__()
        cls.__access__.add('coupon')
        cls.active = fields.Function(
            fields.Boolean("Active"), 'get_active', searcher='search_active')

    @classmethod
    def _active_query(cls):
        pool = Pool()
        Coupon = pool.get('sale.promotion.coupon')
        Sale = pool.get('sale.sale')
        Sale_Number = pool.get('sale.sale-sale.promotion.coupon.number')
        table = cls.__table__()
        coupon = Coupon.__table__()
        sale = Sale.__table__()
        sale_number = Sale_Number.__table__()
        context = Transaction().context
        party = context.get('party')
        if isinstance(party, int):
            party = [party]

        query = (table
            .join(sale_number, 'LEFT',
                condition=table.id == sale_number.number)
            .join(coupon, condition=table.coupon == coupon.id))

        if party:
            query = query.join(sale, 'LEFT',
                condition=(sale_number.sale == sale.id)
                & (sale.party.in_(party)))
            active = Case(
                ((coupon.number_of_use > 0) & (coupon.per_party),
                    Count(sale.id) < coupon.number_of_use),
                ((coupon.number_of_use > 0)
                    & ~Coalesce(coupon.per_party, False),
                    Count(sale_number.sale) < coupon.number_of_use),
                else_=Literal(True))
        else:
            active = Case(
                ((coupon.number_of_use > 0)
                    & ~Coalesce(coupon.per_party, False),
                    Count(sale_number.sale) < coupon.number_of_use),
                else_=Literal(True))

        query = query.select(
            group_by=[table.id, coupon.number_of_use, coupon.per_party])
        return query, table, active

    @classmethod
    def get_active(cls, numbers, name):
        cursor = Transaction().connection.cursor()

        query, table, active = cls._active_query()
        query.columns = [table.id, active]

        result = {}
        for sub_numbers in grouped_slice(numbers):
            query.where = reduce_ids(table.id, map(int, sub_numbers))
            cursor.execute(*query)
            result.update(dict(cursor))
        return result

    @classmethod
    def search_active(cls, name, clause):
        _, operator, value = clause
        Operator = fields.SQL_OPERATORS[operator]

        query, table, active = cls._active_query()
        query.columns = [table.id]
        query.having = Operator(active, value)
        return [('id', 'in', query)]

    @classmethod
    def validate_fields(cls, numbers, field_names):
        super().validate_fields(numbers, field_names)
        cls.check_unique(numbers, field_names)

    @classmethod
    def check_unique(cls, numbers, field_names=None):
        if field_names and not (field_names & {'number', 'coupon'}):
            return
        duplicates = []
        for sub_numbers in grouped_slice(numbers):
            domain = ['OR']
            for number in sub_numbers:
                domain.append([
                        ('number', '=', number.number),
                        ('id', '!=', number.id),
                        ('coupon.promotion.company', '=',
                            number.coupon.promotion.company.id),
                        ])
            duplicates.extend(cls.search(domain))
        if duplicates:
            numbers = ', '.join(n.number for n in duplicates[:5])
            if len(duplicates) > 5:
                numbers += '...'
            raise DuplicateError(
                gettext('sale_promotion_coupon.msg_duplicate_numbers',
                    numbers=numbers))


class Sale(metaclass=PoolMeta):
    __name__ = 'sale.sale'

    coupons = fields.Many2Many(
        'sale.sale-sale.promotion.coupon.number', 'sale', 'number', "Coupons",
        domain=[
            ('coupon.promotion.company', '=', Eval('company', -1)),
            ],
        context={
            'party': Eval('coupon_parties', []),
            },
        states={
            'readonly': Eval('state') != 'draft',
            },
        depends={'coupon_parties'})
    coupon_parties = fields.Function(fields.Many2Many(
            'party.party', None, None, "Coupon Parties",
            context={
                'company': Eval('company', -1),
                }),
        'on_change_with_coupon_parties')

    @fields.depends(methods=['_coupon_parties'])
    def on_change_with_coupon_parties(self, name=None):
        return list(self._coupon_parties())

    @fields.depends('party')
    def _coupon_parties(self):
        parties = set()
        if self.party:
            parties.add(self.party)
        return parties

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, sales):
        for sale in sales:
            if sale.coupons:
                sale.coupons = []
        cls.save(sales)
        super(Sale, cls).cancel(sales)


class Sale_PromotionCouponNumber(ModelSQL):
    "Sale - Promotion Coupon Number"
    __name__ = 'sale.sale-sale.promotion.coupon.number'

    sale = fields.Many2One(
        'sale.sale', "Sale", required=True, ondelete='CASCADE')
    number = fields.Many2One(
        'sale.promotion.coupon.number', "Number",
        required=True, ondelete='RESTRICT')
