"""
Copyright  2014-2020 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 logging
from typing import Union

from aiohttp import ClientResponse

from duniterpy.api.client import Client, RESPONSE_AIOHTTP

logger = logging.getLogger("duniter/blockchain")

MODULE = "blockchain"

BLOCK_SCHEMA = {
    "type": "object",
    "properties": {
        "version": {"type": "number"},
        "currency": {"type": "string"},
        "nonce": {"type": "number"},
        "number": {"type": "number"},
        "time": {"type": "number"},
        "medianTime": {"type": "number"},
        "dividend": {"type": ["number", "null"]},
        "monetaryMass": {"type": ["number", "null"]},
        "issuer": {"type": "string"},
        "previousHash": {"type": ["string", "null"]},
        "previousIssuer": {"type": ["string", "null"]},
        "membersCount": {"type": "number"},
        "hash": {"type": "string"},
        "inner_hash": {"type": "string"},
        "identities": {"type": "array", "items": {"type": "string"}},
        "joiners": {"type": "array", "items": {"type": "string"}},
        "leavers": {"type": "array", "items": {"type": "string"}},
        "revoked": {"type": "array", "items": {"type": "string"}},
        "excluded": {"type": "array", "items": {"type": "string"}},
        "certifications": {"type": "array", "items": {"type": "string"}},
        "transactions": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "signatures": {"type": "array"},
                    "version": {"type": "number"},
                    "currency": {"type": "string"},
                    "issuers": {"type": "array", "items": {"type": "string"}},
                    "inputs": {"type": "array", "items": {"type": "string"}},
                    "unlocks": {"type": "array", "items": {"type": "string"}},
                    "outputs": {"type": "array", "item": {"type": "string"}},
                },
                "required": [
                    "signatures",
                    "version",
                    "currency",
                    "issuers",
                    "inputs",
                    "outputs",
                ],
            },
        },
        "signature": {"type": "string"},
    },
    "required": [
        "version",
        "currency",
        "nonce",
        "number",
        "time",
        "medianTime",
        "dividend",
        "monetaryMass",
        "issuer",
        "previousHash",
        "previousIssuer",
        "membersCount",
        "hash",
        "inner_hash",
        "identities",
        "joiners",
        "leavers",
        "excluded",
        "certifications",
        "transactions",
        "signature",
    ],
}

BLOCK_NUMBERS_SCHEMA = {
    "type": "object",
    "properties": {
        "result": {
            "type": "object",
            "properties": {"blocks": {"type": "array", "items": {"type": "number"}}},
            "required": ["blocks"],
        }
    },
    "required": ["result"],
}

PARAMETERS_SCHEMA = {
    "type": "object",
    "properties": {
        "currency": {"type": "string"},
        "c": {"type": "number"},
        "dt": {"type": "number"},
        "ud0": {"type": "number"},
        "sigPeriod": {"type": "number"},
        "sigStock": {"type": "number"},
        "sigWindow": {"type": "number"},
        "sigValidity": {"type": "number"},
        "sigQty": {"type": "number"},
        "sigReplay": {"type": "number"},
        "xpercent": {"type": "number"},
        "msValidity": {"type": "number"},
        "msPeriod": {"type": "number"},
        "stepMax": {"type": "number"},
        "medianTimeBlocks": {"type": "number"},
        "avgGenTime": {"type": "number"},
        "dtDiffEval": {"type": "number"},
        "percentRot": {"type": "number"},
        "udTime0": {"type": "number"},
        "udReevalTime0": {"type": "number"},
        "dtReeval": {"type": "number"},
    },
    "required": [
        "currency",
        "c",
        "dt",
        "ud0",
        "sigPeriod",
        "sigValidity",
        "sigQty",
        "xpercent",
        "sigStock",
        "sigWindow",
        "msValidity",
        "stepMax",
        "medianTimeBlocks",
        "avgGenTime",
        "dtDiffEval",
        "percentRot",
        "udTime0",
        "udReevalTime0",
        "dtReeval",
    ],
}

MEMBERSHIPS_SCHEMA = {
    "type": "object",
    "properties": {
        "pubkey": {"type": "string"},
        "uid": {"type": "string"},
        "sigDate": {"type": "string"},
        "memberships": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "version": {"type": "number"},
                    "currency": {"type": "string"},
                    "membership": {"type": "string"},
                    "blockNumber": {"type": "number"},
                    "written": {"type": ["number", "null"]},
                },
                "required": [
                    "version",
                    "currency",
                    "membership",
                    "blockNumber",
                    "blockHash",
                    "written",
                ],
            },
        },
    },
    "required": ["pubkey", "uid", "sigDate", "memberships"],
}

BLOCKS_SCHEMA = {"type": "array", "items": BLOCK_SCHEMA}

HARDSHIP_SCHEMA = {
    "type": "object",
    "properties": {"block": {"type": "number"}, "level": {"type": "number"}},
    "required": ["block", "level"],
}

DIFFICULTIES_SCHEMA = {
    "type": "object",
    "properties": {
        "block": {"type": "number"},
        "levels": {
            "type": "array",
            "items": [
                {
                    "type": "object",
                    "properties": {
                        "uid": {"type": "string"},
                        "level": {"type": "number"},
                    },
                    "required": ["uid", "level"],
                }
            ],
        },
    },
    "required": ["block", "levels"],
}

BRANCHES_SCHEMA = {"type": "object", "properties": {"blocks": BLOCKS_SCHEMA}}


async def parameters(client: Client) -> dict:
    """
    GET the blockchain parameters used by this node

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/parameters", schema=PARAMETERS_SCHEMA)


