# Fusion Accounting - 3-Way Matching
# Copyright (C) 2026 Nexa Systems Inc. (https://nexasystems.ca)
# Original implementation for the Fusion Accounting module.
#
# Compares Purchase Order amount, receipt quantity, and bill amount
# to flag discrepancies before payment approval.
# Soft dependency on the 'purchase' module.

import logging

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

_logger = logging.getLogger(__name__)


class FusionThreeWayMatch(models.Model):
    """Extends account.move with 3-way matching for vendor bills.

    The 3-way match compares:
      1. Purchase Order total
      2. Goods-received quantity (from stock.picking receipts)
      3. Vendor Bill amount

    The match status indicates whether the bill is safe to pay.
    Works only when the ``purchase`` module is installed.
    """

    _inherit = 'account.move'

    # =====================================================================
    # Fields
    # =====================================================================

    fusion_3way_match_status = fields.Selection(
        selection=[
            ('not_applicable', 'Not Applicable'),
            ('matched', 'Fully Matched'),
            ('partial', 'Partially Matched'),
            ('unmatched', 'Unmatched'),
        ],
        string="3-Way Match",
        compute='_compute_3way_match',
        store=True,
        readonly=True,
        copy=False,
        default='not_applicable',
        help="Indicates whether this vendor bill matches the corresponding "
             "Purchase Order and received goods quantities.\n"
             "- Fully Matched: PO, receipt, and bill all agree.\n"
             "- Partially Matched: some lines match but others don't.\n"
             "- Unmatched: significant discrepancies detected.\n"
             "- Not Applicable: not a vendor bill or no linked PO.",
    )
    fusion_po_ref = fields.Char(
        string="Purchase Order Ref",
        copy=False,
        help="Reference to the linked purchase order for 3-way matching.",
    )
    fusion_3way_po_amount = fields.Monetary(
        string="PO Amount",
        compute='_compute_3way_match',
        store=True,
        readonly=True,
        help="Total amount from the linked purchase order.",
    )
    fusion_3way_received_qty_value = fields.Monetary(
        string="Received Value",
        compute='_compute_3way_match',
        store=True,
        readonly=True,
        help="Monetary value of goods actually received.",
    )
    fusion_3way_bill_amount = fields.Monetary(
        string="Bill Amount",
        compute='_compute_3way_match',
        store=True,
        readonly=True,
        help="Total amount on this vendor bill.",
    )

    # =====================================================================
    # Purchase module availability check
    # =====================================================================

    @api.model
    def _fusion_purchase_module_installed(self):
        """Return True if the purchase module is installed."""
        return bool(self.env['ir.module.module'].sudo().search([
            ('name', '=', 'purchase'),
            ('state', '=', 'installed'),
        ], limit=1))

    # =====================================================================
    # Compute 3-Way Match
    # =====================================================================

    @api.depends(
        'move_type', 'state', 'amount_total',
        'invoice_line_ids.quantity', 'invoice_line_ids.price_subtotal',
        'fusion_po_ref',
    )
    def _compute_3way_match(self):
        """Compare PO amount, received quantity value, and bill amount.

        The tolerance for matching is set at the company currency's rounding
        precision.  Each line is evaluated individually and then the overall
        status is determined:

        - ``matched``:  all lines agree within tolerance
        - ``partial``:  some lines match, others don't
        - ``unmatched``: no lines match or totals diverge significantly
        - ``not_applicable``:  not a vendor bill or no linked PO
        """
        purchase_installed = self._fusion_purchase_module_installed()

        for move in self:
            # Defaults
            move.fusion_3way_po_amount = 0.0
            move.fusion_3way_received_qty_value = 0.0
            move.fusion_3way_bill_amount = 0.0

            # Only vendor bills
            if move.move_type not in ('in_invoice', 'in_refund'):
                move.fusion_3way_match_status = 'not_applicable'
                continue

            if not purchase_installed or not move.fusion_po_ref:
                # Try to auto-detect the PO from invoice origin
                if purchase_installed and move.invoice_origin:
                    po = self.env['purchase.order'].search([
                        ('name', '=', move.invoice_origin),
                        ('company_id', '=', move.company_id.id),
                    ], limit=1)
                    if po:
                        move.fusion_po_ref = po
                    else:
                        move.fusion_3way_match_status = 'not_applicable'
                        continue
                else:
                    move.fusion_3way_match_status = 'not_applicable'
                    continue

            po = move.fusion_po_ref
            currency = move.currency_id
            precision = currency.rounding

            # ---- Gather PO totals ----
            po_amount = po.amount_total
            move.fusion_3way_po_amount = po_amount

            # ---- Gather received quantities value ----
            received_value = 0.0
            for po_line in po.order_line:
                received_qty = po_line.qty_received
                received_value += received_qty * po_line.price_unit * (
                    1.0 - (po_line.discount or 0.0) / 100.0
                )
            move.fusion_3way_received_qty_value = received_value

            # ---- Gather bill totals ----
            bill_amount = move.amount_total
            move.fusion_3way_bill_amount = bill_amount

            # ---- Determine match status ----
            po_vs_bill = float_compare(po_amount, bill_amount, precision_rounding=precision)
            recv_vs_bill = float_compare(received_value, bill_amount, precision_rounding=precision)

            if po_vs_bill == 0 and recv_vs_bill == 0:
                # All three agree
                move.fusion_3way_match_status = 'matched'
            elif po_vs_bill == 0 or recv_vs_bill == 0:
                # Two of three agree
                move.fusion_3way_match_status = 'partial'
            else:
                # Per-line comparison for partial detection
                matched_lines = 0
                total_lines = 0
                for bill_line in move.invoice_line_ids.filtered(
                    lambda l: l.display_type == 'product' and l.product_id
                ):
                    total_lines += 1
                    po_line = po.order_line.filtered(
                        lambda pl: pl.product_id == bill_line.product_id
                    )
                    if po_line:
                        po_line = po_line[0]
                        # Compare quantity and price
                        qty_match = float_compare(
                            bill_line.quantity,
                            po_line.qty_received,
                            precision_rounding=0.01,
                        ) == 0
                        price_match = float_compare(
                            bill_line.price_unit,
                            po_line.price_unit,
                            precision_rounding=precision,
                        ) == 0
                        if qty_match and price_match:
                            matched_lines += 1

                if total_lines == 0:
                    move.fusion_3way_match_status = 'unmatched'
                elif matched_lines == total_lines:
                    move.fusion_3way_match_status = 'matched'
                elif matched_lines > 0:
                    move.fusion_3way_match_status = 'partial'
                else:
                    move.fusion_3way_match_status = 'unmatched'

    # =====================================================================
    # Actions
    # =====================================================================

    def action_view_purchase_order(self):
        """Open the linked purchase order."""
        self.ensure_one()
        if not self.fusion_po_ref:
            return
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'purchase.order',
            'res_id': self.fusion_po_ref.id,
            'view_mode': 'form',
            'target': 'current',
        }

    def action_refresh_3way_match(self):
        """Manually re-compute the 3-way match status."""
        self._compute_3way_match()
