import re
from math import copysign

from odoo import _, models, Command
from odoo.exceptions import UserError


class ReconcileModelLine(models.Model):
    """Extends reconciliation model lines with methods for computing
    journal item values across manual and bank reconciliation contexts."""

    _inherit = 'account.reconcile.model.line'

    # ------------------------------------------------------------------
    # Core helpers
    # ------------------------------------------------------------------

    def _resolve_taxes_for_partner(self, partner):
        """Return the tax recordset that should be applied, taking fiscal
        position mapping into account when a partner is provided."""
        tax_records = self.tax_ids
        if not tax_records or not partner:
            return tax_records
        fpos = self.env['account.fiscal.position']._get_fiscal_position(partner)
        if fpos:
            tax_records = fpos.map_tax(tax_records)
        return tax_records

    def _prepare_aml_vals(self, partner):
        """Build a base dictionary of account.move.line values derived from
        this reconciliation model line.

        Fiscal-position tax remapping is applied automatically when the
        supplied *partner* record has a matching fiscal position.

        Args:
            partner: ``res.partner`` record to attach to the move line.

        Returns:
            ``dict`` suitable for later account.move.line creation.
        """
        self.ensure_one()

        mapped_taxes = self._resolve_taxes_for_partner(partner)

        result_values = {
            'name': self.label,
            'partner_id': partner.id,
            'analytic_distribution': self.analytic_distribution,
            'tax_ids': [Command.set(mapped_taxes.ids)],
            'reconcile_model_id': self.model_id.id,
        }

        if self.account_id:
            result_values['account_id'] = self.account_id.id

        return result_values

    # ------------------------------------------------------------------
    # Manual reconciliation
    # ------------------------------------------------------------------

    def _compute_manual_amount(self, remaining_balance, currency):
        """Derive the line amount for manual reconciliation based on the
        configured amount type (percentage or fixed).

        Raises ``UserError`` for amount types that are only valid inside the
        bank reconciliation widget (e.g. regex, percentage_st_line).
        """
        if self.amount_type == 'percentage':
            return currency.round(remaining_balance * self.amount / 100.0)

        if self.amount_type == 'fixed':
            direction = 1 if remaining_balance > 0.0 else -1
            return currency.round(self.amount * direction)

        raise UserError(
            _("This reconciliation model cannot be applied in the manual "
              "reconciliation widget because its amount type is not supported "
              "in that context.")
        )

    def _apply_in_manual_widget(self, residual_amount_currency, partner, currency):
        """Produce move-line values for the manual reconciliation widget.

        The ``journal_id`` field is deliberately included in the result even
        though it is a related (read-only) field on the move line.  The manual
        reconciliation widget relies on its presence to group lines into a
        single journal entry per journal.

        Args:
            residual_amount_currency: Open balance in the account's currency.
            partner: ``res.partner`` record for the counterpart.
            currency: ``res.currency`` record used by the account.

        Returns:
            ``dict`` ready for account.move.line creation.
        """
        self.ensure_one()

        computed_amount = self._compute_manual_amount(residual_amount_currency, currency)

        line_data = self._prepare_aml_vals(partner)
        line_data.update({
            'currency_id': currency.id,
            'amount_currency': computed_amount,
            'journal_id': self.journal_id.id,
        })
        return line_data

    # ------------------------------------------------------------------
    # Bank reconciliation
    # ------------------------------------------------------------------

    def _extract_regex_amount(self, payment_ref, residual_balance):
        """Try to extract a numeric amount from *payment_ref* using the
        regex pattern stored on this line.

        Returns the parsed amount with the correct sign, or ``0.0`` when
        parsing fails or the pattern does not match.
        """
        pattern_match = re.search(self.amount_string, payment_ref)
        if not pattern_match:
            return 0.0

        separator = self.model_id.decimal_separator
        direction = 1 if residual_balance > 0.0 else -1
        try:
            raw_group = pattern_match.group(1)
            digits_only = re.sub(r'[^\d' + separator + ']', '', raw_group)
            parsed_value = float(digits_only.replace(separator, '.'))
            return copysign(parsed_value * direction, residual_balance)
        except (ValueError, IndexError):
            return 0.0

    def _compute_percentage_st_line_amount(self, st_line, currency):
        """Calculate the move-line amount and currency when the amount type
        is ``percentage_st_line``.

        Depending on the model configuration the calculation uses either the
        raw transaction figures or the journal-currency figures.

        Returns a ``(computed_amount, target_currency)`` tuple.
        """
        (
            txn_amount, txn_currency,
            jnl_amount, jnl_currency,
            _comp_amount, _comp_currency,
        ) = st_line._get_accounting_amounts_and_currencies()

        ratio = self.amount / 100.0
        is_invoice_writeoff = (
            self.model_id.rule_type == 'writeoff_button'
            and self.model_id.counterpart_type in ('sale', 'purchase')
        )

        if is_invoice_writeoff:
            # Invoice creation – use the original transaction currency.
            return currency.round(-txn_amount * ratio), txn_currency, ratio
        # Standard write-off – follow the journal currency.
        return currency.round(-jnl_amount * ratio), jnl_currency, None

    def _apply_in_bank_widget(self, residual_amount_currency, partner, st_line):
        """Produce move-line values for the bank reconciliation widget.

        Handles three amount-type strategies:

        * ``percentage_st_line`` – percentage of the statement line amount
        * ``regex`` – amount extracted from the payment reference
        * fallback – delegates to :meth:`_apply_in_manual_widget`

        Args:
            residual_amount_currency: Open balance in the statement line currency.
            partner: ``res.partner`` record for the counterpart.
            st_line: ``account.bank.statement.line`` being reconciled.

        Returns:
            ``dict`` ready for account.move.line creation.
        """
        self.ensure_one()

        line_currency = (
            st_line.foreign_currency_id
            or st_line.journal_id.currency_id
            or st_line.company_currency_id
        )

        # -- percentage of statement line ---------------------------------
        if self.amount_type == 'percentage_st_line':
            computed_amount, target_cur, pct_ratio = (
                self._compute_percentage_st_line_amount(st_line, line_currency)
            )
            entry_data = self._prepare_aml_vals(partner)
            entry_data['currency_id'] = target_cur.id
            entry_data['amount_currency'] = computed_amount
            if pct_ratio is not None:
                entry_data['percentage_st_line'] = pct_ratio
            if not entry_data.get('name'):
                entry_data['name'] = st_line.payment_ref
            return entry_data

        # -- regex extraction from payment reference ----------------------
        if self.amount_type == 'regex':
            extracted = self._extract_regex_amount(
                st_line.payment_ref, residual_amount_currency,
            )
            entry_data = self._prepare_aml_vals(partner)
            entry_data['currency_id'] = line_currency.id
            entry_data['amount_currency'] = extracted
            if not entry_data.get('name'):
                entry_data['name'] = st_line.payment_ref
            return entry_data

        # -- percentage / fixed – reuse manual widget logic ---------------
        entry_data = self._apply_in_manual_widget(
            residual_amount_currency, partner, line_currency,
        )
        if not entry_data.get('name'):
            entry_data['name'] = st_line.payment_ref
        return entry_data
