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

from collections import defaultdict, OrderedDict

from odoo import _, api, models
from odoo.tools import float_compare, float_is_zero, format_date


class ReceptionReport(models.AbstractModel):
    _name = 'report.stock.report_reception'
    _description = "Stock Reception Report"

    @api.model
    def get_report_data(self, docids, data):
        report_values = self._get_report_values(docids, data)
        report_values['docs'] = self._format_html_docs(report_values.get('docs', False))
        report_values['sources_info'] = self._format_html_sources_info(report_values.get('sources_to_lines', {}))
        report_values['sources_to_lines'] = self._format_html_sources_to_lines(report_values.get('sources_to_lines', {}))
        report_values['sources_to_formatted_scheduled_date'] = self._format_html_sources_to_date(report_values.get('sources_to_formatted_scheduled_date', {}))
        report_values['show_uom'] = self.env.user.has_group('uom.group_uom')
        return report_values

    @api.model
    def _get_report_values(self, docids, data=None):
        ''' This report is flexibly designed to work with both individual and batch pickings.
        '''
        docs = self._get_docs(docids)
        doc_states = docs.mapped('state')
        # unsupported cases
        doc_types = self._get_doc_types()
        if not docs:
            msg = _("No %s selected or a delivery order selected", doc_types)
        elif 'done' in doc_states and len(set(doc_states)) > 1:
            docs = False
            msg = _("This report cannot be used for done and not done %s at the same time", doc_types)
        if not docs:
            return {'pickings': False, 'reason': msg}

        # incoming move qtys
        product_to_qty_draft = defaultdict(float)
        product_to_qty_to_assign = defaultdict(list)
        product_to_total_assigned = defaultdict(lambda: [0.0, []])

        # to support batch pickings we need to track the total already assigned
        move_ids = self._get_moves(docs)
        assigned_moves = move_ids.mapped('move_dest_ids')
        product_to_assigned_qty = defaultdict(float)
        for assigned in assigned_moves:
            product_to_assigned_qty[assigned.product_id] += assigned.product_qty

        for move in move_ids:
            move_quantity = (
                move.product_qty or
                move.product_uom._compute_quantity(move.quantity, move.product_id.uom_id, rounding_method='HALF-UP')
            )
            qty_already_assigned = 0
            if move.move_dest_ids:
                qty_already_assigned = min(product_to_assigned_qty[move.product_id], move_quantity)
                product_to_assigned_qty[move.product_id] -= qty_already_assigned
            if qty_already_assigned:
                product_to_total_assigned[move.product_id][0] += qty_already_assigned
                product_to_total_assigned[move.product_id][1].append(move.id)
            if move_quantity != qty_already_assigned:
                if move.state == 'draft':
                    product_to_qty_draft[move.product_id] += move_quantity - qty_already_assigned
                else:
                    quantity_to_assign = move_quantity
                    product_to_qty_to_assign[move.product_id].append((quantity_to_assign - qty_already_assigned, move))

        # only match for non-mto moves in same warehouse
        warehouse = docs[0].picking_type_id.warehouse_id
        wh_location_ids = self.env['stock.location']._search([('id', 'child_of', warehouse.view_location_id.id), ('usage', '!=', 'supplier')])

        allowed_states = ['confirmed', 'partially_available', 'waiting']
        if 'done' in doc_states:
            # only done moves are allowed to be assigned to already reserved moves
            allowed_states += ['assigned']

        outs = self.env['stock.move'].search(
            [
                ('state', 'in', allowed_states),
                ('product_qty', '>', 0),
                ('location_id', 'in', wh_location_ids),
                ('move_orig_ids', '=', False),
                ('product_id', 'in',
                    [p.id for p in list(product_to_qty_to_assign.keys()) + list(product_to_qty_draft.keys())]),
            ] + self._get_extra_domain(docs),
            order='reservation_date, priority desc, date, id')

        products_to_outs = defaultdict(list)
        for out in outs:
            products_to_outs[out.product_id].append(out)

        sources_to_lines = defaultdict(list)  # group by source so we can print them together
        # show potential moves that can be assigned
        for product_id, outs in products_to_outs.items():
            for out in outs:
                # we expect len(source) = 2 when picking + origin [e.g. SO] and len() = 1 otherwise [e.g. MO]
                source = (out._get_source_document(),)
                if not source:
                    continue
                if out.picking_id and source[0] != out.picking_id:
                    source = (out.picking_id, source[0])

                qty_to_reserve = out.product_qty
                product_uom = out.product_id.uom_id
                if 'done' not in doc_states and out.state == 'partially_available':
                    qty_to_reserve -= out.product_uom._compute_quantity(out.quantity, product_uom)
                moves_in_ids = []
                quantity = 0
                for move_in_qty, move_in in product_to_qty_to_assign[out.product_id]:
                    moves_in_ids.append(move_in.id)
                    if float_compare(quantity + move_in_qty, qty_to_reserve, precision_rounding=product_uom.rounding) <= 0:
                        qty_to_add = move_in_qty
                        move_in_qty = 0
                    else:
                        qty_to_add = qty_to_reserve - quantity
                        move_in_qty -= qty_to_add
                    quantity += qty_to_add
                    if move_in_qty:
                        product_to_qty_to_assign[out.product_id][0] = (move_in_qty, move_in)
                    else:
                        product_to_qty_to_assign[out.product_id] = product_to_qty_to_assign[out.product_id][1:]
                    if float_compare(qty_to_reserve, quantity, precision_rounding=product_uom.rounding) == 0:
                        break

                if not float_is_zero(quantity, precision_rounding=product_uom.rounding):
                    sources_to_lines[source].append(self._prepare_report_line(quantity, product_id, out, source[0], move_ins=self.env['stock.move'].browse(moves_in_ids)))

                # draft qtys can be shown but not assigned
                qty_expected = product_to_qty_draft.get(product_id, 0)
                if float_compare(qty_to_reserve, quantity, precision_rounding=product_uom.rounding) > 0 and\
                        not float_is_zero(qty_expected, precision_rounding=product_uom.rounding):
                    to_expect = min(qty_expected, qty_to_reserve - quantity)
                    sources_to_lines[source].append(self._prepare_report_line(to_expect, product_id, out, source[0], is_qty_assignable=False))
                    product_to_qty_draft[product_id] -= to_expect

        # show already assigned moves
        for product_id, qty_and_ins in product_to_total_assigned.items():
            total_assigned = qty_and_ins[0]
            moves_in = self.env['stock.move'].browse(qty_and_ins[1])
            out_moves = moves_in.move_dest_ids

            for out_move in out_moves:
                if float_is_zero(total_assigned, precision_rounding=out_move.product_id.uom_id.rounding):
                    # it is possible there are different in moves linked to the same out moves due to batch
                    # => we guess as to which outs correspond to this report...
                    continue
                source = (out_move._get_source_document(),)
                if not source:
                    continue
                if out_move.picking_id and source[0] != out_move.picking_id:
                    source = (out_move.picking_id, source[0])
                qty_assigned = min(total_assigned, out_move.product_qty)
                sources_to_lines[source].append(
                    self._prepare_report_line(qty_assigned, product_id, out_move, source[0], is_assigned=True, move_ins=moves_in))

        # dates aren't auto-formatted when printed in report :(
        sources_to_formatted_scheduled_date = defaultdict(list)
        for source, dummy in sources_to_lines.items():
            sources_to_formatted_scheduled_date[source] = self._get_formatted_scheduled_date(source[0])

        return {
            'data': data,
            'doc_ids': docids,
            'doc_model': self._get_doc_model(),
            'sources_to_lines': sources_to_lines,
            'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure'),
            'docs': docs,
            'sources_to_formatted_scheduled_date': sources_to_formatted_scheduled_date,
        }

    def _prepare_report_line(self, quantity, product, move_out, source=False, is_assigned=False, is_qty_assignable=True, move_ins=False):
        return {
            'source': source,
            'product': {
                'id': product.id,
                'display_name': product.display_name
            },
            'uom': product.uom_id.display_name,
            'quantity': quantity,
            'is_qty_assignable': is_qty_assignable,
            'move_out': move_out,
            'is_assigned': is_assigned,
            'move_ins': move_ins and move_ins.ids or False,
        }

    def _get_docs(self, docids):
        docids = self.env.context.get('default_picking_ids', docids)
        return self.env['stock.picking'].search([('id', 'in', docids), ('picking_type_code', '!=', 'outgoing'), ('state', '!=', 'cancel')])

    def _get_doc_model(self):
        return 'stock.picking'

    def _get_doc_types(self):
        return "transfers"

    def _get_moves(self, docs):
        return docs.move_ids.filtered(lambda m: m.product_id.is_storable and m.state != 'cancel')

    def _get_extra_domain(self, docs):
        return [('picking_id', 'not in', docs.ids)]

    def _get_formatted_scheduled_date(self, source):
        """ Unfortunately different source record types have different field names for their "Scheduled Date"
        Therefore an extendable method is needed.
        """
        if source._name == 'stock.picking':
            return format_date(self.env, source.scheduled_date)
        return False

    def action_assign(self, move_ids, qtys, in_ids):
        """ Assign picking move(s) [i.e. link] to other moves (i.e. make them MTO)
        :param move_id ids: the ids of the moves to make MTO
        :param qtys list: the quantities that are being assigned to the move_ids (in same order as move_ids)
        :param in_ids ids: the ids of the moves that are to be assigned to move_ids
        """
        outs = self.env['stock.move'].browse(move_ids)
        # Split outs with only part of demand assigned to prevent reservation problems later on.
        # We do this first so we can create their split moves in batch
        out_to_new_out = OrderedDict()
        new_move_vals = []
        for out, qty_to_link in zip(outs, qtys):
            if float_compare(out.product_qty, qty_to_link, precision_rounding=out.product_id.uom_id.rounding) == 1:
                new_move = out._split(out.product_qty - qty_to_link)
                if new_move:
                    new_move[0]['reservation_date'] = out.reservation_date
                new_move_vals += new_move
                out_to_new_out[out.id] = self.env['stock.move']
        new_outs = self.env['stock.move'].create(new_move_vals)
        # don't do action confirm to avoid creating additional unintentional reservations
        new_outs.write({'state': 'confirmed'})
        for i, k in enumerate(out_to_new_out.keys()):
            out_to_new_out[k] = new_outs[i]

        for out, qty_to_link, ins in zip(outs, qtys, in_ids):
            potential_ins = self.env['stock.move'].browse(ins)
            if out.id in out_to_new_out:
                new_out = out_to_new_out[out.id]
                if potential_ins[0].state != 'done' and out.quantity:
                    # let's assume if 1 of the potential_ins isn't done, then none of them are => we are only assigning the not-reserved
                    # qty and the new move should have all existing reserved quants (i.e. move lines) assigned to it
                    out.move_line_ids.move_id = new_out
                elif potential_ins[0].state == 'done' and out.quantity > qty_to_link:
                    # let's assume if 1 of the potential_ins is done, then all of them are => we can link them to already reserved moves, but we
                    # need to make sure the reserved qtys still match the demand amount the move (we're assigning).
                    out.move_line_ids.move_id = new_out
                    assigned_amount = 0
                    for move_line_id in new_out.move_line_ids:
                        if assigned_amount + move_line_id.quantity_product_uom > qty_to_link:
                            new_move_line = move_line_id.copy({'quantity': 0})
                            new_move_line.quantity = move_line_id.quantity
                            move_line_id.quantity = out.product_id.uom_id._compute_quantity(qty_to_link - assigned_amount, out.product_uom, rounding_method='HALF-UP')
                            new_move_line.quantity -= out.product_id.uom_id._compute_quantity(move_line_id.quantity_product_uom, out.product_uom, rounding_method='HALF-UP')
                        move_line_id.move_id = out
                        assigned_amount += move_line_id.quantity_product_uom
                        if float_compare(assigned_amount, qty_to_link, precision_rounding=out.product_id.uom_id.rounding) == 0:
                            break

            for in_move in reversed(potential_ins):
                move_quantity = in_move.product_qty or in_move.product_uom._compute_quantity(in_move.quantity, in_move.product_id.uom_id, rounding_method='HALF-UP')
                quantity_remaining = move_quantity - sum(in_move.move_dest_ids.mapped('product_qty'))
                if in_move.product_id != out.product_id or float_compare(0, quantity_remaining, precision_rounding=in_move.product_id.uom_id.rounding) >= 0:
                    # in move is already completely linked (e.g. during another assign click) => don't count it again
                    potential_ins = potential_ins[1:]
                    continue

                linked_qty = min(move_quantity, qty_to_link)
                in_move.move_dest_ids |= out
                self._action_assign(in_move, out)
                out.procure_method = 'make_to_order'
                quantity_remaining -= linked_qty
                qty_to_link -= linked_qty
                if float_is_zero(qty_to_link, precision_rounding=out.product_id.uom_id.rounding):
                    break  # we have satistfied the qty_to_link

        (outs | new_outs)._recompute_state()

        # always try to auto-assign to prevent another move from reserving the quant if incoming move is done
        self.env['stock.move'].browse(move_ids)._action_assign()

    def action_unassign(self, move_id, qty, in_ids):
        """ Unassign moves [i.e. unlink] from a move (i.e. make non-MTO)
        :param move_id id: the id of the move to make non-MTO
        :param qty float: the total quantity that is being unassigned from move_id
        :param in_ids ids: the ids of the moves that are to be unassigned from move_id
        """
        out = self.env['stock.move'].browse(move_id)
        ins = self.env['stock.move'].browse(in_ids)

        amount_unassigned = 0
        for in_move in ins:
            if out.id not in in_move.move_dest_ids.ids:
                continue
            move_quantity = in_move.product_qty or in_move.product_uom._compute_quantity(in_move.quantity, in_move.product_id.uom_id, rounding_method='HALF-UP')
            in_move.move_dest_ids -= out
            self._action_unassign(in_move, out)
            amount_unassigned += min(qty, move_quantity)
            if float_compare(qty, amount_unassigned, precision_rounding=out.product_id.uom_id.rounding) <= 0:
                break
        if out.move_orig_ids and out.state != 'done':
            # annoying use cases where we need to split the out move:
            # 1. batch reserved + individual picking unreserved
            # 2. moves linked from backorder generation
            total_still_linked = sum(out.move_orig_ids.mapped('product_qty'))
            new_move_vals = out._split(total_still_linked)
            if new_move_vals:
                new_move_vals[0]['procure_method'] = 'make_to_order'
                new_move_vals[0]['reservation_date'] = out.reservation_date
                new_out = self.env['stock.move'].create(new_move_vals)
                # don't do action confirm to avoid creating additional unintentional reservations
                new_out.write({'state': 'confirmed'})
                out.move_line_ids.move_id = new_out
                (out | new_out)._compute_quantity()
                if new_out.quantity > new_out.product_qty:
                    # extra reserved amount goes to no longer linked out
                    reserved_amount_to_remain = new_out.quantity - new_out.product_qty
                    for move_line_id in new_out.move_line_ids:
                        if reserved_amount_to_remain <= 0:
                            break
                        if move_line_id.quantity_product_uom > reserved_amount_to_remain:
                            new_move_line = move_line_id.copy({'quantity': 0})
                            new_move_line.quantity = out.product_id.uom_id._compute_quantity(move_line_id.quantity_product_uom - reserved_amount_to_remain, move_line_id.product_uom_id, rounding_method='HALF-UP')
                            move_line_id.quantity -= new_move_line.quantity
                            move_line_id.move_id = out
                            break
                        else:
                            move_line_id.move_id = out
                            reserved_amount_to_remain -= move_line_id.quantity_product_uom
                    (out | new_out)._compute_quantity()
                out.move_orig_ids = False
                new_out._recompute_state()
        out.procure_method = 'make_to_stock'
        out._do_unreserve()
        return True

    def _action_assign(self, in_move, out_move):
        """ For extension purposes only """
        return

    def _action_unassign(self, in_move, out_move):
        """ For extension purposes only """
        return

    def _format_html_docs(self, docs):
        """ Format docs to be sent in an html request. """
        return [{
            'id': doc.id,
            'name': doc.display_name,
            'state': doc.state,
            'display_state': dict(doc._fields['state']._description_selection(self.env)).get(doc.state),
        } for doc in docs] if docs else docs

    def _format_html_sources_to_date(self, sources_to_dates):
        """ Format sources_to_formatted_scheduled_date to be sent in an html request. """
        return {str(source): date for (source, date) in sources_to_dates.items()}

    def _format_html_sources_to_lines(self, sources_to_lines):
        """ Format sources_to_lines to be sent in an html request, while adding an index for OWL's t-foreach. """
        return {
            str(source): [{**line, 'index': i, 'move_out_id': line['move_out'].id} for i, line in enumerate(lines)]
            for source, lines in sources_to_lines.items()
        }

    def _format_html_sources_info(self, sources_to_lines):
        """ Format used info from sources of sources_to_lines to be sent in an html request. """
        return {str(source): [self._format_html_source(s, s._name == 'stock.picking')for s in source] for source in sources_to_lines.keys()}

    def _format_html_source(self, source, is_picking=False):
        """ Format used info from a single source to be sent in an html request. """
        formatted = {
            'id': source.id,
            'model': source._name,
            'name': source.display_name,
        }
        if is_picking:
            formatted.update({
                'priority': source.priority,
                'partner_id': source.partner_id.id if source.partner_id else False,
                'partner_name': source.partner_id.name if source.partner_id else False,
            })
        return formatted
