import json
import os
import subprocess
import tarfile
from enum import IntEnum, unique, auto
from time import sleep
from typing import List, TYPE_CHECKING, Union

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.builder.transaction_builder import CallTransactionBuilder, DeployTransactionBuilder, TransactionBuilder
from iconsdk.exception import JSONRPCException
from iconsdk.icon_service import IconService
from iconsdk.libs.in_memory_zip import gen_deploy_data_content
from iconsdk.providers.http_provider import HTTPProvider
from iconsdk.signed_transaction import SignedTransaction
from iconsdk.wallet.wallet import KeyWallet
from iconservice.base.address import GOVERNANCE_ADDRESS, SYSTEM_ADDRESS
from iconservice.base.type_converter_templates import ConstantKeys
from iconservice.icon_constant import Revision

from tbears.config.tbears_config import TEST_ACCOUNTS, TEST1_PRIVATE_KEY
from tbears.util.arg_parser import uri_parser


DEBUG = 1
DIR_PATH = os.path.abspath(os.path.dirname(__file__))
DEFAULT_STEP_LIMIT = 1_000_000_000
DEFAULT_NID = 3
ICX = 10 ** 18


def debug_print(msg: str):
    if DEBUG:
        print(msg)


@unique
class SyncType(IntEnum):
    REVISION = 0
    GOVERNANCE_SCORE = auto()
    STEP_COST = auto()


sync_list = [
    {
        "type": SyncType.GOVERNANCE_SCORE,
        "params": {
            "version": "0.0.5"
        }
    },
    {
        "type": SyncType.GOVERNANCE_SCORE,
        "params": {
            "version": "0.0.6"
        }
    },
    {
        "type": SyncType.STEP_COST,
        "params": {
            "stepType": "apiCall",
            "cost": "0x2710"
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x4",      # not errata
            "name": "1.2.3"
        }
    },
    {
        "type": SyncType.GOVERNANCE_SCORE,
        "params": {
            "version": "0.0.7"
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x4",
            "name": "1.3.3"
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x5",      # IISS prevote
            "name": "1.4.1"
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x6",      # Decentralization
            "name": "1.5.15"
        }
    },
    {
        "type": SyncType.GOVERNANCE_SCORE,
        "params": {
            "version": "1.0.0"  # support network proposal
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x8",
            "name": "1.5.20"
        }
    },
    {
        "type": SyncType.GOVERNANCE_SCORE,
        "params": {
            "version": "1.1.1"
        }
    },
    {
        "type": SyncType.REVISION,
        "params": {
            "code": "0x9",
            "name": "1.7.3"
        }
    },
]


