from odoo import api, fields, models
import math
from datetime import timedelta, datetime, time


class PurchaseOrderRecommendationSupermarketWizard(models.TransientModel):
    _inherit = "purchase.order.recommendation"

    order_days = fields.Integer(
        string="Order coverage (days)",
        help="Indicate for how many days the new order should cover the stock. If not set, the default module behavior is kept.",
    )
    order_by_packages = fields.Boolean(
        string="Order by packages",
        default=False,
        help="Indicates if the order should be made by packages.",
    )
    ignore_zero_stock_days = fields.Boolean(
        string="Ignore days with zero stock",
        default=False,
        help="If enabled, days when the product stock was 0 or less will not be considered in the daily sales calculation.",
    )
    include_previous_period = fields.Boolean(
        string="Include previous period stats",
        default=False,
        help="If disabled, the previous period (same number of days immediately preceding) is NOT queried. This saves a read_group on stock move lines and skips zero-stock day analysis for that window.",
    )
    filter_by_main_seller = fields.Boolean(
        string="Only main vendor products",
        default=False,
        help="If enabled, only products whose main vendor matches the purchase order vendor will be recommended.",
    )
    total_days = fields.Integer(
        string="Total days",
        compute="_compute_total_days",
        readonly=True,
        help="Total number of days between the start and end dates of the recommendation.",
    )
    avg_days_between_orders = fields.Float(
        string="Average days between orders",
        compute="_compute_avg_days_between_orders",
        help="Average number of days between orders for this vendor.",
        readonly=True,
    )
    date_last_order = fields.Date(
        string="Date of last order",
        compute="_compute_avg_days_between_orders",
        help="Date of the last order received from the vendor.",
        readonly=True,
    )
    currency_id = fields.Many2one(
        related="order_id.currency_id",
        readonly=True,
    )
    order_total_amount = fields.Monetary(
        string="Order total amount",
        currency_field="currency_id",
        compute="_compute_order_total_amount",
        help="Order total amount without taxes",
        readonly=True,
    )
    line_amount = fields.Integer(
        default=0,
    )
    last_order_total_amount = fields.Monetary(
        string="Last Order total amount",
        currency_field="currency_id",
        compute="_compute_avg_days_between_orders",
        help="Last Order total amount without taxes",
        readonly=True,
    )

    # Inherit fields to add help
    price_unit = fields.Monetary(
        help="Purchase price without discount and without VAT for this vendor."
    )
    units_available = fields.Float(
        help="Current stock available in the warehouse.",
    )
    units_virtual_available = fields.Float(
        help="Forecasted stock, considering incoming and outgoing stock.",
    )
    units_received = fields.Float(
        help="Quantity of products already received for this order.",
    )
    units_delivered = fields.Float(
        help="Quantity of products delivered to customers during the selected period.",
    )
    units_avg_delivered = fields.Float(
        help="Average daily consumption during the selected period.",
    )
    units_included = fields.Float(
        help="Quantity to order to have stock for the entire defined period.",
    )

    @api.depends("date_begin", "date_end")
    def _compute_total_days(self):
        for rec in self:
            if rec.date_begin and rec.date_end:
                rec.total_days = self._get_total_days()
            else:
                rec.total_days = 0

    @api.depends("order_id")
    def _compute_avg_days_between_orders(self):
        if not self.order_id.partner_id:
            self.avg_days_between_orders = 0.0
            return

        orders = self.env["purchase.order"].search(
            [
                ("partner_id", "=", self.order_id.partner_id.id),
                ("state", "in", ["purchase", "done"]),
            ],
            order="date_order asc",
        )
        if orders:
            self.date_last_order = (
                orders[-1].effective_date.date()
                if orders[-1].effective_date
                else orders[-1].date_planned.date()
            )
            self.last_order_total_amount = orders[-1].amount_total
        else:
            self.date_last_order = False
            self.last_order_total_amount = 0.0

        dates = [o.date_order.date() for o in orders if o.date_order]
        if len(dates) < 2:
            self.avg_days_between_orders = 0.0
            return

        day_diffs = [
            (dates[i + 1] - dates[i]).days
            for i in range(len(dates) - 1)
            if (dates[i + 1] - dates[i]).days > 0
        ]

        if not day_diffs:
            self.avg_days_between_orders = 0.0
        else:
            self.avg_days_between_orders = sum(day_diffs) / len(day_diffs)

    @api.depends("line_ids")
    def _compute_order_total_amount(self):
        """Compute the total amount of the order."""
        if not self.order_id:
            self.order_total_amount = 0.0
            return
        total_amount = sum(line.subtotal_amount for line in self.line_ids)
        self.order_total_amount = total_amount

    # Override the method to remove onchange and recalculate only by clicking the button
    def _generate_recommendations(self):
        super()._generate_recommendations()

    def regenerate_recommendations(self):
        self._generate_recommendations()
        return {
            "type": "ir.actions.act_window",
            "res_model": self._name,
            "res_id": self.id,
            "view_mode": "form",
            "target": "new",
        }

    def _prepare_wizard_line(self, vals, order_line=False):
        """Used to create the wizard line"""
        res = super()._prepare_wizard_line(vals, order_line=order_line)
        product_id = order_line and order_line.product_id or vals["product_id"]
        qty_to_order = res["units_included"]

        if self.ignore_zero_stock_days:
            days_with_stock = self._get_total_days() - self._get_days_out_of_stock(
                product_id
            )
            res["units_avg_delivered"] = (
                vals.get("qty_delivered", 0) / days_with_stock
                if days_with_stock != 0
                else 1
            )
        uom_categ_unit = self.env.ref(
            "uom.product_uom_categ_unit", raise_if_not_found=False
        )
        if self.order_days != 0:
            qty_calc = (self.order_days * res["units_avg_delivered"]) - res[
                "units_virtual_available"
            ]
            qty_to_order = max(0, qty_calc)
            # Round only if the UoM category is 'uom.product_uom_categ_unit'
            if (
                product_id.uom_po_id
                and uom_categ_unit
                and product_id.uom_po_id.category_id.id == uom_categ_unit.id
            ):
                qty_to_order = math.ceil(qty_to_order)

        # Force a minimum suggested quantity when forecast <= 0 and product configured
        # We apply this BEFORE packaging adjustment so packages logic can upscale it.
        if (
            product_id.po_min_suggest_if_forecast_le_zero
            and res["units_virtual_available"] <= 0
        ):
            # If ordering by packages we later bump to one full package.
            qty_to_order = max(qty_to_order, 1)
            res["is_modified"] = True
        res["units_included_original"] = qty_to_order

        # Adjust qty_to_order to packaging multiples if order_by_packages is checked
        if self.order_by_packages and product_id.packaging_ids:
            packaging_qty = product_id.packaging_ids[:1].qty
            if packaging_qty:
                qty_to_order = math.ceil(qty_to_order / packaging_qty) * packaging_qty
        # Round only if the UoM category is 'uom.product_uom_categ_unit'
        if (
            product_id.uom_po_id
            and uom_categ_unit
            and product_id.uom_po_id.category_id.id == uom_categ_unit.id
        ):
            res["units_included"] = math.ceil(qty_to_order)
        else:
            res["units_included"] = qty_to_order

        # Get quantities scrapped
        domain = self._get_move_line_domain(product_id, src="internal", dst="inventory")
        found_scrapped = self.env["stock.move.line"].read_group(
            domain, ["product_id", "qty_done"], ["product_id"]
        )
        if len(found_scrapped):
            res["units_scrapped"] = found_scrapped[0]["qty_done"]

        if self.include_previous_period:
            days = self._get_total_days()
            prev_date_end = self.date_begin - timedelta(days=1)
            prev_date_begin = self.date_begin - timedelta(days=days)
            domain = self.with_context(
                {
                    "period_date_begin": prev_date_begin,
                    "period_date_end": prev_date_end,
                }
            )._get_move_line_domain(product_id, src="internal", dst="customer")
            found_previous_period = self.env["stock.move.line"].read_group(
                domain, ["product_id", "qty_done"], ["product_id"]
            )
            if len(found_previous_period):
                res["units_delivered_prev"] = found_previous_period[0]["qty_done"]
                if self.ignore_zero_stock_days:
                    days_with_stock = days - self.with_context(
                        {
                            "period_date_begin": prev_date_begin,
                            "period_date_end": prev_date_end,
                        }
                    )._get_days_out_of_stock(product_id)
                    res["units_avg_delivered_prev"] = (
                        res["units_delivered_prev"] / days_with_stock
                        if days_with_stock != 0
                        else 1
                    )
                else:
                    res["units_avg_delivered_prev"] = (
                        found_previous_period[0]["qty_done"] / days if days != 0 else 1
                    )
        seller = product_id._select_seller(
            partner_id=self.order_id.partner_id,
            date=fields.Date.today(),
            quantity=qty_to_order,
            uom_id=product_id.uom_po_id,
        )
        res.update(self._prepare_wizard_line_from_seller(res, seller))
        return res

    def action_accept(self):
        super().action_accept()
        for line in self.order_id.order_line:
            if line.product_qty <= 0:
                line.unlink()

    def _get_days_out_of_stock(self, product):
        """Compute days without stock using precomputed periods (product.stock.out.period).

        A day is counted if it belongs to any stored stock-out period overlapping
        the target window [date_from, date_to]. The period model stores end_date daily
        (or closure day).
        """
        date_from = self.env.context.get("period_date_begin", self.date_begin)
        date_to = self.env.context.get("period_date_end", self.date_end)
        if not date_from or not date_to:
            return 0
        # Access template periods (product may be product.product or product.template)
        product_tmpl = (
            product.product_tmpl_id if hasattr(product, "product_tmpl_id") else product
        )
        periods = product_tmpl.stock_out_period_ids
        total = 0
        for p in periods:
            if not p.start_date or not p.end_date:
                continue
            # Overlap window
            start = max(p.start_date, date_from)
            end = min(p.end_date, date_to)
            if start <= end:
                total += (end - start).days
        return total

    def _get_products(self):
        """Overwrite because filter by cateogory is not working in the base method."""
        products = self._get_supplier_products()
        # Filter products by category if set.
        # It will apply to show_all_partner_products as well
        if self.product_category_ids:
            products = products.filtered(
                lambda x: x.categ_id.id in self.product_category_ids.ids
            )
        if self.filter_by_main_seller:
            partner = self.order_id.partner_id.commercial_partner_id
            products = products.filtered(
                lambda x: x.product_tmpl_id.main_seller_id == partner
            )
        return products


