# -*- coding: utf-8 -*-

# Copyright(C) 2010-2016 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.


from datetime import date, datetime
from binascii import crc32
import re

from weboob.capabilities.base import empty
from weboob.exceptions import BrowserQuestion

from .base import BaseObject, Field, StringField, DecimalField, IntField, \
                  UserError, Currency, NotAvailable
from .date import DateField
from .collection import CapCollection


__all__ = ['AccountNotFound', 'TransferError', 'TransferStep', 'Recipient',
           'Account', 'Transaction', 'Investment', 'Transfer', 'CapBank',
           'RecipientNotFound']


class AccountNotFound(UserError):
    """
    Raised when an account is not found.
    """

    def __init__(self, msg='Account not found'):
        UserError.__init__(self, msg)


class RecipientNotFound(UserError):
    """
    Raised when a recipient is not found.
    """

    def __init__(self, msg='Recipient not found'):
        UserError.__init__(self, msg)


class TransferError(UserError):
    """
    A transfer has failed.
    """


class BaseAccount(BaseObject, Currency):
    """
    Generic class aiming to be parent of :class:`Recipient` and
    :class:`Account`.
    """
    label =          StringField('Pretty label')
    currency =       StringField('Currency', default=None)
    iban =           StringField('International Bank Account Number')
    bank_name =      StringField('Bank Name')

    def __init__(self, id='0', url=None):
        BaseObject.__init__(self, id, url)

    @property
    def currency_text(self):
        return Currency.currency2txt(self.currency)

    @property
    def ban(self):
        """ Bank Account Number part of IBAN"""
        if not self.iban:
            return NotAvailable
        return self.iban[4:]


class Recipient(BaseAccount):
    """
    Recipient of a transfer.
    """
    enabled_at =     DateField('Date of availability')
    category =       StringField('Recipient category')


class Account(BaseAccount):
    """
    Bank account.
    """
    TYPE_UNKNOWN          = 0
    TYPE_CHECKING         = 1
    "Transaction, everyday transactions"
    TYPE_SAVINGS          = 2
    "Savings/Deposit, can be used for every banking"
    TYPE_DEPOSIT          = 3
    "Term of Fixed Deposit, has time/amount constraints"
    TYPE_LOAN             = 4
    "Loan account"
    TYPE_MARKET           = 5
    "Stock market or other variable investments"
    TYPE_JOINT            = 6
    "Joint account"
    TYPE_CARD             = 7
    "Card account"
    TYPE_LIFE_INSURANCE   = 8
    "Life insurances"
    TYPE_PEE              = 9
    "Employee savings PEE"
    TYPE_PERCO            = 10
    "Employee savings PERCO"
    TYPE_ARTICLE_83       = 11
    "Article 83"
    TYPE_RSP              = 12
    "Employee savings RSP"
    TYPE_PEA              = 13
    "Share savings"
    TYPE_CAPITALISATION   = 14
    "Life Insurance capitalisation"
    TYPE_PERP             = 15
    "Retirement savings"
    TYPE_MADELIN          = 16
    "Complementary retirement savings"

    type =      IntField('Type of account', default=TYPE_UNKNOWN)
    balance =   DecimalField('Balance on this bank account')
    coming =    DecimalField('Sum of coming movements')

    # card attributes
    paydate =   DateField('For credit cards. When next payment is due.')
    paymin =    DecimalField('For credit cards. Minimal payment due.')
    cardlimit = DecimalField('For credit cards. Credit limit.')

    number =    StringField('Shown by the bank to identify your account ie XXXXX7489')
    # market and lifeinssurance accounts
    valuation_diff = DecimalField('+/- values total')

    def __repr__(self):
        return "<Account id=%r label=%r>" % (self.id, self.label)


