#! /usr/bin/python3
import logging
import shutil
import tempfile

import coincurve
import functools

from .bitfield import bitfield
from .errors import SpecFileError
from .structure import Sequence
from .event import Event, MustNotMsg, ExpectMsg
from .utils import privkey_expand
from .keyset import KeySet
from abc import ABC, abstractmethod
from typing import Dict, Optional, List, Union, Any, Callable


class Conn(object):
    """Class for connections.  Details filled in by the particular runner."""

    def __init__(self, connprivkey: str):
        """Create a connection from a node with the given hex privkey: we use
        trivial values for private keys, so we simply left-pad with zeroes"""
        self.name = connprivkey
        self.connprivkey = privkey_expand(connprivkey)
        self.pubkey = coincurve.PublicKey.from_secret(self.connprivkey.secret)
        self.expected_error = False
        self.must_not_events: List[MustNotMsg] = []

    def __str__(self) -> str:
        return self.name


class Runner(ABC):
    """Abstract base class for runners.

    Most of the runner parameters can be extracted at runtime, but we do
    require that minimum_depth be 3, just for test simplicity.
    """

    def __init__(self, config: Any):
        self.config = config
        self.directory = tempfile.mkdtemp(prefix="lnpt-cl-")
        # key == connprivkey, value == Conn
        self.conns: Dict[str, Conn] = {}
        self.last_conn: Optional[Conn] = None
        self.stash: Dict[str, Dict[str, Any]] = {}
        self.logger = logging.getLogger(__name__)
        if self.config.getoption("verbose"):
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.INFO)

    def _is_dummy(self) -> bool:
        """The DummyRunner returns True here, as it can't do some things"""
        return False

    def find_conn(self, connprivkey: Optional[str]) -> Optional[Conn]:
        # Default is whatever we specified last.
        if connprivkey is None:
            return self.last_conn
        if connprivkey in self.conns:
            self.last_conn = self.conns[connprivkey]
            return self.last_conn
        return None

    def add_conn(self, conn: Conn) -> None:
        self.conns[conn.name] = conn
        self.last_conn = conn

    def disconnect(self, event: Event, conn: Conn) -> None:
        if conn is None:
            raise SpecFileError(event, "Unknown conn")
        del self.conns[conn.name]
        self.check_final_error(event, conn, conn.expected_error, conn.must_not_events)

    def check_error(self, event: Event, conn: Conn) -> Optional[str]:
        conn.expected_error = True
        return None

    def post_check(self, sequence: Sequence) -> None:
        # Make sure no connection had an error.
        for conn_name in list(self.conns.keys()):
            logging.debug(
                f"disconnection connection with key={conn_name} and value={self.conns[conn_name]}"
            )
            self.disconnect(sequence, self.conns[conn_name])

    def restart(self) -> None:
        self.conns = {}
        self.last_conn = None
        self.stash = {}

    # FIXME: Why can't we use SequenceUnion here?
    def run(self, events: Union[Sequence, List[Event], Event]) -> None:
        sequence = Sequence(events)
        self.start()
        while True:
            all_done = sequence.action(self)
            self.post_check(sequence)
            if all_done:
                self.stop()
                return
            self.restart()

    def add_stash(self, stashname: str, vals: Any) -> None:
        """Add a dict to the stash."""
        self.stash[stashname] = vals

    def get_stash(self, event: Event, stashname: str, default: Any = None) -> Any:
        """Get an entry from the stash."""
        if stashname not in self.stash:
            if default is not None:
                return default
            raise SpecFileError(event, "Unknown stash name {}".format(stashname))
        return self.stash[stashname]

    def teardown(self):
        """The Teardown method is called at the end of the test,
        and it is used to clean up the root dir where the tests are run."""
        shutil.rmtree(self.directory)

    def runner_features(
        self,
        additional_features: Optional[List[int]] = None,
        globals: bool = False,
    ) -> str:
        """
        Provide the features required by the node.
        """
        if additional_features is None:
            return ""
        else:
            return bitfield(*additional_features)

    @abstractmethod
    def is_running(self) -> bool:
        """Return a boolean value that tells whether the runner is running
        or not.
        Is leave up to the runner implementation to keep the runner state"""
        pass

    @abstractmethod
    def connect(self, event: Event, connprivkey: str) -> None:
        pass

    @abstractmethod
    def check_final_error(
        self,
        event: Event,
        conn: Conn,
        expected: bool,
        must_not_events: List[MustNotMsg],
    ) -> None:
        pass

    @abstractmethod
    def start(self) -> None:
        pass

    @abstractmethod
    def stop(self, print_logs: bool = False) -> None:
        """
        Stop the runner, and print all the log that the ln
        implementation produced.

        Print the log is useful when we have a failure e we need
        to debug what happens during the tests.
        """
        pass

    @abstractmethod
    def recv(self, event: Event, conn: Conn, outbuf: bytes) -> None:
        pass

    @abstractmethod
    def get_output_message(self, conn: Conn, event: ExpectMsg) -> Optional[bytes]:
        pass

    @abstractmethod
    def getblockheight(self) -> int:
        pass

    @abstractmethod
    def trim_blocks(self, newheight: int) -> None:
        pass

    @abstractmethod
    def add_blocks(self, event: Event, txs: List[str], n: int) -> None:
        pass

    @abstractmethod
    def expect_tx(self, event: Event, txid: str) -> None:
        pass

    @abstractmethod
    def invoice(self, event: Event, amount: int, preimage: str) -> None:
        pass

    @abstractmethod
    def accept_add_fund(self, event: Event) -> None:
        pass

    @abstractmethod
    def fundchannel(
        self,
        event: Event,
        conn: Conn,
        amount: int,
        feerate: int = 0,
        expect_fail: bool = False,
    ) -> None:
        pass

    @abstractmethod
    def init_rbf(
        self,
        event: Event,
        conn: Conn,
        channel_id: str,
        amount: int,
        utxo_txid: str,
        utxo_outnum: int,
        feerate: int,
    ) -> None:
        pass

    @abstractmethod
    def addhtlc(self, event: Event, conn: Conn, amount: int, preimage: str) -> None:
        pass

    @abstractmethod
    def get_keyset(self) -> KeySet:
        pass

    @abstractmethod
    def get_node_privkey(self) -> str:
        pass

    @abstractmethod
    def get_node_bitcoinkey(self) -> str:
        pass

    @abstractmethod
    def has_option(self, optname: str) -> Optional[str]:
        pass

    @abstractmethod
    def add_startup_flag(self, flag: str) -> None:
        pass

    @abstractmethod
    def close_channel(self, channel_id: str) -> None:
        """
        Close the channel with the specified channel id.

        :param channel_id:  the channel id as string value where the
        caller want to close;
        :return No value in case of success is expected,
        but an `RpcError` is expected in case of err.
        """
        pass


