# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import re
from itertools import chain
from stdnum.util import clean

from odoo import api, fields, models, _
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.base_iban.models.res_partner_bank import normalize_iban, pretty_iban, validate_iban, get_iban_part
from odoo.exceptions import ValidationError
from odoo.tools import LazyTranslate, street_split
from odoo.tools.misc import mod10r

_lt = LazyTranslate(__name__)

# Swiss Payment Standards - Swiss Implementation Guidelines for the QR-bill - version 2.3
UNICODE_ALLOWED = {chr(ucode) for ucode in
                   chain(range(0x20, 0x80), range(0xA0, 0x180), range(0x218, 0x21C), [0x20AC])}

def validate_qr_iban(qr_iban):
    # Check first if it's a valid IBAN.
    validate_iban(qr_iban)

    # We sanitize first so that _check_qr_iban_range() can extract correct IID from IBAN to validate it.
    sanitized_qr_iban = sanitize_account_number(qr_iban)

    if sanitized_qr_iban[:2] not in ['CH', 'LI']:
        raise ValidationError(_lt("QR-IBAN numbers are only available in Switzerland."))

    # Now, check if it's valid QR-IBAN (based on its IID).
    if not check_qr_iban_range(sanitized_qr_iban):
        raise ValidationError(_lt("QR-IBAN “%s” is invalid.", qr_iban))

    return True

def check_qr_iban_range(iban):
    if not iban or len(iban) < 9:
        return False
    iid = get_iban_part(iban, 'bank')
    return re.match(r'\d+', iid) and 30000 <= int(iid) <= 31999 # Those values for iid are reserved for QR-IBANs only


