import logging

from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import float_compare

_logger = logging.getLogger(__name__)


class AccountMove(models.Model):
    """Inherit to implement the tax calculation using avatax API"""

    _inherit = "account.move"

    @api.depends("partner_shipping_id", "partner_id", "company_id")
    def _compute_onchange_exemption(self):
        """
        Set the exemption to use for the Invoice.
        An exemption can be applied if
        there an exemption for the delivery address Country + State
        - Get the delivery address Country & State
        - Search the invoiced commercial partner addresses
          for an exemption in this Country & State
        - In case there is a "country wide" exemption, use it.

        Example:
        - ACME company partner, USA CA, has exemption status
        - ACME Invoicing address, USA CA, no exemption status
        - ACME Delivery address, USA CA, no exemption status

        Invoice to ACME Invoicing, Shipped to ACME Delivery with be exempt.

        For this to work properly, the "exemption_lock" is no longer supported.
        """
        for invoice in self.filtered(lambda x: x.state == "draft"):
            invoice_partner = invoice.partner_id.commercial_partner_id
            ship_to_address = (
                hasattr(invoice, "partner_shipping_id")
                and invoice.partner_shipping_id
                or invoice_partner
            )
            # Find an exemption address matching the Country + State
            # of the Delivery address
            exemption_addresses = (
                invoice_partner | invoice_partner.child_ids
            ).filtered("property_tax_exempt")
            exemption_address_naive = exemption_addresses.filtered(
                lambda a: a.country_id == ship_to_address.country_id
                and (
                    a.state_id == ship_to_address.state_id
                    or invoice_partner.property_exemption_country_wide
                )
            )[:1]
            # Force Company to get the correct values from the Property fields
            exemption_address = exemption_address_naive.with_company(
                invoice.company_id.id
            )
            invoice.exemption_code = exemption_address.property_exemption_number
            invoice.exemption_code_id = exemption_address.property_exemption_code_id

    @api.onchange("warehouse_id")
    def onchange_warehouse_id(self):
        if self.warehouse_id:
            if self.warehouse_id.company_id:
                self.company_id = self.warehouse_id.company_id
            if self.warehouse_id.code:
                self.location_code = self.warehouse_id.code

    is_avatax = fields.Boolean(related="fiscal_position_id.is_avatax")
    invoice_doc_no = fields.Char(
        "Source/Ref Invoice No",
        readonly=True,
        states={"draft": [("readonly", False)]},
        help="Reference of the invoice",
    )
    exemption_code = fields.Char(
        "Exemption Number",
        compute=_compute_onchange_exemption,
        readonly=False,  # New computed writeable fields
        store=True,
        help="It show the customer exemption number",
    )
    exemption_code_id = fields.Many2one(
        "exemption.code",
        "Exemption Code",
        compute=_compute_onchange_exemption,
        readonly=False,  # New computed writeable fields
        store=True,
        help="It show the customer exemption code",
    )
    exemption_locked = fields.Boolean(
        help="Exemption code won't be automatically changed, "
        "for instance, when changing the Customer."
    )
    tax_on_shipping_address = fields.Boolean(
        "Tax based on shipping address", default=True
    )
    tax_address_id = fields.Many2one(
        "res.partner", "Tax Shipping Address", compute="_compute_tax_address_id"
    )
    location_code = fields.Char(readonly=True, states={"draft": [("readonly", False)]})
    warehouse_id = fields.Many2one("stock.warehouse", "Warehouse")
    avatax_amount = fields.Float(string="AvaTax", copy=False)
    calculate_tax_on_save = fields.Boolean()
    so_partner_id = fields.Many2one(comodel_name="res.partner", string="SO Partner")
    avatax_request_log = fields.Text(
        "Avatax API Request Log", readonly=True, copy=False
    )
    avatax_response_log = fields.Text(
        "Avatax API Response Log", readonly=True, copy=False
    )

    @api.model
    @api.depends("company_id")
    def _compute_hide_exemption(self):
        avatax_config = self.env.company.get_avatax_config_company()
        for inv in self:
            inv.hide_exemption = avatax_config.hide_exemption

    hide_exemption = fields.Boolean(
        "Hide Exemption & Tax Based on shipping address",
        compute=_compute_hide_exemption,  # For past transactions visibility
        default=lambda self: self.env.company.get_avatax_config_company,
        help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
        "Also, it will show Tax based on shipping address button",
    )

    @api.depends("tax_on_shipping_address", "partner_id", "partner_shipping_id")
    def _compute_tax_address_id(self):
        for invoice in self:
            invoice.tax_address_id = (
                invoice.partner_shipping_id
                if invoice.tax_on_shipping_address
                else invoice.partner_id
            )

    @api.onchange("tax_address_id", "fiscal_position_id")
    def onchange_reset_avatax_amount(self):
        """
        When changing quantities or prices, reset the Avatax computed amount.
        The Odoo computed tax amount will then be shown, as a reference.
        The Avatax amount will be recomputed upon document validation.
        """
        for inv in self:
            inv.avatax_amount = 0
            for line in inv.invoice_line_ids:
                line.avatax_amt_line = 0

    # Same as v12
    def get_origin_tax_date(self):
        if self.invoice_doc_no:
            orig_invoice = self.search(
                [
                    ("name", "=", self.invoice_doc_no),
                    ("partner_id", "=", self.partner_id.id),
                ]
            )
            return orig_invoice.invoice_date
        return False

    # Same as v12
    def _get_avatax_doc_type(self, commit=True):
        self.ensure_one()
        avatax_config = self.company_id.get_avatax_config_company()
        if avatax_config.disable_tax_reporting:
            commit = False
        if "refund" in self.move_type:
            doc_type = "ReturnInvoice" if commit else "ReturnOrder"
        else:
            doc_type = "SalesInvoice" if commit else "SalesOrder"
        return doc_type

    # Same as v12
    def _avatax_prepare_lines(self, doc_type=None):
        """
        Prepare the lines to use for Avatax computation.
        Returns a list of dicts
        """
        sign = 1 if self.move_type.startswith("out") else -1
        lines = [
            line._avatax_prepare_line(sign, doc_type)
            for line in self.invoice_line_ids
            if line.price_subtotal or line.quantity
        ]
        return [x for x in lines if x]

    def update_tax_details(self, tax, line, tax_result_line):
        """Method to update details in tax"""
        return tax, line

    # Same as v12
    def _avatax_compute_tax(self, commit=False):
        """Contact REST API and recompute taxes for a Sale Order"""
        # Override to handle lines with split taxes (e.g. TN)
        self and self.ensure_one()
        avatax_config = self.company_id.get_avatax_config_company()
        if not avatax_config:
            # Skip Avatax computation if no configuration is found
            return
        doc_type = self._get_avatax_doc_type(commit=commit)
        tax_date = self.get_origin_tax_date() or self.invoice_date
        taxable_lines = self._avatax_prepare_lines(doc_type)
        tax_result = avatax_config.create_transaction(
            self.invoice_date or fields.Date.today(),
            self.name,
            doc_type,
            (
                self.so_partner_id
                if self.so_partner_id and avatax_config.use_so_partner_id
                else self.partner_id
            ),
            self.warehouse_id.partner_id or self.company_id.partner_id,
            self.tax_address_id or self.partner_id,
            taxable_lines,
            self.user_id,
            self.exemption_code or None,
            self.exemption_code_id.code or None,
            commit,
            tax_date,
            # TODO: can we report self.invoice_doc_no?
            self.name if self.move_type == "out_refund" else "",
            self.location_code or "",
            is_override=self.move_type == "out_refund",
            currency_id=self.currency_id,
            ignore_error=300 if commit else None,
            log_to_record=self,
        )
        # If commiting, and document exists, try unvoiding it
        # Error number 300 = GetTaxError, Expected Saved|Posted
        if commit and tax_result.get("number") == 300:
            _logger.info(
                "Document %s (%s) already exists in Avatax. "
                "Should be a voided transaction. "
                "Unvoiding and re-commiting.",
                self.name,
                doc_type,
            )
            avatax_config.unvoid_transaction(self.name, doc_type)
            avatax_config.commit_transaction(self.name, doc_type)
            return tax_result

        if self.state == "draft":
            Tax = self.env["account.tax"]
            tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
            taxes_to_set = {}
            for line in self.invoice_line_ids:
                tax_result_line = tax_result_lines.get(line.id)
                if tax_result_line:
                    # rate = tax_result_line.get("rate", 0.0)
                    tax_calculation = 0.0
                    if tax_result_line["taxableAmount"]:
                        tax_calculation = (
                            tax_result_line["taxCalculated"]
                            / tax_result_line["taxableAmount"]
                        )
                    rate = round(tax_calculation * 100, 4)
                    tax = Tax.get_avalara_tax(rate, doc_type)
                    tax, line = self.update_tax_details(tax, line, tax_result_line)
                    if tax and tax not in line.tax_ids:
                        line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
                        taxes_to_set[line.id] = line_taxes | tax
                    line.avatax_amt_line = tax_result_line["tax"]
            self.with_context(check_move_validity=False).avatax_amount = tax_result[
                "totalTax"
            ]
            container = {"records": self}

            # Set Taxes on lines in a way that properly triggers onchanges
            # This same approach is also used by the official account_taxcloud connector
            with self.with_context(
                avatax_invoice=self, check_move_validity=False
            )._sync_dynamic_lines(container), self.line_ids.mapped(
                "move_id"
            )._check_balanced(
                container
            ):
                for line_id in taxes_to_set.keys():
                    line = self.invoice_line_ids.filtered(lambda x: x.id == line_id)
                    line.write({"tax_ids": [(6, 0, [])]})
                    line.with_context(
                        avatax_invoice=self, check_move_validity=False
                    ).write({"tax_ids": taxes_to_set.get(line_id).ids})
            # After taxes are changed is needed to force compute taxes again, in 16 version
            # change of tax doesn't trigger compute of taxes on header for unknown reason
            self._compute_amount()
            if float_compare(
                self.amount_untaxed + max(self.amount_tax, abs(self.avatax_amount)),
                self.amount_residual,
                precision_rounding=self.currency_id.rounding or 0.001,
            ):
                taxes_data = {
                    iline.id: iline.tax_ids for iline in self.invoice_line_ids
                }
                self.invoice_line_ids.write({"tax_ids": [(6, 0, [])]})
                for line in self.invoice_line_ids:
                    line.write({"tax_ids": taxes_data[line.id].ids})
        return tax_result

    # Same as v13
    def avatax_compute_taxes(self, commit=False):
        """
        Called from Invoice's Action menu.
        Forces computation of the Invoice taxes
        """
        for invoice in self:
            if (
                invoice.move_type in ["out_invoice", "out_refund"]
                and invoice.fiscal_position_id.is_avatax
                and (invoice.state == "draft" or commit)
            ):
                invoice._avatax_compute_tax(commit=commit)
        return True

    def avatax_commit_taxes(self):
        for invoice in self:
            avatax_config = invoice.company_id.get_avatax_config_company()
            if not avatax_config.disable_tax_reporting:
                doc_type = invoice._get_avatax_doc_type()
                avatax_config.commit_transaction(invoice.name, doc_type)
        return True

    def is_avatax_calculated(self):
        """
        Only apply Avatax for these types of documents.
        Can be extended to support other types.
        """
        return self.is_sale_document()

    def _post(self, soft=True):
        for invoice in self:
            if invoice.is_avatax_calculated():
                avatax_config = self.company_id.get_avatax_config_company()
                if avatax_config and avatax_config.force_address_validation:
                    for addr in [self.partner_id, self.partner_shipping_id]:
                        if not addr.date_validation:
                            # The Validate action will be interrupted
                            # if the address is not validated
                            raise UserError(_("Avatax address is not validated!"))
                # We should compute taxes before validating the invoice
                # to ensure correct account moves
                # However, we can't save the invoice because it wasn't assigned a
                # number yet
                invoice.avatax_compute_taxes(commit=False)
        res = super()._post(soft=soft)
        for invoice in res:
            if invoice.is_avatax_calculated():
                # We can only commit to Avatax after validating the invoice
                # because we need the generated Invoice number
                invoice.avatax_compute_taxes(commit=True)
        return res

    # prepare_return in v12
    def _reverse_move_vals(self, default_values, cancel=True):
        # OVERRIDE
        # Don't keep anglo-saxon lines if not cancelling an existing invoice.
        move_vals = super(AccountMove, self)._reverse_move_vals(
            default_values, cancel=cancel
        )
        move_vals.update(
            {
                "invoice_doc_no": self.name,
                "invoice_date": default_values
                and default_values.get("invoice_date")
                or self.invoice_date,
                "tax_on_shipping_address": self.tax_on_shipping_address,
                "warehouse_id": self.warehouse_id.id,
                "location_code": self.location_code,
                "exemption_code": self.exemption_code or "",
                "exemption_code_id": self.exemption_code_id.id or None,
                "tax_address_id": self.tax_address_id.id,
            }
        )
        return move_vals

    # action_cancel in v12
    def button_draft(self):
        """
        Sets invoice to Draft, either from the Posted or Cancelled states
        """
        posted_invoices = self.filtered(
            lambda invoice: invoice.move_type in ["out_invoice", "out_refund"]
            and invoice.fiscal_position_id.is_avatax
            and invoice.state == "posted"
        )
        res = super(AccountMove, self).button_draft()
        for invoice in posted_invoices:
            avatax_config = invoice.company_id.get_avatax_config_company()
            if avatax_config:
                doc_type = invoice._get_avatax_doc_type()
                avatax_config.void_transaction(invoice.name, doc_type)
        return res

    @api.onchange(
        "invoice_line_ids",
        "warehouse_id",
        "tax_address_id",
        "tax_on_shipping_address",
        "partner_id",
    )
    def onchange_avatax_calculation(self):
        avatax_config = self.env.company.get_avatax_config_company()
        self.calculate_tax_on_save = False
        if avatax_config.invoice_calculate_tax:
            if (
                self._origin.warehouse_id != self.warehouse_id
                or self._origin.tax_address_id.street != self.tax_address_id.street
                or self._origin.partner_id != self.partner_id
                or self._origin.tax_on_shipping_address != self.tax_on_shipping_address
            ):
                self.calculate_tax_on_save = True
                return
            for line in self.invoice_line_ids:
                if (
                    line._origin.price_unit != line.price_unit
                    or line._origin.discount != line.discount
                    or line._origin.quantity != line.quantity
                ) and line.display_type == "product":
                    self.calculate_tax_on_save = True
                    break

    def write(self, vals):
        result = super(AccountMove, self).write(vals)
        avatax_config = self.env.company.get_avatax_config_company()
        for record in self:
            if (
                avatax_config.invoice_calculate_tax
                and record.calculate_tax_on_save
                and record.state == "draft"
                and not self._context.get("skip_second_write", False)
            ):
                record.with_context(skip_second_write=True).write(
                    {"calculate_tax_on_save": False}
                )
                record.avatax_compute_taxes()
        return result

    @api.model_create_multi
    def create(self, vals_list):
        moves = super().create(vals_list)
        avatax_config = self.env.company.get_avatax_config_company()
        for move in moves:
            if (
                avatax_config.invoice_calculate_tax
                and move.calculate_tax_on_save
                and not self._context.get("skip_second_write", False)
            ):
                move.with_context(skip_second_write=True).write(
                    {"calculate_tax_on_save": False}
                )
                move.avatax_compute_taxes()
        return moves