class Sync:
    PREPS = TEST_ACCOUNTS[:4]

    def __init__(self):
        self.handlers = [
            self._set_revision,
            self._update_governance_score,
            self._set_step_cost,
        ]
        self.revision = 0
        self.gs_version = "0.0.0"

        self.key_wallet = KeyWallet.load(bytes.fromhex(TEST1_PRIVATE_KEY))
        self.preps = []
        for prep in self.PREPS:
            self.preps.append(KeyWallet.load(prep))
        uri, version = uri_parser("http://127.0.0.1:9000/api/v3")
        self.icon_service = IconService(HTTPProvider(uri, version))

    def apply(self):
        # get status
        response = self._get_revision()
        self.revision = int(response.get("code", "0x0"), 16)

        # start sync
        for i, action in enumerate(sync_list):
            print("")
            type_: int = action.get("type", -1)
            params = action.get("params", {})
            self.handlers[type_](params)

    @staticmethod
    def archive():
        root = "./"
        with tarfile.open('mainnet.tar.gz', 'w:gz') as tar:
            for dir in [".score", ".statedb"]:
                for _, _, files in os.walk(os.path.join(root, dir)):
                    for f in files:
                        tar.add(os.path.join(root, dir, f), arcname=f)

    def _is_network_proposal_enabled(self):
        return self.revision >= Revision.DECENTRALIZATION.value and self.gs_version >= "1.0.0"

    def _set_revision(self, params: dict):
        revision = int(params.get('code'), 16)
        print(f"## Set revision {revision}")
        if self.revision > revision:
            print(f"pass revision {revision}. {revision} < {self.revision}")
            return

        if not self._is_network_proposal_enabled():
            # send setRevision TX
            tx_hash = self._call_tx(key_wallet=self.key_wallet,
                                    to=GOVERNANCE_ADDRESS,
                                    method='setRevision',
                                    params=params)
            print(f"transaction hash: {tx_hash}")
        else:
            # register setRevision network proposal and approve it
            register = self.preps[0]
            np_id = self._register_proposal_tx(key_wallet=register,
                                               title=f"set revision {revision}",
                                               desc=f"T-Bears set revision {revision}",
                                               type=1,
                                               value_dict=params)
            for prep in self.preps:
                if prep == register:
                    continue
                tx_hash = self._vote_proposal_tx(key_wallet=prep,
                                                 id_=np_id,
                                                 vote=1)
                debug_print(f"{prep.get_address()} votes agree to {np_id}. transaction hash: {tx_hash}")

            print(f"Network proposal ID: {np_id}")

        self.revision = revision

        if self.revision == Revision.IISS.value:
            # make 4 P-Reps for network proposal
            self._make_preps(self.preps)

        if self.revision == Revision.DECENTRALIZATION.value:
            response = self._call(to=SYSTEM_ADDRESS, method="getIISSInfo")
            block_height = int(response.get("blockHeight"), 16)
            next_calc = int(response.get("nextCalculation"), 16)
            wait_time = (next_calc - block_height) * 2
            print(f"Wait decentralization..... {wait_time} secs")
            sleep(wait_time)
            while True:
                response = self._call(to=SYSTEM_ADDRESS, method="getIISSInfo")
                print(response.get("nextPRepTerm"))
                if response.get("nextPRepTerm", "0x0") != "0x0":
                    print(f"Decentralization is done.")
                    break
                else:
                    sleep(1)

    def _update_governance_score(self, params: dict):
        version = params.get('version')
        print(f"## Update governance SCORE to {version}")

        path = os.path.join(DIR_PATH, f"data/governance_{version}.zip")
        self._deploy_score(key_wallet=self.key_wallet,
                           score_address=GOVERNANCE_ADDRESS,
                           path=path)

        self.gs_version = version

    def _set_step_cost(self, params: dict):
        step_type = params.get('stepType')
        cost = params.get('cost')
        print(f"## Set stepCost.{step_type} to {cost}")
        if not self._is_network_proposal_enabled():
            # send setStepCost TX
            tx_hash = self._call_tx(key_wallet=self.key_wallet,
                                    to=GOVERNANCE_ADDRESS,
                                    method='setStepCost',
                                    params=params)
            print(f"transaction hash: {tx_hash}")
        else:
            # TODO send setStepCost network proposal and approve it
            print(f"Network proposal : ID")

    def _deploy_score(
            self,
            key_wallet: 'KeyWallet',
            path: str,
            score_address: str = SYSTEM_ADDRESS,
    ) -> dict:
        # Generates an instance of transaction for deploying SCORE.
        transaction = DeployTransactionBuilder() \
            .from_(key_wallet.get_address()) \
            .to(score_address) \
            .step_limit(100_000_000_000) \
            .nid(3) \
            .nonce(100) \
            .content_type("application/zip") \
            .content(gen_deploy_data_content(path)) \
            .build()

        # Returns the signed transaction object having a signature
        signed_transaction = SignedTransaction(transaction, key_wallet)

        # send transaction
        return self.icon_service.send_transaction(signed_transaction)

    def _call(self,
              to: str,
              method: str,
              params: dict = {}) -> Union[dict, str]:
        call = CallBuilder() \
            .from_(self.key_wallet.get_address()) \
            .to(to) \
            .method(method) \
            .params(params) \
            .build()

        return self.icon_service.call(call)

    def _tx(self,
            key_wallet: 'KeyWallet',
            to: str,
            value: int = 0,
            step_limit: int = DEFAULT_STEP_LIMIT,
            nid: int = DEFAULT_NID,
            nonce: int = 0) -> str:
        transaction = TransactionBuilder() \
            .from_(key_wallet.get_address()) \
            .to(to) \
            .value(value) \
            .step_limit(step_limit) \
            .nid(nid) \
            .nonce(nonce) \
            .build()

        # Returns the signed transaction object having a signature
        signed_transaction = SignedTransaction(transaction, key_wallet)

        # Send transaction
        return self.icon_service.send_transaction(signed_transaction)

    def _call_tx(self,
                 key_wallet: 'KeyWallet',
                 to: str,
                 method: str,
                 params: dict = {},
                 value: int = 0,
                 step_limit: int = DEFAULT_STEP_LIMIT,
                 nid: int = DEFAULT_NID,
                 nonce: int = 0) -> str:
        transaction = CallTransactionBuilder() \
            .from_(key_wallet.get_address()) \
            .to(to) \
            .value(value) \
            .step_limit(step_limit) \
            .nid(nid) \
            .nonce(nonce) \
            .method(method) \
            .params(params) \
            .build()

        # Returns the signed transaction object having a signature
        signed_transaction = SignedTransaction(transaction, key_wallet)

        # Send transaction
        return self.icon_service.send_transaction(signed_transaction)

    def _get_revision(self) -> dict:
        try:
            return self._call(to=GOVERNANCE_ADDRESS, method="getRevision")
        except JSONRPCException:
            return {"code": "0x0", "name": "0.0.0"}

    def _register_proposal_tx(self,
                              key_wallet: 'KeyWallet',
                              title: str,
                              desc: str,
                              type: int,
                              value_dict: dict,
                              value: int = 0,
                              step_limit: int = DEFAULT_STEP_LIMIT,
                              nid: int = DEFAULT_NID,
                              nonce: int = 0) -> str:
        params = {
            "title": title,
            "description": desc,
            "type": hex(type),
            "value": "0x" + bytes.hex(json.dumps(value_dict).encode())
        }
        return self._call_tx(key_wallet=key_wallet,
                             to=GOVERNANCE_ADDRESS,
                             method="registerProposal",
                             params=params,
                             value=value,
                             step_limit=step_limit,
                             nid=nid,
                             nonce=nonce)

    def _vote_proposal_tx(self,
                          key_wallet: 'KeyWallet',
                          id_: str,
                          vote: int,
                          value: int = 0,
                          step_limit: int = DEFAULT_STEP_LIMIT,
                          nid: int = DEFAULT_NID,
                          nonce: int = 0) -> 'str':
        params = {
            "id": id_,
            "vote": hex(vote)
        }

        return self._call_tx(key_wallet=key_wallet,
                             to=GOVERNANCE_ADDRESS,
                             method="voteProposal",
                             params=params,
                             value=value,
                             step_limit=step_limit,
                             nid=nid,
                             nonce=nonce)

    def _make_preps(self, preps: List[KeyWallet]):
        print(f"#### make P-Reps")
        delegate_value = self.icon_service.get_total_supply() * 2 // 1000
        transfer_value = delegate_value + 3000 * ICX
        for prep in preps:
            # transfer ICX
            tx_hash = self._tx(key_wallet=self.key_wallet,
                               to=prep.get_address(),
                               value=transfer_value)
            debug_print(f"transfer {transfer_value} to {prep.get_address()} tx_hash: {tx_hash}")
        while True:
            sleep(1)
            balance = self.icon_service.get_balance(preps[0].get_address())
            if balance >= transfer_value:
                break

        for prep in preps:
            # register P-Rep
            name = f"node{prep.get_address()}"
            params = {
                ConstantKeys.NAME: name,
                ConstantKeys.COUNTRY: "KOR",
                ConstantKeys.CITY: "Unknown",
                ConstantKeys.EMAIL: f"{name}@example.com",
                ConstantKeys.WEBSITE: f"https://{name}.example.com",
                ConstantKeys.DETAILS: f"https://{name}.example.com/details",
                ConstantKeys.P2P_ENDPOINT: f"{name}.example.com:7100",
            }
            tx_hash = self._call_tx(key_wallet=prep,
                                    to=SYSTEM_ADDRESS,
                                    value=2000 * ICX,
                                    method="registerPRep",
                                    params=params)
            debug_print(f"register {prep.get_address()} tx_hash: {tx_hash}")

            # stake
            tx_hash = self._call_tx(key_wallet=prep,
                                    to=SYSTEM_ADDRESS,
                                    method="setStake",
                                    params={"value": hex(delegate_value)})
            debug_print(f"set stake {prep.get_address()} tx_hash: {tx_hash}")
            # delegate
            tx_hash = self._call_tx(key_wallet=prep,
                                    to=SYSTEM_ADDRESS,
                                    method="setDelegation",
                                    params={
                                        "delegations": [
                                            {
                                                "address": prep.get_address(),
                                                "value": hex(delegate_value)
                                            }
                                        ]
                                    })
            debug_print(f"delegate {prep.get_address()} tx_hash: {tx_hash}")

        while True:
            sleep(1)
            response = self._call(to=SYSTEM_ADDRESS, method="getPReps")
            if len(response.get("preps", 0)) > 0:
                print(f"make 4 P-Reps")
                for prep in preps:
                    address = prep.get_address()
                    resp = self._call(to=SYSTEM_ADDRESS,
                                      method="getPRep",
                                      params={"address": address})
                    print(f" - {address}, delegated: {int(resp.get('delegated'), 16)//ICX:,} ICX")
                print(self._call(to=SYSTEM_ADDRESS, method="getIISSInfo"))
                break

    def check_revision(self) -> int:
        current_revision = int(self._get_revision().get("code"), 16)
        latest_revision = self._get_latest_revision()

        return current_revision - latest_revision


    @staticmethod
    def _get_latest_revision() -> int:
        for cmd in reversed(sync_list):
            if cmd.get("type") == SyncType.REVISION:
                return int(cmd["params"]["code"], 16)
        return 0


if __name__ == '__main__':
    subprocess.run(['tbears', 'stop'], stdout=subprocess.DEVNULL)
    subprocess.run(['tbears', 'clear'], stdout=subprocess.DEVNULL)
    subprocess.run(['tbears', 'start'], stdout=subprocess.DEVNULL)

    syncer = Sync()
    syncer.apply()

    subprocess.run(['tbears', 'stop'], stdout=subprocess.DEVNULL)
    subprocess.run(['tar', 'czvf', '../../../tbears/data/mainnet.tar.gz', '.score/', '.statedb/'], stdout=subprocess.DEVNULL)
