"""
Fusion Accounting - Avalara AvaTax Provider
============================================

Concrete implementation of :class:`FusionExternalTaxProvider` that integrates
with the **Avalara AvaTax REST API v2** for real-time tax calculation, address
validation, and transaction management.

API Reference: https://developer.avalara.com/api-reference/avatax/rest/v2/

Supported operations
--------------------
* **CreateTransaction** - compute tax on sales/purchase documents.
* **VoidTransaction**   - cancel a previously committed transaction.
* **ResolveAddress**    - validate and normalise postal addresses.
* **Ping**              - connection health check.

Configuration
-------------
Set the *AvaTax Environment* field to ``sandbox`` during development (uses
``sandbox-rest.avatax.com``) and switch to ``production`` for live tax filings.

Copyright (c) Nexa Systems Inc. - All rights reserved.
"""

import base64
import json
import logging

import requests

from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError

_logger = logging.getLogger(__name__)

# AvaTax REST API v2 base URLs
AVATAX_API_URLS = {
    'sandbox': 'https://sandbox-rest.avatax.com/api/v2',
    'production': 'https://rest.avatax.com/api/v2',
}

# Default timeout for AvaTax API requests (seconds)
AVATAX_REQUEST_TIMEOUT = 30

# Mapping of Odoo tax types to AvaTax transaction document types
AVATAX_DOC_TYPES = {
    'out_invoice': 'SalesInvoice',
    'out_refund': 'ReturnInvoice',
    'in_invoice': 'PurchaseInvoice',
    'in_refund': 'ReturnInvoice',
    'entry': 'SalesOrder',
}