class AccountMoveLine(models.Model):
    _inherit = "account.move.line"

    avatax_amt_line = fields.Float(string="AvaTax Line", copy=False)

    def _get_avatax_amount(self, qty=None):
        """
        Return the company currency line amount, after discounts,
        to use for Tax calculation.

        Can be used to compute unit price only, using qty=1.

        Code extracted from account/models/account_move.py,
        from the compute_base_line_taxes() nested function,
        adjusted to compute line amount instead of unit price.
        """
        self.ensure_one()
        base_line = self
        move = base_line.move_id
        sign = -1 if move.is_inbound() else 1
        quantity = qty or base_line.quantity
        base_amount = base_line.price_unit * quantity
        if base_line.currency_id:
            price_unit_foreign_curr = (
                sign * base_amount * (1 - (base_line.discount / 100.0))
            )
            price_unit_comp_curr = base_line.currency_id._convert(
                price_unit_foreign_curr,
                move.company_id.currency_id,
                move.company_id,
                move.date,
            )
        else:
            price_unit_comp_curr = (
                sign * base_amount * (1 - (base_line.discount / 100.0))
            )
        return -price_unit_comp_curr

    # Same in v12
    def _avatax_prepare_line(self, sign=1, doc_type=None):
        """
        Prepare a line to use for Avatax computation.
        Returns a dict
        """
        line = self
        res = {}
        # Add UPC to product item code
        avatax_config = line.company_id.get_avatax_config_company()
        product = line.product_id
        if product.barcode and avatax_config.upc_enable:
            item_code = "UPC:%d" % product.barcode
        else:
            item_code = product.default_code or ("ID:%d" % product.id)
        tax_code = line.product_id.applicable_tax_code_id.name
        amount = sign * line._get_avatax_amount()
        if line.quantity < 0:
            amount = -amount
        res = {
            "qty": line.quantity,
            "itemcode": item_code,
            "description": line.name,
            "amount": amount,
            "tax_code": tax_code,
            "id": line,
            "account_id": line.account_id.id,
            "tax_id": line.tax_ids,
        }
        return res

    @api.onchange("price_unit", "discount", "quantity")
    def onchange_reset_tax_amt(self):
        """
        When changing quantities or prices, reset the Avatax computed amount.
        The Odoo computed tax amount will then be shown, as a reference.
        The Avatax amount will be recomputed upon document validation.
        """
        for line in self:
            line.avatax_amt_line = 0.0
            line.move_id.avatax_amount = 0.0
