# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

import json
import datetime

from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.addons import decimal_precision as dp
from odoo.osv import expression


class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

    user_id = fields.Many2one('res.users', 'User', readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
                                     readonly=True, copy=True,
                                     states={'new': [('readonly', False)], False: [('readonly', False)]})
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, copy=False,
                             compute='_compute_order_state', store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company', related='user_id.company_id', store=True, readonly=False)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(
            orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [alert.message
                     for alert in self.env['lunch.alert'].search([])
                     if alert.display]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': fields.Date.to_string(line.date),
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = self.date
        date_today = fields.Date.context_today(self)
        if date_order < date_today:
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return


class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'Lunch Order Line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
    order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True,
                                 domain=[('available', '=', True)])
    category_id = fields.Many2one('lunch.product.category', string='Product Category',
                                  related='product_id.category_id', readonly=True, store=True)
    date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
    supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier',
                               readonly=True, store=True)
    user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
                              readonly=True, store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price', readonly=True, store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency', related='order_id.currency_id', readonly=False)

    def _check_supplier_availibility(self):
        products = self.mapped('product_id')
        if not all(product.available for product in products):
            supplier_name = ", ".join(product.supplier.display_name for product in products if not product.available)
            raise ValidationError(_("Vendor(s) '%s' is not available today") % supplier_name)

    @api.model
    def create(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).create(vals)
        res.with_context(lunch_date=res.order_id.date)._check_supplier_availibility()
        return res

    @api.model
    def write(self, vals):
        """ Override as an onchange would not apply if using the history buttons """
        res = super(LunchOrderLine, self).write(vals)
        if vals.get('product_id'):
            for line in self:
                line.with_context(lunch_date=line.order_id.date)._check_supplier_availibility()
        return res

    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.write({'state': 'ordered'})

            order = {
                'supplier': False,
                'company': False,
                'currency': False,
            }
            group_lines = {}
            for line in self:
                if not line.supplier:
                    # do not send emails for products with no suppliers
                    continue

                if order['supplier'] and line.supplier != order['supplier']:
                    raise ValidationError(_("Validate order for one supplier at a time to send emails (mixed orders from %s and %s)") % (
                                            order['supplier'].display_name, line.supplier.display_name))
                order['supplier'] = line.supplier

                if order['company'] and line.order_id.company_id != order['company']:
                    raise ValidationError(_("Validate order for one company at a time to send emails (mixed orders from %s and %s)") % (
                                            order['company'].name, line.order_id.company_id.name))
                order['company'] = line.order_id.company_id

                if order['currency'] and line.currency_id != order['currency']:
                    raise ValidationError(_("Validate order for one currency at a time to send emails (mixed orders from %s and %s)") % (
                                            order['currency'].name, line.currency_id.name))
                order['currency'] = line.currency_id

                # group the order by products and note
                key = (line.product_id, line.note)
                group_lines.setdefault(key, 0)
                group_lines[key] += 1

            order['company_name'] = order['company'].name
            order['currency_id'] = order['currency'].id
            order['supplier_id'] = order['supplier'].id
            order['supplier_name'] = order['supplier'].name
            order['supplier_email'] = order['supplier'].email_formatted

            lines = []
            # sort by product name, note
            for product, note in sorted(group_lines, key=lambda k: (k[0].name, bool(k[1]))):
                quantity = group_lines[(product, note)]
                lines.append({
                    'product': product.name,
                    'note': note or '',
                    'quantity': quantity,
                    'price': product.price * quantity,
                })

            order['amount_total'] = sum(l['price'] for l in lines)

            template = self.env.ref('lunch.lunch_order_mail_supplier', raise_if_not_found=False)
            ctx = dict(
                default_composition_mode='comment',
                default_model='lunch.order',
                default_use_template=bool(template),
                default_template_id=template.id,
                default_lang=order['supplier'].lang or self.env.user.lang,
                order=order,
                lines=lines,
            )
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.compose.message',
                'target': 'new',
                'context': ctx,
            }

        else:
            raise AccessError(_("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(_("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))


