# 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 re

import attr

from ...constants import (
    BLOCK_ID_REGEX,
    PUBKEY_REGEX,
    SIGNATURE_REGEX,
    WS2P_HEAD_REGEX,
    WS2P_PRIVATE_PREFIX_REGEX,
    WS2P_PUBLIC_PREFIX_REGEX,
    WS2PID_REGEX,
)
from ...key import VerifyingKey
from ..block_id import BlockID
from ..document import MalformedDocumentError


@attr.s()
class API:
    private = attr.ib(type=str)
    public = attr.ib(type=str)

    re_inline = re.compile(
        f"WS2P({WS2P_PRIVATE_PREFIX_REGEX})?({WS2P_PUBLIC_PREFIX_REGEX})?"
    )

    @classmethod
    def from_inline(cls, inline: str):
        data = API.re_inline.match(inline)
        if data is None:
            raise MalformedDocumentError("WS2P API Document")
        private = "" if data.group(1) is None else data.group(1)
        public = "" if data.group(2) is None else data.group(2)
        return cls(private, public)

    def __str__(self) -> str:
        return f"WS2P{self.private}{self.public}"


@attr.s()
class Head:
    version = attr.ib(type=int)

    re_inline = re.compile(WS2P_HEAD_REGEX)

    @classmethod
    def from_inline(cls, inline: str, signature: str):
        try:
            data = Head.re_inline.match(inline)
            if data is None:
                raise MalformedDocumentError("Head")
            head = data.group(0).split(":")
            version = int(head[1]) if len(head) == 2 else 0
            return cls(version)
        except AttributeError:
            raise MalformedDocumentError("Head") from AttributeError

    def __str__(self) -> str:
        return "HEAD" if self.version == 0 else f"HEAD:{str(self.version)}"


@attr.s()
class HeadV0(Head):
    signature = attr.ib(type=str)
    api = attr.ib(type=API)
    head = attr.ib(type=Head)
    pubkey = attr.ib(type=str)
    block_id = attr.ib(type=BlockID)

    re_inline = re.compile(
        f"^(WS2P(?:{WS2P_PRIVATE_PREFIX_REGEX})?(?:{WS2P_PUBLIC_PREFIX_REGEX})?):\
({WS2P_HEAD_REGEX}):({PUBKEY_REGEX}):({BLOCK_ID_REGEX})(?::)?(.*)"
    )

    re_signature = re.compile(SIGNATURE_REGEX)

    @classmethod
    def from_inline(cls, inline: str, signature: str):
        try:
            data = HeadV0.re_inline.match(inline)
            if data is None:
                raise MalformedDocumentError("HeadV0")
            api = API.from_inline(data.group(1))
            head = Head.from_inline(data.group(2), "")
            pubkey = data.group(3)
            block_id = BlockID.from_str(data.group(4))
            offload = data.group(5)
            return cls(head.version, signature, api, head, pubkey, block_id), offload
        except AttributeError:
            raise MalformedDocumentError("HeadV0") from AttributeError

    def inline(self) -> str:
        values = (
            str(v)
            for v in attr.astuple(
                self,
                recurse=False,
                filter=attr.filters.exclude(
                    attr.fields(HeadV0).version,
                    attr.fields(HeadV0).signature,
                    attr.fields(HeadV0).api,
                ),
            )
        )
        return f'{str(self.api)}:{":".join(values)}'

    def check_signature(self, pubkey: str) -> bool:
        """
        Check if Head signature is from head pubkey

        :param pubkey: Pubkey to check signature upon
        :return:
        """
        verifying_key = VerifyingKey(pubkey)

        return verifying_key.check_signature(self.inline(), self.signature)


@attr.s()
class HeadV1(HeadV0):
    ws2pid = attr.ib(type=str)
    software = attr.ib(type=str)
    software_version = attr.ib(type=str)
    pow_prefix = attr.ib(type=int)

    re_inline = re.compile(
        "({ws2pid}):({software}):({software_version}):({pow_prefix})(?::)?(.*)".format(
            ws2pid=WS2PID_REGEX,
            software="[A-Za-z-_]+",
            software_version="[0-9]+[.][0-9]+[.][0-9]+[-\\w]*",
            pow_prefix="[0-9]+",
        )
    )

    @classmethod
    def from_inline(cls, inline: str, signature: str):
        try:
            v0, offload = HeadV0.from_inline(inline, signature)
            data = HeadV1.re_inline.match(offload)
            if data is None:
                raise MalformedDocumentError("HeadV1")
            ws2pid = data.group(1)
            software = data.group(2)
            software_version = data.group(3)
            pow_prefix = int(data.group(4))
            offload = data.group(5)
            return (
                cls(
                    v0.version,
                    v0.signature,
                    v0.api,
                    v0.head,
                    v0.pubkey,
                    v0.block_id,
                    ws2pid,
                    software,
                    software_version,
                    pow_prefix,
                ),
                offload,
            )
        except AttributeError:
            raise MalformedDocumentError("HeadV1") from AttributeError


@attr.s
class HeadV2(HeadV1):
    free_member_room = attr.ib(type=int)
    free_mirror_room = attr.ib(type=int)

    re_inline = re.compile(
        "({free_member_room}):({free_mirror_room})(?::)?(.*)".format(
            free_member_room="[0-9]+", free_mirror_room="[0-9]+"
        )
    )

    @classmethod
    def from_inline(cls, inline: str, signature: str):
        try:
            v1, offload = HeadV1.from_inline(inline, signature)
            data = HeadV2.re_inline.match(offload)
            if data is None:
                raise MalformedDocumentError("HeadV2")
            free_member_room = int(data.group(1))
            free_mirror_room = int(data.group(2))
            return (
                cls(
                    v1.version,
                    v1.signature,
                    v1.api,
                    v1.head,
                    v1.pubkey,
                    v1.block_id,
                    v1.ws2pid,
                    v1.software,
                    v1.software_version,
                    v1.pow_prefix,
                    free_member_room,
                    free_mirror_room,
                ),
                "",
            )
        except AttributeError:
            raise MalformedDocumentError("HeadV2") from AttributeError