class ResPartnerBank(models.Model):
    _inherit = 'res.partner.bank'

    l10n_ch_qr_iban = fields.Char(string='QR-IBAN',
                                  compute='_compute_l10n_ch_qr_iban',
                                  store=True,
                                  readonly=False,
                                  help="Put the QR-IBAN here for your own bank accounts.  That way, you can "
                                       "still use the main IBAN in the Account Number while you will see the "
                                       "QR-IBAN for the barcode.  ")

    # fields to configure payment slip generation
    l10n_ch_display_qr_bank_options = fields.Boolean(compute='_compute_l10n_ch_display_qr_bank_options')

    @api.depends('partner_id', 'company_id')
    def _compute_l10n_ch_display_qr_bank_options(self):
        for bank in self:
            if bank.partner_id:
                bank.l10n_ch_display_qr_bank_options = bank.partner_id.ref_company_ids.country_id.code in ('CH', 'LI')
            elif bank.company_id:
                bank.l10n_ch_display_qr_bank_options = bank.company_id.account_fiscal_country_id.code in ('CH', 'LI')
            else:
                bank.l10n_ch_display_qr_bank_options = self.env.company.account_fiscal_country_id.code in ('CH', 'LI')

    @api.depends('acc_number')
    def _compute_l10n_ch_qr_iban(self):
        for record in self:
            try:
                validate_qr_iban(record.acc_number)
                valid_qr_iban = True
            except ValidationError:
                valid_qr_iban = False
            if valid_qr_iban:
                record.l10n_ch_qr_iban = record.sanitized_acc_number
            else:
                record.l10n_ch_qr_iban = None

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get('l10n_ch_qr_iban'):
                validate_qr_iban(vals['l10n_ch_qr_iban'])
                vals['l10n_ch_qr_iban'] = pretty_iban(normalize_iban(vals['l10n_ch_qr_iban']))
        return super().create(vals_list)

    def write(self, vals):
        if vals.get('l10n_ch_qr_iban'):
            validate_qr_iban(vals['l10n_ch_qr_iban'])
            vals['l10n_ch_qr_iban'] = pretty_iban(normalize_iban(vals['l10n_ch_qr_iban']))
        return super().write(vals)

    def _l10n_ch_filter_text(self, value):
        value = ' '.join((value or '').split())
        if value.isprintable() and value.isascii() or {*value} < UNICODE_ALLOWED:
            return value  # shortcut for performance
        forbidden = {*value} - UNICODE_ALLOWED
        for char in forbidden:
            value = value.replace(char, '')
        return value.strip()

    def _l10n_ch_get_qr_vals(self, amount, currency, debtor_partner, free_communication, structured_communication):
        filter_text = self._l10n_ch_filter_text
        comment = ""
        if free_communication:
            free_communication = filter_text(free_communication)
            comment = (free_communication[:137] + '...') if len(free_communication) > 140 else free_communication

        cred_street, cred_street_number, cred_zip, cred_city = self._get_partner_address_lines(self.partner_id)
        debt_street, debt_street_number, debt_zip, debt_city = self._get_partner_address_lines(debtor_partner)

        # Compute reference type (empty by default, only mandatory for QR-IBAN,
        # and must then be 27 characters-long, with mod10r check digit as the 27th one)
        reference_type = 'NON'
        reference = ''
        acc_number = self.sanitized_acc_number

        if self.l10n_ch_qr_iban:
            # _check_for_qr_code_errors ensures we can't have a QR-IBAN without a QR-reference here
            reference_type = 'QRR'
            reference = structured_communication
            acc_number = sanitize_account_number(self.l10n_ch_qr_iban)
        elif self._is_iso11649_reference(structured_communication):
            reference_type = 'SCOR'
            reference = structured_communication.replace(' ', '')

        currency = currency or self.currency_id or self.company_id.currency_id
        cred_name = filter_text(self.acc_holder_name or self.partner_id.name)
        debt_name = filter_text(debtor_partner.commercial_partner_id.name)

        result = [
            'SPC',                                                # QR Type
            '0200',                                               # Version
            '1',                                                  # Coding Type
            acc_number,                                           # IBAN / QR-IBAN
            'S',                                                  # Creditor Address Type
            cred_name[:70],                                       # Creditor Name
            cred_street,                                          # Creditor Street Name
            cred_street_number,                                   # Creditor Building Number
            cred_zip,                                             # Creditor Postal Code
            cred_city,                                            # Creditor Town
            self.partner_id.country_id.code,                      # Creditor Country
            '',                                                   # Ultimate Creditor Address Type
            '',                                                   # Name
            '',                                                   # Ultimate Creditor Address Line 1
            '',                                                   # Ultimate Creditor Address Line 2
            '',                                                   # Ultimate Creditor Postal Code
            '',                                                   # Ultimate Creditor Town
            '',                                                   # Ultimate Creditor Country
            '{:.2f}'.format(amount),                              # Amount
            currency.name,                                        # Currency
            'S',                                                  # Ultimate Debtor Address Type
            debt_name[:70],                                       # Ultimate Debtor Name
            debt_street,                                          # Ultimate Debtor Street Name
            debt_street_number,                                   # Ultimate Debtor Building Number
            debt_zip,                                             # Ultimate Debtor Postal Code
            debt_city,                                            # Ultimate Debtor Town
            debtor_partner.country_id.code,                       # Ultimate Debtor Country
            reference_type,                                       # Reference Type
            reference,                                            # Reference
            comment,                                              # Unstructured Message
            'EPD',                                                # Mandatory trailer part
        ]

        # newlines shift field content to a different line, causing the QR code to be rejected
        return [line.replace('\n', ' ') for line in result]

    def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
        if qr_method == 'ch_qr':
            return self._l10n_ch_get_qr_vals(amount, currency, debtor_partner, free_communication, structured_communication)
        return super()._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)

    def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
        if qr_method == 'ch_qr':
            return {
                'barcode_type': 'QR',
                'width': 256,
                'height': 256,
                'quiet': 0,
                'mask': 'ch_cross',
                'value': '\n'.join(self._get_qr_vals(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)),
                # Swiss QR code requires Error Correction Level = 'M' by specification
                'barLevel': 'M',
            }
        return super()._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)

    def _get_partner_address_lines(self, partner):
        """ Retrieves the partner's address fields, truncated to respect the line specs.
        :returns: tuple(street, street_number, zip, city)
        """
        filter_text = self._l10n_ch_filter_text
        street_1_split = street_split(filter_text(partner.street))
        street_name = street_1_split['street_name']
        building_number = f"{street_1_split['street_number']} {street_1_split['street_number2']}".strip()

        if building_number:
            concatenated_building_number = f"{building_number} {filter_text(partner.street2)}".strip()
            if len(concatenated_building_number) <= 16:
                building_number = concatenated_building_number
        else:
            # Try to complete the address with street2
            street_2_split = street_split(filter_text(partner.street2))

            building_number = f"{street_2_split['street_number']} {street_2_split['street_number2']}".strip()
            if building_number:
                street_name = f"{street_name} {street_2_split['street_name']}".strip()
            else:
                building_number = filter_text(partner.street2)

        return street_name[:70], building_number[:16], partner.zip[:16], partner.city[:35]

    @api.model
    def _is_qr_reference(self, reference):
        """ Checks whether the given reference is a QR-reference, i.e. it is
        made of 27 digits, the 27th being a mod10r check on the 26 previous ones.
        """
        return reference \
            and len(reference) == 27 \
            and re.match(r'\d+$', reference) \
            and reference == mod10r(reference[:-1])

    @api.model
    def _is_iso11649_reference(self, reference):
        """ Checks whether the given reference is a ISO11649 (SCOR) reference.
        """
        return reference \
               and len(reference) >= 5 \
               and len(reference) <= 25 \
               and reference.startswith('RF') \
               and int(''.join(str(int(x, 36)) for x in clean(reference[4:] + reference[:4], ' -.,/:').upper().strip())) % 97 == 1
               # see https://github.com/arthurdejong/python-stdnum/blob/master/stdnum/iso11649.py

    def _l10n_ch_qr_debtor_check(self, debtor_partner):
        """  This method should be used in _get_error_messages_for_qr and _check_for_qr_code_errors
             It allows is to permit to set this qr method if a partner is not yet provided when executing _get_error_messages_for_qr
             while preventing to print qr code when executing _check_for_qr_code_errors if the partner is not provided
        """
        if not debtor_partner or debtor_partner.country_id.code not in ('CH', 'LI'):
            return _("The debtor partner's address isn't located in Switzerland.")
        return False

    def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency):
        def _get_error_for_ch_qr():
            error_messages = [_("The Swiss QR code could not be generated for the following reason(s):")]
            if self.acc_type != 'iban':
                error_messages.append(_("The account type isn't QR-IBAN or IBAN."))
            debtor_check = self._l10n_ch_qr_debtor_check(debtor_partner)
            if debtor_partner and debtor_check:
                error_messages.append(debtor_check)
            if currency.id not in (self.env.ref('base.EUR').id, self.env.ref('base.CHF').id):
                error_messages.append(_("The currency isn't EUR nor CHF."))
            return '\r\n'.join(error_messages) if len(error_messages) > 1 else None

        if qr_method == 'ch_qr':
            return _get_error_for_ch_qr()
        return super()._get_error_messages_for_qr(qr_method, debtor_partner, currency)

    def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
        def _partner_fields_set(partner):
            return partner.zip and \
                   partner.city and \
                   partner.country_id.code and \
                   (partner.street or partner.street2)

        if qr_method == 'ch_qr':
            if not _partner_fields_set(self.partner_id):
                return _("The partner set on the bank account meant to receive the payment (%s) must have a complete postal address (street, zip, city and country).", self.acc_number)

            if debtor_partner and not _partner_fields_set(debtor_partner):
                return _("The partner must have a complete postal address (street, zip, city and country).")

            if self.l10n_ch_qr_iban and not self._is_qr_reference(structured_communication):
                return _("When using a QR-IBAN as the destination account of a QR-code, the payment reference must be a QR-reference.")

            debtor_check = self._l10n_ch_qr_debtor_check(debtor_partner)
            if debtor_check:
                return debtor_check

        return super()._check_for_qr_code_errors(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)

    @api.model
    def _get_available_qr_methods(self):
        rslt = super()._get_available_qr_methods()
        rslt.append(('ch_qr', _("Swiss QR bill"), 10))
        return rslt