class FusionAvaTaxProvider(models.Model):
    """Avalara AvaTax integration for automated tax calculation.

    Extends :class:`fusion.external.tax.provider` with AvaTax-specific
    credentials, endpoint configuration, and full REST API v2 support.
    """

    _inherit = "fusion.external.tax.provider"

    # -------------------------------------------------------------------------
    # AvaTax-Specific Fields
    # -------------------------------------------------------------------------
    avatax_account_number = fields.Char(
        string="AvaTax Account Number",
        groups="account.group_account_manager",
        help="Numeric account ID provided by Avalara upon registration.",
    )
    avatax_license_key = fields.Char(
        string="AvaTax License Key",
        groups="account.group_account_manager",
        help="Secret license key issued by Avalara. Stored encrypted.",
    )
    avatax_company_code = fields.Char(
        string="AvaTax Company Code",
        help="Company code configured in the Avalara portal. This identifies "
             "your nexus and tax configuration within AvaTax.",
    )
    avatax_environment = fields.Selection(
        selection=[
            ('sandbox', 'Sandbox (Testing)'),
            ('production', 'Production'),
        ],
        string="AvaTax Environment",
        default='sandbox',
        required=True,
        help="Use Sandbox for testing without real tax filings. Switch to "
             "Production for live transaction recording.",
    )
    avatax_commit_on_post = fields.Boolean(
        string="Commit on Invoice Post",
        default=True,
        help="When enabled, transactions are committed (locked) in AvaTax "
             "the moment the invoice is posted. Otherwise, they remain "
             "uncommitted until explicitly committed.",
    )
    avatax_address_validation = fields.Boolean(
        string="Address Validation",
        default=True,
        help="When enabled, customer addresses are validated and normalised "
             "through the AvaTax address resolution service before tax "
             "calculation.",
    )
    avatax_default_tax_code = fields.Char(
        string="Default Tax Code",
        default='P0000000',
        help="AvaTax tax code applied to products without a specific mapping. "
             "'P0000000' represents tangible personal property.",
    )

    # -------------------------------------------------------------------------
    # Selection Extension
    # -------------------------------------------------------------------------
    @api.model
    def _get_provider_type_selection(self):
        """Add 'avatax' to the provider type selection list."""
        return [('generic', 'Generic'), ('avatax', 'Avalara AvaTax')]

    def _init_provider_type(self):
        """Dynamically extend provider_type selection for AvaTax."""
        selection = self._fields['provider_type'].selection
        if isinstance(selection, list):
            avatax_entry = ('avatax', 'Avalara AvaTax')
            if avatax_entry not in selection:
                selection.append(avatax_entry)

    @api.model_create_multi
    def create(self, vals_list):
        """Set provider code and API URL for AvaTax records automatically."""
        for vals in vals_list:
            if vals.get('provider_type') == 'avatax':
                vals.setdefault('code', 'avatax')
                env = vals.get('avatax_environment', 'sandbox')
                vals.setdefault('api_url', AVATAX_API_URLS.get(env, AVATAX_API_URLS['sandbox']))
        return super().create(vals_list)

    def write(self, vals):
        """Keep the API URL in sync when the environment changes."""
        if 'avatax_environment' in vals:
            vals['api_url'] = AVATAX_API_URLS.get(
                vals['avatax_environment'], AVATAX_API_URLS['sandbox']
            )
        return super().write(vals)

    # -------------------------------------------------------------------------
    # AvaTax REST API Helpers
    # -------------------------------------------------------------------------
    def _avatax_get_api_url(self):
        """Return the base API URL for the configured environment.

        :returns: Base URL string without trailing slash.
        """
        self.ensure_one()
        return AVATAX_API_URLS.get(self.avatax_environment, AVATAX_API_URLS['sandbox'])

    def _avatax_get_auth_header(self):
        """Build the HTTP Basic authentication header.

        AvaTax authenticates via ``Authorization: Basic <base64(account:key)>``.

        :returns: ``dict`` with the Authorization header.
        :raises UserError: When credentials are missing.
        """
        self.ensure_one()
        if not self.avatax_account_number or not self.avatax_license_key:
            raise UserError(_(
                "AvaTax account number and license key are required. "
                "Please configure them on provider '%(name)s'.",
                name=self.name,
            ))
        credentials = f"{self.avatax_account_number}:{self.avatax_license_key}"
        encoded = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
        return {'Authorization': f'Basic {encoded}'}

    def _avatax_request(self, method, endpoint, payload=None, params=None):
        """Execute an authenticated request against the AvaTax REST API v2.

        :param method:   HTTP method (``'GET'``, ``'POST'``, ``'DELETE'``).
        :param endpoint: API path relative to the version root, e.g.
                         ``'/transactions/create'``.
        :param payload:  JSON-serialisable request body (for POST/PUT).
        :param params:   URL query parameters dict.
        :returns:        Parsed JSON response ``dict``.
        :raises UserError: On HTTP or API errors.
        """
        self.ensure_one()
        base_url = self._avatax_get_api_url()
        url = f"{base_url}{endpoint}"
        headers = {
            **self._avatax_get_auth_header(),
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'X-Avalara-Client': 'FusionAccounting;19.0;OdooConnector;1.0',
        }

        if self.log_requests:
            _logger.debug(
                "AvaTax %s %s | payload=%s | params=%s",
                method, url, json.dumps(payload or {}), params,
            )

        try:
            response = requests.request(
                method=method.upper(),
                url=url,
                json=payload,
                params=params,
                headers=headers,
                timeout=AVATAX_REQUEST_TIMEOUT,
            )
        except requests.exceptions.ConnectionError:
            raise UserError(_(
                "Unable to connect to AvaTax at %(url)s. "
                "Please verify your network connection and the configured environment.",
                url=url,
            ))
        except requests.exceptions.Timeout:
            raise UserError(_(
                "The request to AvaTax timed out after %(timeout)s seconds. "
                "Please try again or contact Avalara support.",
                timeout=AVATAX_REQUEST_TIMEOUT,
            ))
        except requests.exceptions.RequestException as exc:
            raise UserError(_(
                "AvaTax request failed: %(error)s",
                error=str(exc),
            ))

        if self.log_requests:
            _logger.debug(
                "AvaTax response %s: %s",
                response.status_code, response.text[:2000],
            )

        if response.status_code in (200, 201):
            return response.json()

        # Handle structured AvaTax error responses
        self._avatax_handle_error(response)

    def _avatax_handle_error(self, response):
        """Parse and raise a descriptive error from an AvaTax API response.

        :param response: ``requests.Response`` with a non-2xx status code.
        :raises UserError: Always.
        """
        try:
            error_data = response.json()
        except (ValueError, KeyError):
            raise UserError(_(
                "AvaTax returned HTTP %(code)s with an unparseable body: %(body)s",
                code=response.status_code,
                body=response.text[:500],
            ))

        error_info = error_data.get('error', {})
        message = error_info.get('message', 'Unknown error')
        details = error_info.get('details', [])
        detail_messages = '\n'.join(
            f"  - [{d.get('severity', 'Error')}] {d.get('message', '')} "
            f"(ref: {d.get('refersTo', 'N/A')})"
            for d in details
        )
        raise UserError(_(
            "AvaTax API Error (HTTP %(code)s): %(message)s\n\nDetails:\n%(details)s",
            code=response.status_code,
            message=message,
            details=detail_messages or _("No additional details provided."),
        ))

    # -------------------------------------------------------------------------
    # Tax Calculation
    # -------------------------------------------------------------------------
    def calculate_tax(self, order_lines):
        """Compute tax via AvaTax CreateTransaction API.

        Builds a ``CreateTransactionModel`` payload from the provided move
        lines and submits it to the AvaTax API. The response is parsed and
        returned as a normalised list of per-line tax results.

        :param order_lines: ``account.move.line`` recordset with product,
                            quantity, price, and associated partner address data.
        :returns: ``dict`` with keys ``doc_code``, ``total_tax``, and ``lines``
                  (list of per-line tax detail dicts).
        :raises UserError: On API failure or missing configuration.
        """
        self.ensure_one()
        if not order_lines:
            return {'doc_code': False, 'total_tax': 0.0, 'lines': []}

        move = order_lines[0].move_id
        partner = move.partner_id
        if not partner:
            raise UserError(_(
                "Cannot compute external taxes: the invoice has no partner set."
            ))

        payload = self._avatax_build_transaction_payload(move, order_lines)
        result = self._avatax_request('POST', '/transactions/create', payload=payload)
        return self._avatax_parse_transaction_result(result, order_lines)

    def _avatax_build_transaction_payload(self, move, lines):
        """Construct the CreateTransactionModel JSON body.

        Maps invoice data to the AvaTax transaction schema described at:
        https://developer.avalara.com/api-reference/avatax/rest/v2/models/CreateTransactionModel/

        :param move:  ``account.move`` record.
        :param lines: ``account.move.line`` recordset (product lines only).
        :returns:     ``dict`` ready for JSON serialisation.
        """
        self.ensure_one()
        partner = move.partner_id
        company = move.company_id

        # Determine document type from move_type
        doc_type = AVATAX_DOC_TYPES.get(move.move_type, 'SalesOrder')

        # Build address objects
        ship_to = self._avatax_build_address(partner)
        ship_from = self._avatax_build_address(company.partner_id)

        # Build line items
        avatax_lines = []
        for idx, line in enumerate(lines.filtered(lambda l: l.display_type == 'product')):
            tax_code = self._avatax_get_product_tax_code(line.product_id)
            avatax_lines.append({
                'number': str(idx + 1),
                'quantity': abs(line.quantity),
                'amount': abs(line.price_subtotal),
                'taxCode': tax_code,
                'itemCode': line.product_id.default_code or line.product_id.name or '',
                'description': (line.name or '')[:255],
                'discounted': bool(line.discount),
                'ref1': str(line.id),
            })

        if not avatax_lines:
            raise UserError(_(
                "No taxable product lines found on invoice %(ref)s.",
                ref=move.name or move.ref or 'New',
            ))

        commit = self.avatax_commit_on_post and move.state == 'posted'
        payload = {
            'type': doc_type,
            'companyCode': self.avatax_company_code or company.name,
            'date': fields.Date.to_string(move.invoice_date or move.date),
            'customerCode': partner.ref or partner.name or str(partner.id),
            'purchaseOrderNo': move.ref or '',
            'addresses': {
                'shipFrom': ship_from,
                'shipTo': ship_to,
            },
            'lines': avatax_lines,
            'commit': commit,
            'currencyCode': move.currency_id.name,
            'description': f"Odoo Invoice {move.name or 'Draft'}",
        }

        # Only include document code for posted invoices
        if move.name and move.name != '/':
            payload['code'] = move.name

        return payload

    def _avatax_build_address(self, partner):
        """Convert a partner record to an AvaTax address dict.

        :param partner: ``res.partner`` record.
        :returns: ``dict`` with AvaTax address fields.
        """
        return {
            'line1': partner.street or '',
            'line2': partner.street2 or '',
            'city': partner.city or '',
            'region': partner.state_id.code or '',
            'country': partner.country_id.code or '',
            'postalCode': partner.zip or '',
        }

    def _avatax_get_product_tax_code(self, product):
        """Resolve the AvaTax tax code for a given product.

        Checks (in order):
        1. A custom field ``avatax_tax_code`` on the product template.
        2. A category-level mapping via ``categ_id.avatax_tax_code``.
        3. The provider's default tax code.

        :param product: ``product.product`` record.
        :returns: Tax code string.
        """
        if product and hasattr(product, 'avatax_tax_code') and product.avatax_tax_code:
            return product.avatax_tax_code
        if (
            product
            and product.categ_id
            and hasattr(product.categ_id, 'avatax_tax_code')
            and product.categ_id.avatax_tax_code
        ):
            return product.categ_id.avatax_tax_code
        return self.avatax_default_tax_code or 'P0000000'

    def _avatax_parse_transaction_result(self, result, order_lines):
        """Parse the AvaTax CreateTransaction response into a normalised format.

        :param result:      Parsed JSON response from AvaTax.
        :param order_lines: Original ``account.move.line`` recordset.
        :returns: ``dict`` with ``doc_code``, ``total_tax``, ``total_amount``,
                  and ``lines`` list.
        """
        doc_code = result.get('code', '')
        total_tax = result.get('totalTax', 0.0)
        total_amount = result.get('totalAmount', 0.0)

        lines_result = []
        for avatax_line in result.get('lines', []):
            line_ref = avatax_line.get('ref1', '')
            tax_details = []
            for detail in avatax_line.get('details', []):
                tax_details.append({
                    'tax_name': detail.get('taxName', ''),
                    'tax_rate': detail.get('rate', 0.0),
                    'tax_amount': detail.get('tax', 0.0),
                    'taxable_amount': detail.get('taxableAmount', 0.0),
                    'jurisdiction': detail.get('jurisName', ''),
                    'jurisdiction_type': detail.get('jurisType', ''),
                    'region': detail.get('region', ''),
                    'country': detail.get('country', ''),
                })
            lines_result.append({
                'line_id': int(line_ref) if line_ref.isdigit() else False,
                'line_number': avatax_line.get('lineNumber', ''),
                'tax_amount': avatax_line.get('tax', 0.0),
                'taxable_amount': avatax_line.get('taxableAmount', 0.0),
                'exempt_amount': avatax_line.get('exemptAmount', 0.0),
                'tax_details': tax_details,
            })

        return {
            'doc_code': doc_code,
            'total_tax': total_tax,
            'total_amount': total_amount,
            'lines': lines_result,
        }

    # -------------------------------------------------------------------------
    # Transaction Void
    # -------------------------------------------------------------------------
    def void_transaction(self, doc_code, doc_type='SalesInvoice'):
        """Void a committed transaction in AvaTax.

        Uses the VoidTransaction API endpoint to mark a previously committed
        tax document as voided. This prevents it from appearing in tax filings.

        :param doc_code: Document code (typically the invoice number).
        :param doc_type: AvaTax document type (default ``'SalesInvoice'``).
        :returns: ``True`` on success.
        :raises UserError: When the API call fails.
        """
        self.ensure_one()
        if not doc_code:
            _logger.warning("void_transaction called with empty doc_code, skipping.")
            return True

        company_code = self.avatax_company_code or self.company_id.name
        endpoint = f"/companies/{company_code}/transactions/{doc_code}/void"
        payload = {'code': 'DocVoided'}

        self._avatax_request('POST', endpoint, payload=payload)
        _logger.info(
            "AvaTax transaction voided: company=%s doc_code=%s",
            company_code, doc_code,
        )
        return True

    # -------------------------------------------------------------------------
    # Address Validation
    # -------------------------------------------------------------------------
    def validate_address(self, partner):
        """Validate and normalise a partner address via the AvaTax address
        resolution service.

        Calls ``POST /addresses/resolve`` and returns the validated address
        components. If validation fails, the original address is returned
        unchanged with a warning.

        :param partner: ``res.partner`` record to validate.
        :returns: ``dict`` with normalised address fields.
        """
        self.ensure_one()
        if not self.avatax_address_validation:
            return {}

        payload = {
            'line1': partner.street or '',
            'line2': partner.street2 or '',
            'city': partner.city or '',
            'region': partner.state_id.code or '',
            'country': partner.country_id.code or '',
            'postalCode': partner.zip or '',
        }

        try:
            result = self._avatax_request('POST', '/addresses/resolve', payload=payload)
        except UserError:
            _logger.warning(
                "AvaTax address validation failed for partner %s, using original address.",
                partner.display_name,
            )
            return {}

        validated = result.get('validatedAddresses', [{}])[0] if result.get('validatedAddresses') else {}
        messages = result.get('messages', [])

        return {
            'street': validated.get('line1', partner.street),
            'street2': validated.get('line2', partner.street2),
            'city': validated.get('city', partner.city),
            'zip': validated.get('postalCode', partner.zip),
            'state_code': validated.get('region', ''),
            'country_code': validated.get('country', ''),
            'latitude': validated.get('latitude', ''),
            'longitude': validated.get('longitude', ''),
            'messages': [
                {'severity': m.get('severity', 'info'), 'summary': m.get('summary', '')}
                for m in messages
            ],
        }

    def action_validate_partner_address(self):
        """Wizard action: validate the address of a selected partner."""
        self.ensure_one()
        partner = self.env.context.get('active_id')
        if not partner:
            raise UserError(_("No partner selected for address validation."))

        partner_rec = self.env['res.partner'].browse(partner)
        result = self.validate_address(partner_rec)

        if not result:
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _("Address Validation"),
                    'message': _("Address validation is disabled or returned no data."),
                    'type': 'warning',
                    'sticky': False,
                },
            }

        # Update the partner with validated address
        update_vals = {}
        if result.get('street'):
            update_vals['street'] = result['street']
        if result.get('street2'):
            update_vals['street2'] = result['street2']
        if result.get('city'):
            update_vals['city'] = result['city']
        if result.get('zip'):
            update_vals['zip'] = result['zip']
        if result.get('state_code'):
            state = self.env['res.country.state'].search([
                ('code', '=', result['state_code']),
                ('country_id.code', '=', result.get('country_code', '')),
            ], limit=1)
            if state:
                update_vals['state_id'] = state.id
        if update_vals:
            partner_rec.write(update_vals)

        msg_parts = [m['summary'] for m in result.get('messages', []) if m.get('summary')]
        summary = '\n'.join(msg_parts) if msg_parts else _("Address validated successfully.")

        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': _("Address Validation"),
                'message': summary,
                'type': 'success' if not msg_parts else 'warning',
                'sticky': bool(msg_parts),
            },
        }

    # -------------------------------------------------------------------------
    # Connection Test
    # -------------------------------------------------------------------------
    def test_connection(self):
        """Ping the AvaTax API to verify credentials and connectivity.

        Calls ``GET /utilities/ping`` which returns authentication status.

        :returns: ``True`` on successful ping.
        :raises UserError: When the ping fails.
        """
        self.ensure_one()
        result = self._avatax_request('GET', '/utilities/ping')
        authenticated = result.get('authenticated', False)
        if not authenticated:
            raise UserError(_(
                "AvaTax ping succeeded but the credentials are not valid. "
                "Please check your account number and license key."
            ))
        _logger.info(
            "AvaTax connection test passed: authenticated=%s version=%s",
            authenticated, result.get('version', 'unknown'),
        )
        return True

    # -------------------------------------------------------------------------
    # Onchange
    # -------------------------------------------------------------------------
    @api.onchange('avatax_environment')
    def _onchange_avatax_environment(self):
        """Update the API URL when the environment selection changes."""
        if self.avatax_environment:
            self.api_url = AVATAX_API_URLS.get(
                self.avatax_environment, AVATAX_API_URLS['sandbox']
            )
