import base64
import inspect
import json
import re
import zlib
from contextlib import contextmanager
from datetime import datetime
from enum import Enum

import mt940

from .models import Holding


def mt940_to_array(data):
    data = data.replace("@@", "\r\n")
    data = data.replace("-0000", "+0000")
    transactions = mt940.models.Transactions()
    return transactions.parse(data)


def classproperty(f):
    class fx:
        def __init__(self, getter):
            self.getter = getter
        def __get__(self, obj, type=None):
            return self.getter(type)
    return fx(f)


def compress_datablob(magic: bytes, version: int, data: dict):
    data = dict(data)
    for k, v in data.items():
        if k.endswith("_bin"):
            if v:
                data[k] = base64.b64encode(v).decode("us-ascii")
    serialized = json.dumps(data).encode('utf-8')
    compressed = zlib.compress(serialized, 9)
    return b';'.join([magic, b'1', str(version).encode('us-ascii'), compressed])


def decompress_datablob(magic: bytes, blob: bytes, obj: object = None):
    if not blob.startswith(magic):
        raise ValueError("Incorrect data blob")
    s = blob.split(b';', 3)
    if len(s) != 4:
        raise ValueError("Incorrect data blob")
    if not s[1].isdigit() or not s[2].isdigit():
        raise ValueError("Incorrect data blob")
    encoding_version = int(s[1].decode('us-ascii'), 10)
    blob_version = int(s[2].decode('us-ascii'), 10)

    if encoding_version != 1:
        raise ValueError("Unsupported encoding version {}".format(encoding_version))

    decompressed = zlib.decompress(s[3])
    data = json.loads(decompressed.decode('utf-8'))
    for k, v in data.items():
        if k.endswith("_bin"):
            if v:
                data[k] = base64.b64decode(v.encode('us-ascii'))

    if obj:
        setfunc = getattr(obj, "_set_data_v{}".format(blob_version), None)
        if not setfunc:
            raise ValueError("Unknown data blob version")

        setfunc(data)
    else:
        return blob_version, data


class SubclassesMixin:
    @classmethod
    def _all_subclasses(cls):
        for subcls in cls.__subclasses__():
            yield from subcls._all_subclasses()
        yield cls


class DocTypeMixin:
    _DOC_TYPE = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        type_ = self._DOC_TYPE
        if type_ is None:
            if isinstance(getattr(self, 'type', None), type):
                type_ = getattr(self, 'type')

        if type_ is not None:
            if not self.__doc__:
                self.__doc__ = ""

            name = type_.__name__
            if type_.__module__ != 'builtins':
                name = "{}.{}".format(type_.__module__, name)

            self.__doc__ = self.__doc__ + "\n\n:type: :class:`{}`".format(name)


class FieldRenderFormatStringMixin:
    _FORMAT_STRING = None

    def _render_value(self, value):
        retval = self._FORMAT_STRING.format(value)
        self._check_value_length(retval)

        return retval


class FixedLengthMixin:
    _FIXED_LENGTH = [None, None, None]
    _DOC_TYPE = str

    def __init__(self, *args, **kwargs):
        for i, a in enumerate(('length', 'min_length', 'max_length')):
            kwargs[a] = self._FIXED_LENGTH[i] if len(self._FIXED_LENGTH) > i else None

        super().__init__(*args, **kwargs)


class ShortReprMixin:
    def __repr__(self):
        return "{}{}({})".format(
            "{}.".format(self.__class__.__module__),
            self.__class__.__name__,
            ", ".join(
                ("{!r}".format(value) if not name.startswith("_") else "{}={!r}".format(name, value))
                for (name, value) in self._repr_items
            )
        )

    def print_nested(self, stream=None, level=0, indent="    ", prefix="", first_level_indent=True, trailer="", print_doc=True, first_line_suffix=""):
        stream.write(
            ( (prefix + level*indent) if first_level_indent else "")
            + "{!r}{}{}\n".format(self, trailer, first_line_suffix)
        )