def remote_revocation_basepoint() -> Callable[[Runner, Event, str], str]:
    """Get the remote revocation basepoint"""

    def _remote_revocation_basepoint(runner: Runner, event: Event, field: str) -> str:
        return runner.get_keyset().revocation_basepoint()

    return _remote_revocation_basepoint


def remote_payment_basepoint() -> Callable[[Runner, Event, str], str]:
    """Get the remote payment basepoint"""

    def _remote_payment_basepoint(runner: Runner, event: Event, field: str) -> str:
        return runner.get_keyset().payment_basepoint()

    return _remote_payment_basepoint


def remote_delayed_payment_basepoint() -> Callable[[Runner, Event, str], str]:
    """Get the remote delayed_payment basepoint"""

    def _remote_delayed_payment_basepoint(
        runner: Runner, event: Event, field: str
    ) -> str:
        return runner.get_keyset().delayed_payment_basepoint()

    return _remote_delayed_payment_basepoint


def remote_htlc_basepoint() -> Callable[[Runner, Event, str], str]:
    """Get the remote htlc basepoint"""

    def _remote_htlc_basepoint(runner: Runner, event: Event, field: str) -> str:
        return runner.get_keyset().htlc_basepoint()

    return _remote_htlc_basepoint


def remote_funding_pubkey() -> Callable[[Runner, Event, str], str]:
    """Get the remote funding pubkey (FIXME: we assume there's only one!)"""

    def _remote_funding_pubkey(runner: Runner, event: Event, field: str) -> str:
        return (
            coincurve.PublicKey.from_secret(
                privkey_expand(runner.get_node_bitcoinkey()).secret
            )
            .format()
            .hex()
        )

    return _remote_funding_pubkey


def remote_funding_privkey() -> Callable[[Runner, Event, str], str]:
    """Get the remote funding privkey (FIXME: we assume there's only one!)"""

    def _remote_funding_privkey(runner: Runner, event: Event, field: str) -> str:
        return runner.get_node_bitcoinkey()

    return _remote_funding_privkey


def remote_per_commitment_point(n: int) -> Callable[[Runner, Event, str], str]:
    """Get the n'th remote per-commitment point"""

    def _remote_per_commitment_point(
        n: int, runner: Runner, event: Event, field: str
    ) -> str:
        return runner.get_keyset().per_commit_point(n)

    return functools.partial(_remote_per_commitment_point, n)


def remote_per_commitment_secret(n: int) -> Callable[[Runner, Event, str], str]:
    """Get the n'th remote per-commitment secret"""

    def _remote_per_commitment_secret(runner: Runner, event: Event, field: str) -> str:
        return runner.get_keyset().per_commit_secret(n)

    return _remote_per_commitment_secret