async def memberships(client: Client, search: str) -> dict:
    """
    GET list of Membership documents for UID/Public key

    :param client: Client to connect to the api
    :param search: UID/Public key
    :return:
    """
    return await client.get(
        MODULE + "/memberships/%s" % search, schema=MEMBERSHIPS_SCHEMA
    )


async def membership(client: Client, membership_signed_raw: str) -> ClientResponse:
    """
    POST a Membership document

    :param client: Client to connect to the api
    :param membership_signed_raw: Membership signed raw document
    :return:
    """
    return await client.post(
        MODULE + "/membership",
        {"membership": membership_signed_raw},
        rtype=RESPONSE_AIOHTTP,
    )


async def current(client: Client) -> dict:
    """
    GET the last accepted block

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/current", schema=BLOCK_SCHEMA)


async def block(
    client: Client, number: int = 0, block_raw: str = None, signature: str = None
) -> Union[dict, ClientResponse]:
    """
    GET/POST a block from/to the blockchain

    :param client: Client to connect to the api
    :param number: Block number to get
    :param block_raw: Block document to post
    :param signature: Signature of the block document issuer
    :return:
    """
    # POST block
    if block_raw is not None and signature is not None:
        return await client.post(
            MODULE + "/block",
            {"block": block_raw, "signature": signature},
            rtype=RESPONSE_AIOHTTP,
        )
    # GET block
    return await client.get(MODULE + "/block/%d" % number, schema=BLOCK_SCHEMA)


async def blocks(client: Client, count: int, start: int) -> list:
    """
    GET list of blocks from the blockchain

    :param client: Client to connect to the api
    :param count: Number of blocks
    :param start: First block number
    :return:
    """
    assert type(count) is int
    assert type(start) is int

    return await client.get(
        MODULE + "/blocks/%d/%d" % (count, start), schema=BLOCKS_SCHEMA
    )


async def hardship(client: Client, pubkey: str) -> dict:
    """
    GET hardship level for given member's public key for writing next block

    :param client: Client to connect to the api
    :param pubkey:  Public key of the member
    :return:
    """
    return await client.get(MODULE + "/hardship/%s" % pubkey, schema=HARDSHIP_SCHEMA)


async def difficulties(client: Client) -> dict:
    """
    GET difficulties levels for members into current window for writing next block

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/difficulties", schema=DIFFICULTIES_SCHEMA)


async def branches(client: Client) -> list:
    """
    GET current branches of the node (top block of each branch)

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/branches", schema=BRANCHES_SCHEMA)


async def newcomers(client: Client) -> dict:
    """
    GET the block numbers containing newcomers

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/newcomers", schema=BLOCK_NUMBERS_SCHEMA)


async def certifications(client: Client) -> dict:
    """
    GET the block numbers containing certifications

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/certs", schema=BLOCK_NUMBERS_SCHEMA)


async def joiners(client: Client) -> dict:
    """
    GET the block numbers containing joiners

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/joiners", schema=BLOCK_NUMBERS_SCHEMA)


async def actives(client: Client) -> dict:
    """
    GET the block numbers containing actives

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/actives", schema=BLOCK_NUMBERS_SCHEMA)


async def leavers(client: Client) -> dict:
    """
    GET the block numbers containing leavers

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/leavers", schema=BLOCK_NUMBERS_SCHEMA)


async def revoked(client: Client) -> dict:
    """
    GET the block numbers containing revoked members.

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/revoked", schema=BLOCK_NUMBERS_SCHEMA)


async def excluded(client: Client) -> dict:
    """
    GET the block numbers containing excluded

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/excluded", schema=BLOCK_NUMBERS_SCHEMA)


async def ud(client: Client) -> dict:
    """
    GET the block numbers containing universal dividend

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/ud", schema=BLOCK_NUMBERS_SCHEMA)


async def tx(client: Client) -> dict:
    """
    GET the block numbers containing transactions

    :param client: Client to connect to the api
    :return:
    """
    return await client.get(MODULE + "/with/tx", schema=BLOCK_NUMBERS_SCHEMA)
