"""
Fusion Accounting - Check Printing Support

Extends ``account.payment`` with fields and logic required for
printing physical checks, including automatic check numbering and
amount-to-words conversion.
"""

import logging
import math

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

_log = logging.getLogger(__name__)

# ======================================================================
# Amount-to-words conversion (English)
# ======================================================================

_ONES = [
    '', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight',
    'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen',
    'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen',
]
_TENS = [
    '', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy',
    'Eighty', 'Ninety',
]
_SCALES = [
    (10 ** 12, 'Trillion'),
    (10 ** 9, 'Billion'),
    (10 ** 6, 'Million'),
    (10 ** 3, 'Thousand'),
    (10 ** 2, 'Hundred'),
]


def _int_to_words(number):
    """Convert a non-negative integer to its English word representation.

    :param int number: A non-negative integer (0 .. 999 999 999 999 999).
    :return: English words, e.g. ``'One Thousand Two Hundred Thirty-Four'``.
    :rtype: str
    """
    if number == 0:
        return 'Zero'
    if number < 0:
        return 'Minus ' + _int_to_words(-number)

    parts = []
    for scale_value, scale_name in _SCALES:
        count, number = divmod(number, scale_value)
        if count:
            if scale_value == 100:
                parts.append(f'{_int_to_words(count)} {scale_name}')
            else:
                parts.append(f'{_int_to_words(count)} {scale_name}')
    if 0 < number < 20:
        parts.append(_ONES[number])
    elif number >= 20:
        tens_idx, ones_idx = divmod(number, 10)
        word = _TENS[tens_idx]
        if ones_idx:
            word += '-' + _ONES[ones_idx]
        parts.append(word)

    return ' '.join(parts)


def amount_to_words(amount, currency_name='Dollars', cents_name='Cents'):
    """Convert a monetary amount to an English sentence.

    Example::

        >>> amount_to_words(1234.56)
        'One Thousand Two Hundred Thirty-Four Dollars and Fifty-Six Cents'

    :param float amount: The monetary amount.
    :param str currency_name: Name of the major currency unit.
    :param str cents_name: Name of the minor currency unit.
    :return: The amount expressed in English words.
    :rtype: str
    """
    if amount < 0:
        return 'Minus ' + amount_to_words(-amount, currency_name, cents_name)

    whole = int(amount)
    # Round to avoid floating-point artefacts (e.g. 1.005 -> 0 cents)
    cents = round((amount - whole) * 100)
    if cents >= 100:
        whole += 1
        cents = 0

    result = f'{_int_to_words(whole)} {currency_name}'
    if cents:
        result += f' and {_int_to_words(cents)} {cents_name}'
    else:
        result += f' and Zero {cents_name}'
    return result


# ======================================================================
# Odoo model
# ======================================================================


class FusionCheckPrinting(models.Model):
    """Adds check-printing capabilities to ``account.payment``.

    Features
    --------
    * Manual or automatic check numbering per journal.
    * Human-readable amount-in-words field for check printing.
    * Validation to prevent duplicate check numbers within a journal.
    """

    _inherit = 'account.payment'

    # ------------------------------------------------------------------
    # Fields
    # ------------------------------------------------------------------

    check_number = fields.Char(
        string='Check Number',
        copy=False,
        tracking=True,
        help="The number printed on the physical check.",
    )
    check_manual_sequencing = fields.Boolean(
        string='Manual Numbering',
        related='journal_id.fusion_check_manual_sequencing',
        readonly=True,
        help="When enabled, check numbers are entered manually instead "
             "of being assigned automatically.",
    )
    check_next_number = fields.Char(
        string='Next Check Number',
        related='journal_id.fusion_check_next_number',
        readonly=False,
        help="The next check number to be assigned automatically.",
    )
    check_amount_in_words = fields.Char(
        string='Amount in Words',
        compute='_compute_check_amount_in_words',
        store=True,
        help="Human-readable representation of the payment amount, "
             "suitable for printing on a check.",
    )

    # ------------------------------------------------------------------
    # Computed fields
    # ------------------------------------------------------------------

    @api.depends('amount', 'currency_id')
    def _compute_check_amount_in_words(self):
        """Compute the textual representation of the payment amount."""
        for payment in self:
            if payment.currency_id and payment.amount:
                currency_name = payment.currency_id.currency_unit_label or 'Units'
                cents_name = payment.currency_id.currency_subunit_label or 'Cents'
                payment.check_amount_in_words = amount_to_words(
                    payment.amount,
                    currency_name=currency_name,
                    cents_name=cents_name,
                )
            else:
                payment.check_amount_in_words = ''

    # ------------------------------------------------------------------
    # Constraints
    # ------------------------------------------------------------------

    _sql_constraints = [
        (
            'check_number_unique',
            'UNIQUE(check_number, journal_id)',
            'A check number must be unique per journal.',
        ),
    ]

    # ------------------------------------------------------------------
    # Business logic
    # ------------------------------------------------------------------

    def action_assign_check_number(self):
        """Assign the next available check number from the journal.

        If the journal is configured for manual sequencing the user
        must enter the number themselves; this method handles only the
        automatic case.

        :raises UserError: if the journal uses manual sequencing.
        """
        for payment in self:
            if payment.check_manual_sequencing:
                raise UserError(_(
                    "Journal '%(journal)s' uses manual check numbering. "
                    "Please enter the check number manually.",
                    journal=payment.journal_id.display_name,
                ))
            if payment.check_number:
                continue  # already assigned

            next_number = payment.journal_id.fusion_check_next_number or '1'
            payment.check_number = next_number.zfill(6)
            # Increment the journal's next-number counter
            try:
                payment.journal_id.fusion_check_next_number = str(
                    int(next_number) + 1
                )
            except ValueError:
                _log.warning(
                    "Could not auto-increment check number '%s' on "
                    "journal %s", next_number,
                    payment.journal_id.display_name,
                )

    def action_print_check(self):
        """Print the check report for the selected payments.

        Automatically assigns check numbers to any payment that does
        not already have one.

        :return: Report action dictionary.
        :rtype: dict
        """
        payments_without_number = self.filtered(
            lambda p: not p.check_number and not p.check_manual_sequencing
        )
        payments_without_number.action_assign_check_number()

        missing = self.filtered(lambda p: not p.check_number)
        if missing:
            raise UserError(_(
                "The following payments still have no check number:\n"
                "%(payments)s\nPlease assign check numbers before printing.",
                payments=', '.join(missing.mapped('name')),
            ))

        return self.env.ref(
            'fusion_accounting.action_report_check'
        ).report_action(self)


class FusionAccountJournalCheck(models.Model):
    """Adds check-numbering configuration to ``account.journal``."""

    _inherit = 'account.journal'

    fusion_check_manual_sequencing = fields.Boolean(
        string='Manual Check Numbering',
        help="Enable to enter check numbers manually instead of using "
             "automatic sequencing.",
    )
    fusion_check_next_number = fields.Char(
        string='Next Check Number',
        default='1',
        help="The next check number that will be automatically assigned "
             "when printing checks from this journal.",
    )