class MT535_Miniparser:
    re_identification = re.compile(r"^:35B:ISIN\s(.*)\|(.*)\|(.*)$")
    re_marketprice = re.compile(r"^:90B::MRKT\/\/ACTU\/([A-Z]{3})(\d*),{1}(\d*)$")
    re_pricedate = re.compile(r"^:98A::PRIC\/\/(\d*)$")
    re_pieces = re.compile(r"^:93B::AGGR\/\/UNIT\/(\d*),(\d*)$")
    re_totalvalue = re.compile(r"^:19A::HOLD\/\/([A-Z]{3})(\d*),{1}(\d*)$")
    re_acquisitionprice = re.compile(r"^:70E::HOLD\/\/\d*STK\|2(\d*?),{1}(\d*?)\+([A-Z]{3})$")

    def parse(self, lines):
        retval = []
        # First: Collapse multiline clauses into one clause
        clauses = self.collapse_multilines(lines)
        # Second: Scan sequence of clauses for financial instrument
        # sections
        finsegs = self.grab_financial_instrument_segments(clauses)
        # Third: Extract financial instrument data
        for finseg in finsegs:
            isin, name, market_price, price_symbol, price_date, pieces, acquisitionprice = (None,)*7
            for clause in finseg:
                # identification of instrument
                # e.g. ':35B:ISIN LU0635178014|/DE/ETF127|COMS.-MSCI EM.M.T.U.ETF I'
                m = self.re_identification.match(clause)
                if m:
                    isin = m.group(1)
                    name = m.group(3)
                # current market price
                # e.g. ':90B::MRKT//ACTU/EUR38,82'
                m = self.re_marketprice.match(clause)
                if m:
                    price_symbol = m.group(1)
                    market_price = float(m.group(2) + "." + m.group(3))
                # date of market price
                # e.g. ':98A::PRIC//20170428'
                m = self.re_pricedate.match(clause)
                if m:
                    price_date = datetime.strptime(m.group(1), "%Y%m%d").date()
                # number of pieces
                # e.g. ':93B::AGGR//UNIT/16,8211'
                m = self.re_pieces.match(clause)
                if m:
                    pieces = float(m.group(1) + "." + m.group(2))
                # total value of holding
                # e.g. ':19A::HOLD//EUR970,17'
                m = self.re_totalvalue.match(clause)
                if m:
                    total_value = float(m.group(2) + "." + m.group(3))
                # Acquisition price
                # e.g ':70E::HOLD//1STK23,968293+EUR'
                m = self.re_acquisitionprice.match(clause)
                if m:
                    acquisitionprice = float(m.group(1) + '.' + m.group(2))

            # processed all clauses
            retval.append(
                Holding(
                    ISIN=isin, name=name, market_value=market_price,
                    value_symbol=price_symbol, valuation_date=price_date,
                    pieces=pieces, total_value=total_value,
                    acquisitionprice=acquisitionprice))
        return retval

    def collapse_multilines(self, lines):
        clauses = []
        prevline = ""
        for line in lines:
            if line.startswith(":"):
                if prevline != "":
                    clauses.append(prevline)
                prevline = line
            elif line.startswith("-"):
                # last line
                clauses.append(prevline)
                clauses.append(line)
            else:
                prevline += "|{}".format(line)
        return clauses

    def grab_financial_instrument_segments(self, clauses):
        retval = []
        stack = []
        within_financial_instrument = False
        for clause in clauses:
            if clause.startswith(":16R:FIN"):
                # start of financial instrument
                within_financial_instrument = True
            elif clause.startswith(":16S:FIN"):
                # end of financial instrument - move stack over to
                # return value
                retval.append(stack)
                stack = []
                within_financial_instrument = False
            else:
                if within_financial_instrument:
                    stack.append(clause)
        return retval


class Password(str):
    protected = False

    def __init__(self, value):
        self.value = value
        self.blocked = False

    @classmethod
    @contextmanager
    def protect(cls):
        try:
            cls.protected = True
            yield None
        finally:
            cls.protected = False

    def block(self):
        self.blocked = True

    def __str__(self):
        if self.blocked and not self.protected:
            raise Exception("Refusing to use PIN after block")
        return '***' if self.protected else str(self.value)

    def __repr__(self):
        return self.__str__().__repr__()

    def __add__(self, other):
        return self.__str__().__add__(other)

    def replace(self, *args, **kwargs):
        return self.__str__().replace(*args, **kwargs)


class RepresentableEnum(Enum):
    def __init__(self, *args, **kwargs):
        Enum.__init__(self)

        # Hack alert: Try to parse the docstring from the enum source, if available. Fail softly.
        # FIXME Needs test
        try:
            val_1 = val_2 = repr(args[0])
            if val_1.startswith("'"):
                val_2 = '"' + val_1[1:-1] + '"'
            elif val_1.startswith('"'):
                val_2 = "'" + val_1[1:-1] + "'"
            regex = re.compile(r"^.*?\S+\s*=\s*(?:(?:{})|(?:{}))\s*#:\s*(\S.*)$".format(
                        re.escape(val_1), re.escape(val_2)))
            for line in inspect.getsourcelines(self.__class__)[0]:
                m = regex.match(line)
                if m:
                    self.__doc__ = m.group(1).strip()
                    break
        except:
            raise

    def __repr__(self):
        return "{}.{}.{}".format(self.__class__.__module__, self.__class__.__name__, self.name)

    def __str__(self):
        return self.value


def minimal_interactive_cli_bootstrap(client):
    """
    This is something you usually implement yourself to ask your user in a nice, user-friendly way about these things.
    This is mainly included to keep examples in the documentation simple and allow you to get started quickly.
    """
    # Fetch available TAN mechanisms by the bank, if we don't know it already. If the client was created with cached data,
    # the function is already set.
    if not client.get_current_tan_mechanism():
        client.fetch_tan_mechanisms()
        mechanisms = list(client.get_tan_mechanisms().items())
        if len(mechanisms) > 1:
            print("Multiple tan mechanisms available. Which one do you prefer?")
            for i, m in enumerate(mechanisms):
                print(i, "Function {p.security_function}: {p.name}".format(p=m[1]))
            choice = input("Choice: ").strip()
            client.set_tan_mechanism(mechanisms[int(choice)][0])

    if client.is_tan_media_required() and not client.selected_tan_medium:
        print("We need the name of the TAN medium, let's fetch them from the bank")
        m = client.get_tan_media()
        if len(m[1]) == 1:
            client.set_tan_medium(m[1][0])
        else:
            print("Multiple tan media available. Which one do you prefer?")
            for i, mm in enumerate(m[1]):
                print(i,
                      "Medium {p.tan_medium_name}: Phone no. {p.mobile_number_masked}, Last used {p.last_use}".format(
                          p=mm))
            choice = input("Choice: ").strip()
            client.set_tan_medium(m[1][int(choice)])
