# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from collections import defaultdict
from pprint import pformat

from odoo import models, api, fields, _
from odoo.addons.odex30_account_avatax.lib.avatax_client import AvataxClient
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.release import version
from odoo.tools import float_round, format_list

_logger = logging.getLogger(__name__)


class AccountExternalTaxMixin(models.AbstractModel):
    _inherit = 'account.external.tax.mixin'

    is_avatax = fields.Boolean(compute='_compute_is_avatax')

    @api.depends('fiscal_position_id')
    def _compute_is_avatax(self):
        for record in self:
            record.is_avatax = record.fiscal_position_id.is_avatax

    def _compute_is_tax_computed_externally(self):
        super()._compute_is_tax_computed_externally()
        self.filtered('is_avatax').is_tax_computed_externally = True

    def _get_external_taxes(self):
        """ Override. """
        def find_or_create_tax(doc, detail):
            def repartition_line(repartition_type, account=None):
                return (0, 0, {
                    'repartition_type': repartition_type,
                    'tag_ids': [],
                    'company_id': doc.company_id.id,
                    'account_id': account and account.id,
                })

            fixed = detail.get('unitOfBasis') == 'FlatAmount'
            rate = detail['rate'] if fixed else detail['rate'] * 100
            name_precision = 4
            rounded_rate = float_round(rate, name_precision)
            tax_group_name = detail['taxName'].removesuffix(' TAX')
            tax_name = '%s %s' % (
                tax_group_name,
                ("$ %.4g" if fixed else "%.4g%%") % rounded_rate,
            )
            group_key = (tax_group_name, doc.company_id)
            if group_key not in tax_group_cache:
                tax_group_cache[group_key] = self.env['account.tax.group'].search([
                    *self.env['account.tax.group']._check_company_domain(doc.company_id),
                    ('name', '=', tax_group_name),
                ], limit=1) or self.env['account.tax.group'].sudo().with_company(doc.company_id).create({
                    'name': tax_group_name,
                })

            key = (tax_name, doc.company_id)
            if key not in tax_cache:
                tax_cache[key] = self.env['account.tax'].search([
                    *self.env['account.tax']._check_company_domain(doc.company_id),
                    ('name', '=', tax_name),
                ], limit=1) or self.env['account.tax'].sudo().with_company(self._find_avatax_credentials_company(doc.company_id)).create({
                    'name': tax_name,
                    'tax_group_id': tax_group_cache[group_key].id,
                    'amount': rate,
                    'amount_type': 'fixed' if fixed else 'percent',
                    'refund_repartition_line_ids': [
                        repartition_line('base'),
                        repartition_line('tax', doc.fiscal_position_id.avatax_refund_account_id),
                    ],
                    'invoice_repartition_line_ids': [
                        repartition_line('base'),
                        repartition_line('tax', doc.fiscal_position_id.avatax_invoice_account_id),
                    ],
                })
            return tax_cache[key]

        details, summary = super()._get_external_taxes()
        tax_cache = {}
        tax_group_cache = {}

        query_results = self.filtered('is_avatax')._query_avatax_taxes()
        errors = []
        for document, query_result in query_results.items():
            error = self._handle_response(query_result, _(
                'EXP could not fetch the taxes related to %(document)s.\n'
                'Please check the status of `%(technical)s` in the AvaTax portal.',
                document=document.display_name,
                technical=document.avatax_unique_code,
            ))
            if error:
                errors.append(error)
        if errors:
            raise UserError('\n\n'.join(errors))

        for document, query_result in query_results.items():
            is_return = document._get_avatax_document_type() == 'ReturnInvoice'
            line_amounts_sign = -1 if is_return else 1

            for line_result in query_result['lines']:
                record_id = line_result['lineNumber'].split(',')
                record = self.env[record_id[0]].browse(int(record_id[1]))
                details.setdefault(record, {})
                details[record]['total'] = line_amounts_sign * line_result['lineAmount']
                details[record]['tax_amount'] = line_amounts_sign * line_result['tax']
                for detail in line_result['details']:
                    tax = find_or_create_tax(document, detail)
                    details[record].setdefault('tax_ids', self.env['account.tax'])
                    details[record]['tax_ids'] += tax

            summary[document] = defaultdict(float)
            for summary_line in query_result['summary']:
                tax = find_or_create_tax(document, summary_line)

                summary[document][tax] += -summary_line['tax']

        return details, summary


    @api.constrains('partner_id', 'fiscal_position_id')
    def _check_address(self):
        incomplete_partner_to_records = self._get_partners_with_incomplete_information()

        if incomplete_partner_to_records:
            error = _("The following customer(s) need to have a zip, state and country when using Avatax:")
            partner_errors = [
                _(
                    "- %(partner_name)s (ID: %(partner_id)s) on %(record_list)s",
                    partner_name=partner.display_name,
                    partner_id=partner.id,
                    record_list=format_list(self.env, [record.display_name for record in records]),
                )
                for partner, records in incomplete_partner_to_records.items()
            ]
            raise ValidationError(error + "\n" + "\n".join(partner_errors))

    def _get_partners_with_incomplete_information(self, partner=None):

        incomplete_partner_to_records = {}
        for record in self.filtered(lambda r: r._perform_address_validation()):
            partner = partner or record.partner_id
            country = partner.country_id
            if (
                partner and partner != self.env.ref('base.public_partner')
                and (
                    not country
                    or (country.zip_required and not partner.zip)
                    or (country.state_required and not partner.state_id)
                )
            ):
                incomplete_partner_to_records.setdefault(partner, []).append(record)

        return incomplete_partner_to_records


    def _get_avatax_dates(self):

        raise NotImplementedError()

    def _get_avatax_document_type(self):

        raise NotImplementedError()

    def _get_avatax_ship_to_partner(self):

        return self.partner_shipping_id or self.partner_id

    def _perform_address_validation(self):

        return self.fiscal_position_id.is_avatax


    def _get_avatax_invoice_line(self, line_data):

        product = line_data['product_id']
        if not product._get_avatax_category_id():
            raise UserError(_(
                'The Avalara Tax Code is required for %(name)s (#%(id)s)\n'
                'See https://taxcode.avatax.avalara.com/',
                name=product.display_name,
                id=product.id,
            ))
        item_code = product.code or ""
        if self.env.company.avalara_use_upc and product.barcode:
            item_code = f'UPC:{product.barcode}'
        return {
            'amount': -line_data["price_subtotal"] if line_data["is_refund"] else line_data["price_subtotal"],
            'description': product.display_name,
            'quantity': abs(line_data["qty"]),
            'taxCode': product._get_avatax_category_id().code,
            'itemCode': item_code,
            'number': "%s,%s" % (line_data["model_name"], line_data["id"]),
        }

    @api.model
    def _find_avatax_credentials_company(self, company):
        has_avatax_credentials = bool(company.sudo().avalara_api_id and company.sudo().avalara_api_key)
        if has_avatax_credentials:
            return company
        elif company.parent_id:
            return self._find_avatax_credentials_company(company.parent_id)


    def _get_avatax_ref(self):
        return self.name or ''

    def _get_avatax_address_from_partner(self, partner):

        incomplete_partner = self._get_partners_with_incomplete_information(partner)
        if incomplete_partner.get(partner):
            res = {
                'latitude': partner.partner_latitude,
                'longitude': partner.partner_longitude,
            }
        else:
            res = {
                'city': partner.city,
                'country': partner.country_id.code,
                'region': partner.state_id.code,
                'postalCode': partner.zip,
                'line1': partner.street,
            }
        return res

    def _get_avatax_addresses(self, partner):

        res = {
            'shipFrom': self._get_avatax_address_from_partner(self.company_id.partner_id),
            'shipTo': self._get_avatax_address_from_partner(partner),
        }
        return res

    def _get_avatax_invoice_lines(self):
        return [self._get_avatax_invoice_line(line_data) for line_data in self._get_line_data_for_external_taxes()]

    def _get_avatax_taxes(self, commit):

        self.ensure_one()
        partner = self.partner_id.commercial_partner_id
        document_date, tax_date = self._get_avatax_dates()
        taxes = {
            'addresses': self._get_avatax_addresses(self._get_avatax_ship_to_partner()),
            'companyCode': self.company_id.partner_id.avalara_partner_code or '',
            'customerCode': partner.avalara_partner_code or partner.avatax_unique_code,
            'entityUseCode': partner.with_company(self.company_id).avalara_exemption_id.code or '',
            'businessIdentificationNo': partner.vat or '',
            'date': (document_date or fields.Date.today()).isoformat(),
            'lines': self._get_avatax_invoice_lines(),
            'type': self._get_avatax_document_type(),
            'code': self.avatax_unique_code,
            'referenceCode': self._get_avatax_ref(),
            'currencyCode': self.currency_id.name or '',
            'commit': commit and self.company_id.avalara_commit,
        }

        if tax_date:
            taxes['taxOverride'] = {
                'type': 'taxDate',
                'reason': 'Manually changed the tax calculation date',
                'taxDate': tax_date.isoformat(),
            }

        return taxes

    def _commit_avatax_taxes(self):
        self._query_avatax_taxes(commit=True)

    def _query_avatax_taxes(self, commit=False):

        if not self:
            return {}
        client = self._get_client(self.company_id)
        transactions = {record: record._get_avatax_taxes(commit) for record in self}
        return {
            record: client.create_transaction(transaction, include='Lines')
            for record, transaction in transactions.items()
        }

    def _uncommit_external_taxes(self):
        for record in self.filtered('is_avatax'):
            if not record.company_id.avalara_commit:
                continue
            client = self._get_client(record.company_id)
            query_result = client.uncommit_transaction(
                companyCode=record.company_id.partner_id.avalara_partner_code,
                transactionCode=record.avatax_unique_code,
            )
            error = self._handle_response(query_result, _(
                'EXP could not change the state of the transaction related to %(document)s in'
                ' AvaTax\nPlease check the status of `%(technical)s` in the AvaTax portal.',
                document=record.display_name,
                technical=record.avatax_unique_code,
            ))
            if error:
                raise UserError(error)

        return super()._uncommit_external_taxes()

    def _void_external_taxes(self):
        for record in self.filtered('is_avatax'):
            if not record.company_id.avalara_commit:
                continue
            client = self._get_client(record.company_id)
            query_result = client.void_transaction(
                companyCode=record.company_id.partner_id.avalara_partner_code,
                transactionCode=record.avatax_unique_code,
                model={"code": "DocVoided"},
            )

            if query_result.get('error', {}).get('code') == 'EntityNotFoundError':
                _logger.info(pformat(query_result))
                continue

            error = self._handle_response(query_result, _(
                'EXP could not void the transaction related to %(document)s in AvaTax\nPlease '
                'check the status of `%(technical)s` in the AvaTax portal.',
                document=record.display_name,
                technical=record.avatax_unique_code,
            ))
            if error:
                raise UserError(error)

        return super()._void_external_taxes()


    def _handle_response(self, response, title):
        if response.get('errors'):
            _logger.warning(pformat(response), stack_info=True)
            return '%s\n%s' % (title, response.get('title', ''))
        if response.get('error'):
            _logger.warning(pformat(response), stack_info=True)
            messages = '\n'.join(detail['message'] for detail in response['error']['details'])
            return '%s\n%s' % (title, messages)

    def _get_client(self, company):
        company = self._find_avatax_credentials_company(company)
        if not company:
            raise RedirectWarning(
                _('Please add your AvaTax credentials'),
                self.env.ref('base_setup.action_general_configuration').id,
                _("Go to the configuration panel"),
            )

        client = AvataxClient(
            app_name='Odoo',
            app_version=version,
            environment=company.avalara_environment,
        )
        client.add_credentials(
            company.sudo().avalara_api_id or '',
            company.sudo().avalara_api_key or '',
        )
        client.logger = lambda message: self._log_external_tax_request(
            'Avatax US', 'odex30_account_avatax.log.end.date', message
        )
        return client