class Transaction(BaseObject):
    """
    Bank transaction.
    """
    TYPE_UNKNOWN       = 0
    TYPE_TRANSFER      = 1
    TYPE_ORDER         = 2
    TYPE_CHECK         = 3
    TYPE_DEPOSIT       = 4
    TYPE_PAYBACK       = 5
    TYPE_WITHDRAWAL    = 6
    TYPE_CARD          = 7
    TYPE_LOAN_PAYMENT  = 8
    TYPE_BANK          = 9
    TYPE_CASH_DEPOSIT  = 10
    TYPE_CARD_SUMMARY  = 11
    TYPE_DEFERRED_CARD = 12

    date =      DateField('Debit date on the bank statement')
    rdate =     DateField('Real date, when the payment has been made; usually extracted from the label or from credit card info')
    vdate =     DateField('Value date, or accounting date; usually for professional accounts')
    type =      IntField('Type of transaction, use TYPE_* constants', default=TYPE_UNKNOWN)
    raw =       StringField('Raw label of the transaction')
    category =  StringField('Category of the transaction')
    label =     StringField('Pretty label')
    amount =    DecimalField('Amount of the transaction')

    card =              StringField('Card number (if any)')
    commission =        DecimalField('Commission part on the transaction (in account currency)')

    # International
    original_amount =   DecimalField('Original amount (in another currency)')
    original_currency = StringField('Currency of the original amount')
    country =           StringField('Country of transaction')

    # Financial arbitrations
    investments =       Field('List of investments related to the transaction', list, default=[])

    def __repr__(self):
        return "<Transaction date=%r label=%r amount=%r>" % (self.date, self.label, self.amount)

    def unique_id(self, seen=None, account_id=None):
        """
        Get an unique ID for the transaction based on date, amount and raw.

        :param seen: if given, the method uses this dictionary as a cache to
                     prevent several transactions with the same values to have the same
                     unique ID.
        :type seen: :class:`dict`
        :param account_id: if given, add the account ID in data used to create
                           the unique ID. Can be useful if you want your ID to be unique across
                           several accounts.
        :type account_id: :class:`str`
        :returns: an unique ID encoded in 8 length hexadecimal string (for example ``'a64e1bc9'``)
        :rtype: :class:`str`
        """
        crc = crc32(str(self.date))
        crc = crc32(str(self.amount), crc)
        if not empty(self.raw):
            label = self.raw
        else:
            label = self.label

        crc = crc32(re.sub('[ ]+', ' ', label.encode("utf-8")), crc)

        if account_id is not None:
            crc = crc32(str(account_id), crc)

        if seen is not None:
            while crc in seen:
                crc = crc32("*", crc)

            seen.add(crc)

        return "%08x" % (crc & 0xffffffff)


class Investment(BaseObject):
    """
    Investment in a financial market.
    """

    label =              StringField('Label of stocks')
    code =               StringField('Identifier of the stock (ISIN code)')
    description =        StringField('Short description of the stock')
    quantity =           DecimalField('Quantity of stocks')
    unitprice =          DecimalField('Buy price of one stock')
    unitvalue =          DecimalField('Current value of one stock')
    valuation =          DecimalField('Total current valuation of the Investment')
    vdate     =          DateField('Value date of the valuation amount')
    diff =               DecimalField('Difference between the buy cost and the current valuation')
    diff_percent =       DecimalField('Difference in percentage between the buy cost and the current valuation')
    portfolio_share =    DecimalField('Percentage of the current amount relative to the total')

    # International
    original_currency = StringField('Currency of the original amount')
    original_valuation = DecimalField('Original valuation (in another currency)')
    original_unitvalue = DecimalField('Original unitvalue (in another currency)')
    original_unitprice = DecimalField('Original unitprice (in another currency)')
    original_diff = DecimalField('Original diff (in another currency)')


class TransferStep(BrowserQuestion):
    def __init__(self, transfer, *values):
        super(TransferStep, self).__init__(*values)
        self.transfer = transfer