class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'Lunch Product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
    active = fields.Boolean(default=True)
    available = fields.Boolean(compute='_get_available_product', search='_search_available_products')

    @api.depends('supplier')
    def _get_available_product(self):
        for product in self:
            if not product.supplier:
                product.available = True
            else:
                alerts = self.env['lunch.alert'].search([
                    ('partner_id', '=', self.supplier.id)
                ])
                if alerts and not any(alert.display for alert in alerts):
                    # every alert is not available
                    product.available = False
                else:
                    # no alert for the supplier or at least one is not available
                    product.available = True

    def _search_available_products(self, operator, value):
        alerts = self.env['lunch.alert'].search([])
        supplier_w_alerts = alerts.mapped('partner_id')
        available_suppliers = alerts.filtered(lambda a: a.display).mapped('partner_id')
        available_products = self.search([
            '|',
                ('supplier', 'not in', supplier_w_alerts.ids),
                ('supplier', 'in', available_suppliers.ids)
        ])

        if (operator in expression.NEGATIVE_TERM_OPERATORS and value) or \
           (operator not in expression.NEGATIVE_TERM_OPERATORS and not value):
            # e.g. (available = False) or (available != True)
            return [('id', 'not in', available_products.ids)]
        else:
            # e.g. (available = True) or (available != False)
            return [('id', 'in', available_products.ids)]


class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _description = 'Lunch Product Category'

    name = fields.Char('Product Category', required=True)


class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'Lunch Cashmove'

    user_id = fields.Many2one('res.users', 'User',
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, default=fields.Date.context_today)
    amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)')
    description = fields.Text('Description', help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment', default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]


class LunchAlert(models.Model):
    """ Alerts to display during a lunch order. An alert can be specific to a
    given day, weekly or daily. The alert is displayed from start to end hour. """
    _name = 'lunch.alert'
    _description = 'Lunch Alert'
    _rec_name = 'message'

    display = fields.Boolean(compute='_compute_display_get')
    message = fields.Text('Message', required=True)
    alert_type = fields.Selection([('specific', 'Specific Day'),
                                   ('week', 'Every Week'),
                                   ('days', 'Every Day')],
                                  string='Recurrence', required=True, index=True, default='specific')
    partner_id = fields.Many2one('res.partner', string="Vendor",
                                 help="If specified, the selected vendor can be ordered only on selected days")
    specific_day = fields.Date('Day', default=fields.Date.context_today)
    monday = fields.Boolean('Monday')
    tuesday = fields.Boolean('Tuesday')
    wednesday = fields.Boolean('Wednesday')
    thursday = fields.Boolean('Thursday')
    friday = fields.Boolean('Friday')
    saturday = fields.Boolean('Saturday')
    sunday = fields.Boolean('Sunday')
    start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
    end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
    active = fields.Boolean(default=True)

    @api.multi
    def name_get(self):
        return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]

    @api.depends('alert_type', 'specific_day', 'monday', 'tuesday', 'thursday',
                 'friday', 'saturday', 'sunday', 'start_hour', 'end_hour')
    def _compute_display_get(self):
        """
        This method check if the alert can be displayed today
        if alert type is specific : compare specific_day(date) with today's date
        if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
        if alert type is day : True
        return : Message if can_display_alert is True else False
        """
        days_codes = {'0': 'sunday',
                      '1': 'monday',
                      '2': 'tuesday',
                      '3': 'wednesday',
                      '4': 'thursday',
                      '5': 'friday',
                      '6': 'saturday'}
        fullday = False
        now = datetime.datetime.now()
        if self.env.context.get('lunch_date'):
            # lunch_date is a fields.Date -> 00:00:00
            lunch_date = fields.Datetime.from_string(self.env.context['lunch_date'])
            # if lunch_date is in the future, planned lunch, ignore hours
            fullday = lunch_date > now
            now = max(lunch_date, now)
        mynow = fields.Datetime.context_timestamp(self, now)

        for alert in self:
            can_display_alert = {
                'specific': (str(alert.specific_day) == fields.Date.to_string(mynow)),
                'week': alert[days_codes[mynow.strftime('%w')]],
                'days': True
            }

            if can_display_alert[alert.alert_type]:
                hour_to = int(alert.end_hour)
                min_to = int((alert.end_hour - hour_to) * 60)
                to_alert = datetime.time(hour_to, min_to)
                hour_from = int(alert.start_hour)
                min_from = int((alert.start_hour - hour_from) * 60)
                from_alert = datetime.time(hour_from, min_from)

                if fullday or (from_alert <= mynow.time() <= to_alert):
                    alert.display = True
                else:
                    alert.display = False