class PurchaseOrderRecommendationLine(models.TransientModel):
    _inherit = "purchase.order.recommendation.line"

    include_previous_period = fields.Boolean(
        related="wizard_id.include_previous_period", store=False, readonly=True
    )

    packaging_id = fields.Many2one(
        comodel_name="product.packaging",
        string="Packaging",
        help="Packaging configured for this product.",
        compute="_compute_first_packaging_id",
        readonly=True,
    )
    packaging_qty = fields.Integer(
        string="Packaging Quantity",
        help="Quantity of packages to order for this product.",
        compute="_compute_packaging_qty",
        inverse="_inverse_packaging_qty",
        store=True,
        readonly=False,
    )
    packaging_contained_qty = fields.Float(
        string="Packaging Quantity Contained",
        help="Quantity contained in the selected packaging for this product.",
        related="packaging_id.qty",
        readonly=True,
    )
    days_without_stock = fields.Integer(
        string="Days without stock",
        compute="_compute_days_without_stock",
        help="Number of days in the selected period in which the on-hand quantity was 0.",
        readonly=True,
    )
    units_scrapped = fields.Float(
        readonly=True, help="Number of units recorded as scrap during the period"
    )
    subtotal_amount = fields.Monetary(
        string="Subtotal Amount",
        compute="_compute_subtotal_amount",
        help="Order subtotal per line.",
        readonly=True,
    )
    units_included_original = fields.Float(
        string="Units Included Original",
        help="Quantity to order to have stock for the entire defined period.",
        readonly=True,
    )
    stock_duration = fields.Float(
        string="Stock Duration",
        compute="_compute_stock_duration",
        help="Number of days the current stock should last.",
        readonly=True,
    )
    units_delivered_prev = fields.Float(
        help="Quantity of products sold during the previous period. The 'previous period' is defined as the same number of days (Total days) immediately preceding.",
        readonly=True,
    )
    units_avg_delivered_prev = fields.Float(
        digits="Product Unit of Measure",
        help="Average daily consumption during the previous period",
        readonly=True,
    )

    # Inherit fields to add help
    product_id = fields.Many2one(
        help="Product to be purchased from the vendor.",
    )
    product_name = fields.Char(
        help="Name of the product to be purchased from the vendor.",
    )
    product_code = fields.Char(
        help="Internal reference of the product to be purchased from the vendor.",
    )
    price_unit = fields.Monetary(
        help="Purchase price without discount and without VAT for this vendor."
    )
    units_available = fields.Float(
        help="Current stock available in the warehouse.",
    )
    units_virtual_available = fields.Float(
        help="Forecasted stock, considering incoming and outgoing stock.",
    )
    units_received = fields.Float(
        help="Quantity of products already received for this order.",
    )
    units_delivered = fields.Float(
        help="Quantity of products delivered to customers during the selected period.",
    )
    units_avg_delivered = fields.Float(
        help="Average daily consumption during the selected period.",
    )
    units_included = fields.Float(
        help="Quantity to be transferred to the order. This field can be modified manually.",
    )

    @api.depends("units_included", "product_id")
    def _compute_subtotal_amount(self):
        for rec in self:
            if rec.product_id and rec.units_included:
                rec.subtotal_amount = rec.units_included * rec.price_unit
            else:
                rec.subtotal_amount = 0.0

    @api.depends("wizard_id.date_begin", "wizard_id.date_end")
    def _compute_days_without_stock(self):
        for rec in self:
            if rec.product_id:
                rec.days_without_stock = rec.wizard_id._get_days_out_of_stock(
                    rec.product_id
                )
            else:
                rec.days_without_stock = 0

    @api.onchange("packaging_qty")
    def _inverse_packaging_qty(self):
        for rec in self:
            if rec.packaging_id and rec.packaging_id.qty:
                rec.units_included = rec.packaging_qty * rec.packaging_id.qty

    @api.depends("packaging_id", "units_included")
    def _compute_packaging_qty(self):
        for rec in self:
            if rec.packaging_id and rec.packaging_id.qty:
                rec.packaging_qty = math.ceil(rec.units_included / rec.packaging_id.qty)
            else:
                rec.packaging_qty = 0

    @api.depends("product_id")
    def _compute_first_packaging_id(self):
        for rec in self:
            rec.packaging_id = (
                rec.product_id.packaging_ids[:1].id
                if rec.product_id and rec.product_id.packaging_ids
                else False
            )

    @api.depends("units_avg_delivered")
    def _compute_stock_duration(self):
        for rec in self:
            rec.stock_duration = (
                rec.units_virtual_available / rec.units_avg_delivered
                if rec.units_avg_delivered > 0
                else 0.0
            )