class Transfer(BaseObject, Currency):
    """
    Transfer from an account to a recipient.
    """

    amount =          DecimalField('Amount to transfer')
    currency =        StringField('Currency', default=None)
    fees =            DecimalField('Fees', default=None)

    exec_date =       Field('Date of transfer', date, datetime)

    account_id =      StringField('ID of origin account')
    account_iban =    StringField('International Bank Account Number')
    account_label =   StringField('Label of origin account')
    account_balance = DecimalField('Balance of origin account before transfer')

    recipient_id =    StringField('ID of recipient account')
    recipient_iban =  StringField('International Bank Account Number')
    recipient_label = StringField('Label of recipient account')

    label =           StringField('Reason')


class CapBank(CapCollection):
    """
    Capability of bank websites to see accounts and transactions.
    """

    def iter_resources(self, objs, split_path):
        """
        Iter resources.

        Default implementation of this method is to return on top-level
        all accounts (by calling :func:`iter_accounts`).

        :param objs: type of objects to get
        :type objs: tuple[:class:`BaseObject`]
        :param split_path: path to discover
        :type split_path: :class:`list`
        :rtype: iter[:class:`BaseObject`]
        """
        if Account in objs:
            self._restrict_level(split_path)

            return self.iter_accounts()

    def iter_accounts(self):
        """
        Iter accounts.

        :rtype: iter[:class:`Account`]
        """
        raise NotImplementedError()

    def get_account(self, id):
        """
        Get an account from its ID.

        :param id: ID of the account
        :type id: :class:`str`
        :rtype: :class:`Account`
        :raises: :class:`AccountNotFound`
        """
        raise NotImplementedError()

    def iter_history(self, account):
        """
        Iter history of transactions on a specific account.

        :param account: account to get history
        :type account: :class:`Account`
        :rtype: iter[:class:`Transaction`]
        :raises: :class:`AccountNotFound`
        """
        raise NotImplementedError()

    def iter_coming(self, account):
        """
        Iter coming transactions on a specific account.

        :param account: account to get coming transactions
        :type account: :class:`Account`
        :rtype: iter[:class:`Transaction`]
        :raises: :class:`AccountNotFound`
        """
        raise NotImplementedError()

    def iter_investment(self, account):
        """
        Iter investment of a market account

        :param account: account to get investments
        :type account: :class:`Account`
        :rtype: iter[:class:`Investment`]
        :raises: :class:`AccountNotFound`
        """
        raise NotImplementedError()


class CapCgp(CapBank):
    """
    Capability of cgp website to see accounts and transactions.
    """

class CapBankTransfer(CapBank):
    def iter_transfer_recipients(self, account):
        """
        Iter recipients availables for a transfer from a specific account.

        :param account: account which initiate the transfer
        :type account: :class:`Account`
        :rtype: iter[:class:`Recipient`]
        :raises: :class:`AccountNotFound`
        """
        raise NotImplementedError()

    def init_transfer(self, transfer, **params):
        """
        Initiate a transfer.

        :param :class:`Transfer`
        :rtype: :class:`Transfer`
        :raises: :class:`TransferError`
        """
        raise NotImplementedError()

    def execute_transfer(self, transfer, **params):
        """
        Execute a transfer.

        :param :class:`Transfer`
        :rtype: :class:`Transfer`
        :raises: :class:`TransferError`
        """
        raise NotImplementedError()

    def transfer(self, transfer, **params):
        """
        Do a transfer from an account to a recipient.

        :param :class:`Transfer`
        :rtype: :class:`Transfer`
        :raises: :class:`TransferError`
        """
        t = self.init_transfer(transfer, **params)
        for key, value in t.iter_fields():
            if hasattr(transfer, key) and key != 'id':
                transfer_val = getattr(transfer, key)
                try:
                    if hasattr(self, 'transfer_check_%s' % key):
                        assert getattr(self, 'transfer_check_%s' % key)(transfer_val, value)
                    else:
                        assert transfer_val == value or empty(transfer_val)
                except AssertionError:
                    raise TransferError('%s changed during transfer processing (from %s to %s)' % (key, transfer_val, value))
        return self.execute_transfer(t, **params)
