# SPDX-FileCopyrightText: 2024 <wmj.py@gmx.com>
#
# SPDX-License-Identifier: LGPL-3.0-or-later

# -*- coding: utf-8 -*-
'''
.. note::

    license: GNU Lesser General Public License v3.0 (see LICENSE)
'''
from typing import (
    Optional,
    Any,
    Iterator,
    Dict,
    Union,
    List,
)

from deprecated.sphinx import deprecated

from ..utils import (
    raiseCsb43Exception,
    b_left_pad,
    b_right_pad,
    check_strict,
    messages as msg,
    export_date,
    export_decimal,
)
from ..i18n import tr as _
from .item import Item
from .record import RecordSequence
from .exchange import Exchange


@deprecated(
    version="0.10.0",
    reason="use csb43.aeb43.transaction.Transaction"
)
class Transaction(RecordSequence):
    '''
    A Csb43 transaction / (es) Movimiento

    - Create a :class:`Transaction` object from a `CSB43` string record::

        >>> from csb43.csb43 import Transaction
        >>> record = b'22    000000000000000000000200000000012345000000000000000000000\
0                '
        >>> t = Transaction(record)

    - From an empty object to a `CSB43` string record::

        >>> t = Transaction()
        >>> t.amount = 123.45
        >>> bytes(t)
        b'22    000000000000000000000200000000012345000000000000000000000\
0                '
    '''

    MAX_ITEMS = 5

    def __init__(
        self,
        record: Optional[bytes] = None,
        informationMode=3,
        **record_settings
    ):
        '''
        :param record: csb record
        :type record: :class:`basestring` or `None`

        :param informationMode: define mode used for transaction parsing, \
        mostly for reference checksum
        :type informationMode: :class:`int`

        :param \*\*record_settings: see :class:`csb43.csb43.RecordSequence`

        :raises: :class:`csb43.utils.Csb43Exception`

        '''
        super().__init__(**record_settings)
        self.__items: List[Item] = []
        self.__exchange: Optional[Exchange] = None
        self.informationMode = informationMode

        self.__padding: Optional[bytes] = None
        self.__branchCode: Optional[str] = None
        self.__transaction_date: Optional[bytes] = None
        self.__effective_date: Optional[bytes] = None
        self.__sharedItem: Optional[str] = None
        self.__ownItem: Optional[str] = None
        self.__debitOrCredit: Optional[bytes] = None
        self.__amount: Optional[bytes] = None
        self.__documentNumber: Optional[bytes] = None
        self.__reference1: Optional[str] = None
        self.__reference2: Optional[str] = None

        if record is not None:
            chk_args = self._get_check_args()

            record = self.str_encode(record)
            if not Transaction.is_valid(record):
                self._raise_error(msg.BAD_RECORD(record))

            # base 1
            # libre 3-6 -
            self._set_padding(record[2:6], **chk_args)
            # clave oficina origen 7-10 N
            self._set_branch_code(record[6:10], **chk_args)
            # operation date
            self._set_transaction_date(record[10:16], **chk_args)
            # effective date
            self._set_effective_date(record[16:22], **chk_args)
            # concepto comun
            self._set_shared_item(record[22:24], **chk_args)
            # concepto propio
            self._set_own_item(record[24:27], **chk_args)
            # debe o haber
            self._set_expense_or_income(record[27:28], **chk_args)
            # importe
            self._set_amount(record[28:42], **chk_args)
            # num. de documento
            self._set_document_number(record[42:52], **chk_args)
            # referencia 1
            self._set_reference_1(record[52:64], **chk_args)
            # referencia 2
            self._set_reference_2(record[64:80], **chk_args)

    @staticmethod
    def is_valid(record: bytes) -> bool:
        return (
            isinstance(record, (str, bytes))
            and (len(record) == 80) and (record[0:2] == b'22')
        )

    @property
    def informationMode(self):
        return self.str_decode(self.__informationMode)

    @informationMode.setter
    def informationMode(self, value):
        if value:
            value = self._text2field(value)
        self.__informationMode = value

    # exchange
    #
    # repr: Exchange
    # user in: exchange, record
    # user out: Exchange

    @property
    def exchange(self):
        """exchange object / (es) objecto de cambio de divisa

:rtype: :class:`Exchange`"""
        return self.__exchange

    @exchange.setter
    def exchange(self, value):
        self.add_exchange(value)

    def add_exchange(self, record: Union[bytes, Exchange], update=False):
        '''
        Add a new additional exchange record to the transaction

        :param record: csb exchange record or object
        :type record: :class:`Exchange` or :class:`basestring`

        :param update: update the current exchange object if it exists
        :type update: :class:`bool`

        :raises: :class:`csb43.utils.Csb43Exception`

        Add an exchange object from a `CSB43` string record::

            >>> t = Transaction()
            >>> record = '{: <80}'.format('240197800000000012345')
            >>> t.add_exchange(record)
            >>> str(t.exchange)
            '24019780000000001234\
5                                                           '

        Add an exchange object from a :class:`Exchange`::

            >>> from csb43.csb43 import Exchange
            >>> t = Transaction()
            >>> e = Exchange()
            >>> e.amount = 123.45
            >>> e.sourceCurrency = 'EUR'
            >>> t.add_exchange(e)
            >>> str(t.exchange)
            '240197800000000012345                                                           '

        '''
        if isinstance(record, Exchange):
            exchange = record
        elif isinstance(record, (str, bytes)):
            exchange = Exchange(record, **self._settings())
        else:
            raiseCsb43Exception(
                msg.T_INCOMPATIBLE_OBJECT.format(
                    found=record,
                    expected=", ".join(("Exchange", "basestring"))
                ),
                True
            )

        if (self.__exchange is not None) and not update:
            self._raise_error(
                _("maximum number of exchange record reached (%s)") % 1
            )
        self.__exchange = exchange

    # expense_or_income
    #
    # repr: bytes
    # user in: none (through amount)
    # user out: none (through amount)

    # debit or credit
    @check_strict(br'^[12]$')
    def _set_expense_or_income(self, value: bytes, **chk_args):
        self.__debitOrCredit = value

    def _get_expense_or_income(self) -> Optional[bytes]:
        return self.__debitOrCredit

    # amount
    #
    # repr: bytes
    # user in: text, number
    # user out: number

    @check_strict(br'^\d{14}$')
    def _set_amount(self, value: bytes, **chk_args):
        self.__amount = value

    @property
    def amount(self):
        """amount of the transaction / (es) cantidad implicada \
en el movimiento

:rtype: :class:`Decimal`

Quantities can be assigned as numbers or strings, and they will be truncated to
adjust the decimal format set for the object::

    >>> t = Transaction()
    >>> t.amount = 123
    >>> t.amount
    Decimal('123')
    >>> t.amount = 123.45
    >>> t.amount
    Decimal('123.45')
    >>> t.amount = '1234.56'
    >>> t.amount
    Decimal('1234.56')
    >>> t.amount = 1.2345
    >>> t.amount
    Decimal('1.23')"""
        if self.__amount is None:
            return None
        return self._format_currency(self.__amount,
                                     debit=self.__debitOrCredit)

    @amount.setter
    def amount(self, value):
        v = float(value)
        quantity, flag_sign = self._unformat_currency(v)
        field = "%014d" % quantity

        self._set_amount(self.str_encode(field))
        self._set_expense_or_income(self.str_encode(flag_sign))

    # document_number
    #
    # repr: bytes
    # user in: text, number
    # user out: number

    @check_strict(br'^\d{10}$')
    def _set_document_number(self, value: bytes, **chk_args):
        self.__documentNumber = value

    @property
    def documentNumber(self):
        """document number / (es) número del documento

:rtype: :class:`int`

>>> t = Transaction()
>>> t.documentNumber = 1
>>> t.documentNumber
1"""
        if self.__documentNumber is None:
            return None
        return int(self.__documentNumber)

    @documentNumber.setter
    def documentNumber(self, value):
        field = self._enc_left_pad(value, n=10, fill=b"0")
        self._set_document_number(field)

    # reference1
    #
    # repr: text
    # user in: bytes,text
    # user out: text

    def _set_reference_1(self, value: bytes, **chk_args):
        # modalidad 1 y 2 = libre, modalidad 3 = checksum
        if self.informationMode == '3':
            self._set_reference_1_mode_3(value, **chk_args)
        else:
            self._set_reference_1_other_modes(value, **chk_args)

    @check_strict(br'^\d{12}$')
    def _set_reference_1_mode_3(self, value: bytes, **chk_args):
        text = self.str_decode(value)
        Transaction._validate_reference(text or '', **chk_args)
        self.__reference1 = text

    @check_strict(br'^[ \w]{12}$')
    def _set_reference_1_other_modes(self, value: bytes, **chk_args):
        text = self.str_decode(value)
        self.__reference1 = text.rstrip(' ') if text is not None else None

    @property
    def reference1(self):
        """first reference (checksummed) / (es) primera \
referencia (verificada)

When using the information mode 3, the field is checksummed.

:rtype: :class:`str`

>>> t = Transaction()
>>> t.informationMode = 3
>>> t.reference1 = '012345678900'
>>> t.reference1
'012345678900'
>>> from csb43.utils import Csb43Exception
>>> try:
...     t.reference1 = '012345678901'
... except Csb43Exception as e:
...     print(e)
...
Validación fallida para el campo de referencia '012345678901'"""
        return self.__reference1

    @reference1.setter
    def reference1(self, value):
        value = self.str_encode(value)
        self._set_reference_1(value, **self._get_check_args())

    @staticmethod
    def _validate_reference(value: str, **chk_args) -> str:
        n = value.strip(' ')
        try:
            int(n)
            control = int(n[-1])
            res = (
                sum(
                    int(x) * ((i % 8) + 2) for (i, x)
                    in enumerate(reversed(n[:-1]))
                ) % 11
            ) % 10

            if res != control:
                raiseCsb43Exception(
                    _("Validation failed for reference '%s'") % value,
                    **chk_args
                )

            return n
        except ValueError:
            return n

    # reference2
    #
    # repr: text
    # user in: bytes,text
    # user out: text

    @check_strict(br'^[\x20-\xFF]{16}$')
    def _set_reference_2(self, value: bytes, **chk_args):
        text = self.str_decode(value)
        self.__reference2 = text.rstrip(' ') if text is not None else None

    @property
    def reference2(self):
        """second reference (not checksummed) / (es) \
segunda referencia (no verificada)

:rtype: :class:`str`

>>> t = Transaction()
>>> t.reference2 = '{: >16}'.format('something')
>>> t.reference2
'       something'"""
        return self.__reference2

    @reference2.setter
    def reference2(self, value):
        value = self.str_encode(value)
        self._set_reference_2(value)

    # optional_items
    #
    # repr: [item]
    # user in: none
    # user out: [item]

    @property
    def optionalItems(self):
        """list of optional items / (es) lista de \
conceptos adicionales

:rtype: :class:`list` of :class:`Item` attached to this transaction"""
        return self.__items

    def add_item(self, record: Union[bytes, Item]):
        '''
        Add a new optional item to the transaction.

        :param record: item record
        :type record: :class:`Item` or :class:`basestring`

        :raises: :class:`csb43.utils.Csb43Exception` when the record is \
        impossible to parse, or if the maximum number of complementary items \
        has been reached
        '''

        if len(self.__items) == Transaction.MAX_ITEMS:
            self._raise_error(
                _("the maximum number of complementary items"
                  " for transaction has been reached: %d") % Transaction.MAX_ITEMS
            )
        if isinstance(record, (str, bytes)):
            self.__items.append(Item(record, **self._settings()))
        elif isinstance(record, Item):
            self.__items.append(record)
        else:
            raiseCsb43Exception(
                msg.T_INCOMPATIBLE_OBJECT.format(
                    found=record,
                    expected=", ".join(("Item", "basestring"))
                ),
                True
            )

    # padding
    #
    # repr: bytes
    # user in: text
    # user out: bytes

    @check_strict(br'^.{4}$')
    def _set_padding(self, value: bytes, **chk_args):
        self.__padding = value.rstrip(b' ')

    @property
    def padding(self):
        return self.__padding

    @padding.setter
    def padding(self, value):
        value = self.str_encode(value)
        self._set_padding(value)

    # branch_code
    #
    # repr: bytes
    # user in: text
    # user out: bytes

    @check_strict(br'^\d{4}$')
    def _set_branch_code(self, value: bytes, **chk_args):
        self.__branchCode = self.str_decode(value)

    @property
    def branchCode(self):
        """branch code / (es) código de sucursal u oficina

:rtype: :class:`str`

>>> t = Transaction()
>>> t.branchCode = '0000'
>>> t.branchCode
'0000'"""
        return self.__branchCode

    @branchCode.setter
    def branchCode(self, value):
        field = self._enc_left_pad(value, n=4, fill=b"0")
        self._set_branch_code(field)

    # transaction_date
    #
    # repr: bytes
    # user in: datetime.date
    # user out: datetime.date

    @check_strict(br'^\d{6}$')
    def _set_transaction_date(self, value: bytes, **chk_args):
        self.__transaction_date = value

    @property
    def transactionDate(self):
        """transaction date / (es) fecha de la operación

:rtype: :class:`datetime.date`

Setting a date::

    >>> from datetime import date
    >>> t = Transaction()
    >>> t.transactionDate = date(year=2012,month=2,day=13)
    >>> t.transactionDate
    datetime.date(2012, 2, 13)"""
        if self.__transaction_date is None:
            return None
        return self._format_date(self.str_decode(self.__transaction_date))

    @transactionDate.setter
    def transactionDate(self, value):
        value = self.str_encode(self._unformat_date(value))
        self._set_transaction_date(value)

    # effective date
    #
    # repr: bytes
    # user in: datetime
    # user out: datetime

    @check_strict(br'^\d{6}$')
    def _set_effective_date(self, value: bytes, **chk_args):
        self.__effective_date = value

    @property
    def valueDate(self):
        """value date / (es) fecha valor

:rtype: :class:`datetime.date`

Setting a date::

    >>> from datetime import date
    >>> t = Transaction()
    >>> t.valueDate = date(year=2012,month=2,day=13)
    >>> t.valueDate
    datetime.date(2012, 2, 13)"""
        if self.__effective_date is None:
            return None
        return self._format_date(self.str_decode(self.__effective_date))

    @valueDate.setter
    def valueDate(self, value):
        value = self.str_encode(self._unformat_date(value))
        self._set_effective_date(value)

    # shared_item
    #
    # repr: text
    # user in: bytes,text, number
    # user out: text

    @check_strict(br'^\d{2}$')
    def _set_shared_item(self, value: bytes, **chk_args):
        self.__sharedItem = self.str_decode(value)

    @property
    def sharedItem(self):
        """inter-bank shared item / (es) concepto común

:rtype: :class:`str`

>>> t = Transaction()
>>> t.sharedItem = 12
>>> t.sharedItem
'12'
>>> t.sharedItem = '04'
>>> t.sharedItem
'04'
>>> from csb43 import utils
>>> utils.CONCEPTOS[t.sharedItem]
'GIROS - TRANSFERENCIAS - TRASPASOS - CHEQUES'"""
        return self.__sharedItem

    @sharedItem.setter
    def sharedItem(self, value):
        field = self._enc_left_pad(value, n=2, fill=b"0")
        self._set_shared_item(field)

    # own item
    #
    # repr: text
    # user in: bytes, text, number
    # user out: text

    @check_strict(br'^\d{3}$')
    def _set_own_item(self, value: bytes, **chk_args):
        self.__ownItem = self.str_decode(value)

    @property
    def ownItem(self):
        """own item (given by each bank to its transactions) / \
(es) concepto propio del banco

:rtype: :class:`str`

>>> t = Transaction()
>>> t.ownItem = 123
>>> t.ownItem
'123'
>>> t.ownItem = '125'
>>> t.ownItem
'125'"""
        return self.__ownItem

    @ownItem.setter
    def ownItem(self, value):
        field = self._enc_left_pad(value, n=3, fill=b"0")
        self._set_own_item(field)

    def __str_main_record(self) -> bytes:
        ':rtype: representation of this object as a `CSB43` record'
        return (
            b"22"
            + b_right_pad(self.padding or b'', 4)
            + b_left_pad(self.str_encode(self.branchCode) or b'', 4, b'0')
            + b_left_pad(self.__transaction_date or b'', 6, b'0')
            + b_left_pad(self.__effective_date or b'', 6, b'0')
            + b_left_pad(self.str_encode(self.__sharedItem) or b'', 2, b'0')
            + b_left_pad(self.str_encode(self.__ownItem) or b'', 3, b'0')
            + (self.__debitOrCredit or b'1')
            + b_left_pad(self.__amount or b'', 14, b'0')
            + b_left_pad(self.__documentNumber or b'', 10, b'0')
            + b_left_pad(self.str_encode(self.__reference1) or b'', 12, b'0')
            + b_right_pad(self.str_encode(self.__reference2) or b'', 16)
        )

    def __iter__(self) -> Iterator[bytes]:
        ''':rtype: iterator of all the `CSB43` records that this object
        represents

>>> [x for x in t] # doctest: +SKIP
['22    000000000012021300000200000000123456000000000001234567890\
0       something', '2301first item recor\
d                                                           '\
, '2302second item recor\
d                                                          '\
, '24018400000000001230\
0                                                           ']

        '''
        yield self.__str_main_record()
        # optional items
        for item in self.optionalItems:
            yield from item
        # Exchange
        exchange = self.exchange
        if exchange:
            yield from exchange

    def __bytes__(self) -> bytes:
        r''':rtype: representation of this object as `CSB43` records
        (using `\\n` as separator)

>>> bytes(t) # doctest: +SKIP
b'22    000000000012021300000200000000123456000000000001234567890\
0       something\n2301first item recor\
d                                                           \n2302\
second item recor\
d                                                          \n2401840000000\
00012300                                                           '
        '''
        return b'\n'.join(x for x in self)

    def as_dict(self, decimal_fallback=None) -> Dict[str, Any]:
        '''
        :param decimal_fallback: decimal number fallback representation
        :type record: :class:`bool`

        :rtype: a representation of this object as a :class:`dict`. \
        The keys will be localised

        >>> t = Transaction()
        >>> t.amount = '123.45'
        >>> t.as_dict() # doctest: +SKIP
        {u'cantidad': Decimal('123.45'), u'primera_referencia': None, \
         u'segunda_referencia': None, u'concepto_propio': None, \
         u'fecha_de_operacion': None, u'numero_del_documento': None, \
         u'codigo_de_sucursal': None, u'concepto_comun': None, \
         u'fecha_valor': None}
        '''
        d = {
            msg.T_BRANCH_CODE: self.branchCode,
            msg.T_TRANSACTION_DATE: export_date(self.transactionDate),
            msg.T_VALUE_DATE: export_date(self.valueDate),
            msg.T_SHARED_ITEM: self.sharedItem,
            msg.T_OWN_ITEM: self.ownItem,
            msg.T_AMOUNT: export_decimal(self.amount, fallback=decimal_fallback),
            msg.T_DOCUMENT_NUMBER: self.documentNumber,
            msg.T_REFERENCE_1: self.reference1,
            msg.T_REFERENCE_2: self.reference2,
        }

        if len(self.optionalItems):
            d[msg.T_OPTIONAL_ITEMS] = [
                x.as_dict(decimal_fallback=decimal_fallback)
                for x in self.optionalItems
            ]

        if self.exchange:
            d[msg.T_EXCHANGE] = self.exchange.as_dict(decimal_fallback=decimal_fallback)

        return d
