# Copyright  2014-2022 Vincent Texier <vit@free.fr>
#
# DuniterPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DuniterPy 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import base64
import hashlib
import re
from typing import List, Optional, Sequence, Type, TypeVar

from ..constants import BLOCK_HASH_REGEX, G1_CURRENCY_CODENAME, PUBKEY_REGEX

# required to type hint cls in classmethod
from ..key import SigningKey, VerifyingKey
from .block_id import BlockID
from .certification import Certification
from .document import Document, MalformedDocumentError
from .identity import Identity
from .membership import Membership
from .revocation import Revocation
from .transaction import Transaction

BlockType = TypeVar("BlockType", bound="Block")

VERSION = 12


class Block(Document):
    """
    The class Block handles Block documents.

    .. note:: A block document is specified by the following format :

        | Version: VERSION
        | Type: Block
        | Currency: CURRENCY
        | Nonce: NONCE
        | Number: BLOCK_NUMBER
        | PoWMin: NUMBER_OF_ZEROS
        | Time: GENERATED_ON
        | MedianTime: MEDIAN_DATE
        | UniversalDividend: DIVIDEND_AMOUNT
        | Issuer: ISSUER_KEY
        | PreviousHash: PREVIOUS_HASH
        | PreviousIssuer: PREVIOUS_ISSUER_KEY
        | Parameters: PARAMETERS
        | MembersCount: WOT_MEM_COUNT
        | Identities:
        | PUBLIC_KEY:SIGNATURE:TIMESTAMP:USER_ID
        | ...
        | Joiners:
        | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
        | ...
        | Actives:
        | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
        | ...
        | Leavers:
        | PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
        | ...
        | Excluded:
        | PUBLIC_KEY
        | ...
        | Certifications:
        | PUBKEY_FROM:PUBKEY_TO:BLOCK_NUMBER:SIGNATURE
        | ...
        | Transactions:
        | COMPACT_TRANSACTION
        | ...
        | BOTTOM_SIGNATURE

    """

    re_type = re.compile("Type: (Block)\n")
    re_number = re.compile("Number: ([0-9]+)\n")
    re_powmin = re.compile("PoWMin: ([0-9]+)\n")
    re_time = re.compile("Time: ([0-9]+)\n")
    re_mediantime = re.compile("MedianTime: ([0-9]+)\n")
    re_universaldividend = re.compile("UniversalDividend: ([0-9]+)\n")
    re_unitbase = re.compile("UnitBase: ([0-9]+)\n")
    re_issuer = re.compile(f"Issuer: ({PUBKEY_REGEX})\n")
    re_issuers_frame = re.compile("IssuersFrame: ([0-9]+)\n")
    re_issuers_frame_var = re.compile("IssuersFrameVar: (0|-?[1-9]\\d{0,18})\n")
    re_different_issuers_count = re.compile("DifferentIssuersCount: ([0-9]+)\n")
    re_previoushash = re.compile(f"PreviousHash: ({BLOCK_HASH_REGEX})\n")
    re_previousissuer = re.compile(f"PreviousIssuer: ({PUBKEY_REGEX})\n")
    re_parameters = re.compile(
        "Parameters: ([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):\
([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):\
([0-9]+):([0-9]+):([0-9]+)\n"
    )
    re_memberscount = re.compile("MembersCount: ([0-9]+)\n")
    re_identities = re.compile("Identities:\n")
    re_joiners = re.compile("Joiners:\n")
    re_actives = re.compile("Actives:\n")
    re_leavers = re.compile("Leavers:\n")
    re_revoked = re.compile("Revoked:\n")
    re_excluded = re.compile("Excluded:\n")
    re_exclusion = re.compile(f"({PUBKEY_REGEX})\n")
    re_certifications = re.compile("Certifications:\n")
    re_transactions = re.compile("Transactions:\n")
    re_hash = re.compile(f"InnerHash: ({BLOCK_HASH_REGEX})\n")
    re_nonce = re.compile("Nonce: ([0-9]+)\n")

    fields_parsers = {
        **Document.fields_parsers,
        **{
            "Type": re_type,
            "Number": re_number,
            "PoWMin": re_powmin,
            "Time": re_time,
            "MedianTime": re_mediantime,
            "UD": re_universaldividend,
            "UnitBase": re_unitbase,
            "Issuer": re_issuer,
            "IssuersFrame": re_issuers_frame,
            "IssuersFrameVar": re_issuers_frame_var,
            "DifferentIssuersCount": re_different_issuers_count,
            "PreviousIssuer": re_previousissuer,
            "PreviousHash": re_previoushash,
            "Parameters": re_parameters,
            "MembersCount": re_memberscount,
            "Identities": re_identities,
            "Joiners": re_joiners,
            "Actives": re_actives,
            "Leavers": re_leavers,
            "Revoked": re_revoked,
            "Excluded": re_excluded,
            "Certifications": re_certifications,
            "Transactions": re_transactions,
            "InnerHash": re_hash,
            "Nonce": re_nonce,
        },
    }

    def __init__(
        self,
        number: int,
        powmin: int,
        time: int,
        mediantime: int,
        ud: Optional[int],
        unit_base: int,
        issuer: str,
        issuers_frame: int,
        issuers_frame_var: int,
        different_issuers_count: int,
        prev_hash: Optional[str],
        prev_issuer: Optional[str],
        parameters: Optional[Sequence[str]],
        members_count: int,
        identities: List[Identity],
        joiners: List[Membership],
        actives: List[Membership],
        leavers: List[Membership],
        revokations: List[Revocation],
        excluded: List[str],
        certifications: List[Certification],
        transactions: List[Transaction],
        inner_hash: str,
        nonce: int,
        signing_key: SigningKey = None,
        version: int = VERSION,
        currency: str = G1_CURRENCY_CODENAME,
    ) -> None:
        """
        Constructor

        :param number: the number of the block
        :param powmin: the powmin value of this block
        :param time: the timestamp of this block
        :param mediantime: the timestamp of the median time of this block
        :param ud: the dividend amount, or None if no dividend present in this block
        :param unit_base: the unit_base of the dividend, or None if no dividend present in this block
        :param issuer: the pubkey of the issuer of the block
        :param issuers_frame:
        :param issuers_frame_var:
        :param different_issuers_count: the count of issuers
        :param prev_hash: the previous block hash
        :param prev_issuer: the previous block issuer
        :param parameters: the parameters of the currency. Should only be present in block 0.
        :param members_count: the number of members found in this block
        :param identities: the self certifications declared in this block
        :param joiners: the joiners memberships via "IN" documents
        :param actives: renewed memberships via "IN" documents
        :param leavers: the leavers memberships via "OUT" documents
        :param revokations: revokations
        :param excluded: members excluded because of missing certifications
        :param certifications: certifications documents
        :param transactions: transactions documents
        :param inner_hash: the block hash
        :param nonce: the nonce value of the block
        :param signing_key: SigningKey instance to sign the document (default=None)
        :param version: Document version (default=block.VERSION)
        :param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
        """
        super().__init__(version, currency)

        documents_versions = max(
            max([1] + [i.version for i in identities]),
            max([1] + [m.version for m in actives + leavers + joiners]),
            max([1] + [r.version for r in revokations]),
            max([1] + [c.version for c in certifications]),
            max([1] + [t.version for t in transactions]),
        )
        if self.version < documents_versions:
            raise MalformedDocumentError(
                f"Block version is too low: {self.version} < {documents_versions}"
            )
        self.number = number
        self.powmin = powmin
        self.time = time
        self.mediantime = mediantime
        self.ud = ud
        self.unit_base = unit_base
        self.issuer = issuer
        self.issuers_frame = issuers_frame
        self.issuers_frame_var = issuers_frame_var
        self.different_issuers_count = different_issuers_count
        self.prev_hash = prev_hash
        self.prev_issuer = prev_issuer
        self.parameters = parameters
        self.members_count = members_count
        self.identities = identities
        self.joiners = joiners
        self.actives = actives
        self.leavers = leavers
        self.revoked = revokations
        self.excluded = excluded
        self.certifications = certifications
        self.transactions = transactions
        self.inner_hash = inner_hash
        self.nonce = nonce

        if signing_key is not None:
            self.sign(signing_key)

    @property
    def block_id(self) -> BlockID:
        """
        Return Block UID

        :return:
        """
        return BlockID(self.number, self.proof_of_work())

    @classmethod
    def from_parsed_json(cls: Type[BlockType], parsed_json_block: dict) -> BlockType:
        """
        Return a Block instance from the python dict produced when parsing json

        :param parsed_json_block: Block as a Python dict

        :return:
        """
        b = parsed_json_block  # alias for readability
        version = b["version"]
        currency = b["currency"]
        arguments = {  # arguments used to create block
            "currency": b["currency"],
            "number": b["number"],
            "powmin": b["powMin"],
            "time": b["time"],
            "mediantime": b["medianTime"],
            "ud": b["dividend"],
            "unit_base": b["unitbase"],
            "issuer": b["issuer"],
            "issuers_frame": b["issuersFrame"],
            "issuers_frame_var": b["issuersFrameVar"],
            "different_issuers_count": b["issuersCount"],
            "prev_hash": b["previousHash"],
            "prev_issuer": b["previousIssuer"],
            "parameters": b["parameters"],
            "members_count": b["membersCount"],
            "identities": b["identities"],
            "joiners": b["joiners"],
            "actives": b["actives"],
            "leavers": b["leavers"],
            "revokations": b["revoked"],
            "excluded": b["excluded"],
            "certifications": b["certifications"],
            "transactions": b["transactions"],
            "inner_hash": b["inner_hash"],
            "nonce": b["nonce"],
            "version": b["version"],
        }
        # parameters: Optional[Sequence[str]]
        if arguments["parameters"]:
            arguments["parameters"] = arguments["parameters"].split(":")
        # identities: List[Identity]
        arguments["identities"] = [
            Identity.from_inline(f"{i}\n", version=version, currency=currency)
            for i in arguments["identities"]
        ]
        # joiners: List[Membership]
        arguments["joiners"] = [
            Membership.from_inline(
                f"{i}\n", version=version, currency=currency, membership_type="IN"
            )
            for i in arguments["joiners"]
        ]
        # actives: List[Membership]
        arguments["actives"] = [
            Membership.from_inline(
                f"{i}\n", version=version, currency=currency, membership_type="IN"
            )
            for i in arguments["actives"]
        ]
        # leavers: List[Membership]
        arguments["leavers"] = [
            Membership.from_inline(
                f"{i}\n", version=version, currency=currency, membership_type="OUT"
            )
            for i in arguments["leavers"]
        ]
        # revokations: List[Revocation]
        arguments["revokations"] = [
            Revocation.from_inline(f"{i}\n", version=version, currency=currency)
            for i in arguments["revokations"]
        ]
        # certifications: List[Certification]
        arguments["certifications"] = [
            Certification.from_inline(
                arguments["inner_hash"], f"{i}\n", version=version, currency=currency
            )
            for i in arguments["certifications"]
        ]
        # transactions: List[Transaction]
        arguments["transactions"] = [
            Transaction.from_bma_history(i, currency=currency)
            for i in arguments["transactions"]
        ]

        block = cls(**arguments)

        # return the block with signature
        block.signature = b["signature"]
        return block

    @classmethod
    def from_signed_raw(cls: Type[BlockType], signed_raw: str) -> BlockType:
        """
        Create Block instance from signed raw format string

        :param signed_raw: Signed raw format string

        :return:
        """
        lines = signed_raw.splitlines(True)
        n = 0

        version = int(Block.parse_field("Version", lines[n]))
        n += 1

        Block.parse_field("Type", lines[n])
        n += 1

        currency = Block.parse_field("Currency", lines[n])
        n += 1

        number = int(Block.parse_field("Number", lines[n]))
        n += 1

        powmin = int(Block.parse_field("PoWMin", lines[n]))
        n += 1

        time = int(Block.parse_field("Time", lines[n]))
        n += 1

        mediantime = int(Block.parse_field("MedianTime", lines[n]))
        n += 1

        ud_match = Block.re_universaldividend.match(lines[n])
        ud = None
        unit_base = 0
        if ud_match is not None:
            ud = int(Block.parse_field("UD", lines[n]))
            n += 1

        unit_base = int(Block.parse_field("UnitBase", lines[n]))
        n += 1

        issuer = Block.parse_field("Issuer", lines[n])
        n += 1

        issuers_frame = Block.parse_field("IssuersFrame", lines[n])
        n += 1
        issuers_frame_var = Block.parse_field("IssuersFrameVar", lines[n])
        n += 1
        different_issuers_count = Block.parse_field("DifferentIssuersCount", lines[n])
        n += 1

        prev_hash = None
        prev_issuer = None
        if number > 0:
            prev_hash = str(Block.parse_field("PreviousHash", lines[n]))
            n += 1

            prev_issuer = str(Block.parse_field("PreviousIssuer", lines[n]))
            n += 1

        parameters = None
        if number == 0:
            try:
                params_match = Block.re_parameters.match(lines[n])
                if params_match is None:
                    raise MalformedDocumentError("Parameters")
                parameters = params_match.groups()
                n += 1
            except AttributeError:
                raise MalformedDocumentError("Parameters") from AttributeError

        members_count = int(Block.parse_field("MembersCount", lines[n]))
        n += 1

        identities = []
        joiners = []
        actives = []
        leavers = []
        revoked = []
        excluded = []
        certifications = []
        transactions = []

        if Block.re_identities.match(lines[n]) is not None:
            n += 1
            while Block.re_joiners.match(lines[n]) is None:
                selfcert = Identity.from_inline(
                    lines[n], version=version, currency=currency
                )
                identities.append(selfcert)
                n += 1

        if Block.re_joiners.match(lines[n]):
            n += 1
            while Block.re_actives.match(lines[n]) is None:
                membership = Membership.from_inline(
                    lines[n], version=version, currency=currency, membership_type="IN"
                )
                joiners.append(membership)
                n += 1

        if Block.re_actives.match(lines[n]):
            n += 1
            while Block.re_leavers.match(lines[n]) is None:
                membership = Membership.from_inline(
                    lines[n], version=version, currency=currency, membership_type="IN"
                )
                actives.append(membership)
                n += 1

        if Block.re_leavers.match(lines[n]):
            n += 1
            while Block.re_revoked.match(lines[n]) is None:
                membership = Membership.from_inline(
                    lines[n], version=version, currency=currency, membership_type="OUT"
                )
                leavers.append(membership)
                n += 1

        if Block.re_revoked.match(lines[n]):
            n += 1
            while Block.re_excluded.match(lines[n]) is None:
                revokation = Revocation.from_inline(
                    lines[n], version=version, currency=currency
                )
                revoked.append(revokation)
                n += 1

        if Block.re_excluded.match(lines[n]):
            n += 1
            while Block.re_certifications.match(lines[n]) is None:
                exclusion_match = Block.re_exclusion.match(lines[n])
                if exclusion_match is not None:
                    exclusion = exclusion_match.group(1)
                    excluded.append(exclusion)
                n += 1

        if Block.re_certifications.match(lines[n]):
            n += 1
            while Block.re_transactions.match(lines[n]) is None:
                certification = Certification.from_inline(
                    prev_hash, lines[n], version=version, currency=currency
                )
                certifications.append(certification)
                n += 1

        if Block.re_transactions.match(lines[n]):
            n += 1
            while not Block.re_hash.match(lines[n]):
                tx_lines = ""
                header_data = Transaction.re_header.match(lines[n])
                if header_data is None:
                    raise MalformedDocumentError(f"Compact transaction ({lines[n]})")
                issuers_num = int(header_data.group(2))
                inputs_num = int(header_data.group(3))
                unlocks_num = int(header_data.group(4))
                outputs_num = int(header_data.group(5))
                has_comment = int(header_data.group(6))
                sup_lines = 2
                tx_max = (
                    n
                    + sup_lines
                    + issuers_num * 2
                    + inputs_num
                    + unlocks_num
                    + outputs_num
                    + has_comment
                )
                for index in range(n, tx_max):
                    tx_lines += lines[index]
                n += tx_max - n
                transaction = Transaction.from_compact(tx_lines, currency=currency)
                transactions.append(transaction)

        inner_hash = Block.parse_field("InnerHash", lines[n])
        n += 1

        nonce = int(Block.parse_field("Nonce", lines[n]))
        n += 1

        signature = Block.parse_field("Signature", lines[n])

        block = cls(
            number,
            powmin,
            time,
            mediantime,
            ud,
            unit_base,
            issuer,
            issuers_frame,
            issuers_frame_var,
            different_issuers_count,
            prev_hash,
            prev_issuer,
            parameters,
            members_count,
            identities,
            joiners,
            actives,
            leavers,
            revoked,
            excluded,
            certifications,
            transactions,
            inner_hash,
            nonce,
            version=version,
            currency=currency,
        )

        # return block with signature
        block.signature = signature
        return block

    def raw(self) -> str:
        """
        Return Block in raw format

        :return:
        """
        doc = f"Version: {self.version}\n\
Type: Block\n\
Currency: {self.currency}\n\
Number: {self.number}\n\
PoWMin: {self.powmin}\n\
Time: {self.time}\n\
MedianTime: {self.mediantime}\n"
        if self.ud:
            doc += f"UniversalDividend: {self.ud}\n"

        doc += f"UnitBase: {self.unit_base}\n"

        doc += f"Issuer: {self.issuer}\n"

        doc += f"IssuersFrame: {self.issuers_frame}\n\
IssuersFrameVar: {self.issuers_frame_var}\n\
DifferentIssuersCount: {self.different_issuers_count}\n"

        if self.number == 0 and self.parameters is not None:
            str_params = ":".join([str(p) for p in self.parameters])
            doc += f"Parameters: {str_params}\n"
        else:
            doc += f"PreviousHash: {self.prev_hash}\n\
PreviousIssuer: {self.prev_issuer}\n"

        doc += f"MembersCount: {self.members_count}\n"

        doc += "Identities:\n"
        for identity in self.identities:
            doc += f"{identity.inline()}\n"

        doc += "Joiners:\n"
        for joiner in self.joiners:
            doc += f"{joiner.inline()}\n"

        doc += "Actives:\n"
        for active in self.actives:
            doc += f"{active.inline()}\n"

        doc += "Leavers:\n"
        for leaver in self.leavers:
            doc += f"{leaver.inline()}\n"

        doc += "Revoked:\n"
        for revokation in self.revoked:
            doc += f"{revokation.inline()}\n"

        doc += "Excluded:\n"
        for exclude in self.excluded:
            doc += f"{exclude}\n"

        doc += "Certifications:\n"
        for cert in self.certifications:
            doc += f"{cert.inline()}\n"

        doc += "Transactions:\n"
        for transaction in self.transactions:
            doc += f"{transaction.compact()}"

        doc += f"InnerHash: {self.inner_hash}\n"

        doc += f"Nonce: {self.nonce}\n"

        return doc

    def proof_of_work(self) -> str:
        """
        Return Proof of Work hash for the Block

        :return:
        """
        doc_str = (
            f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n{self.signature}\n"
        )
        return hashlib.sha256(doc_str.encode("ascii")).hexdigest().upper()

    def computed_inner_hash(self) -> str:
        """
        Return inner hash of the Block

        :return:
        """
        doc = self.raw()
        inner_doc = "\n".join(doc.split("\n")[:-3]) + "\n"
        return hashlib.sha256(inner_doc.encode("ascii")).hexdigest().upper()

    def sign(self, key: SigningKey) -> None:
        """
        Sign the current document with key

        :param key: Libnacl SigningKey instance

        :return:
        """
        string_to_sign = f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n"
        signature = base64.b64encode(key.signature(bytes(string_to_sign, "ascii")))
        self.signature = signature.decode("ascii")

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return NotImplemented
        return self.block_id == other.block_id

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
        return self.block_id < other.block_id

    def __gt__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
        return self.block_id > other.block_id

    def __le__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
        return self.block_id <= other.block_id

    def __ge__(self, other: object) -> bool:
        if not isinstance(other, Block):
            return False
        return self.block_id >= other.block_id

    def check_signature(self, pubkey: str):
        """
        Check if the signature is from pubkey

        :param pubkey: Base58 public key

        :return:
        """
        if self.signature is None:
            raise Exception("Signature is None, can not verify signature")

        content_to_verify = f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n"
        verifying_key = VerifyingKey(pubkey)
        return verifying_key.check_signature(content_to_verify, self.signature)
