import codecs
from pathlib import Path
import sys
from os import environ

import grpc

from lnd_grpc.config import *
import lnd_grpc.protos.rpc_pb2 as ln
from lnd_grpc.utilities import get_lnd_dir

# tell gRPC which cypher suite to use
environ["GRPC_SSL_CIPHER_SUITES"] = (
    "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:"
    "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:"
    "ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384"
)


class BaseClient:
    """
    A Base client which the other client services can build from. Can find tls cert and
    keys, and macaroons in 'default' locations based off lnd_dir and network parameters.

    Has some static helper methods for various applications.
    """

    def __init__(
        self,
        lnd_dir: str = None,
        macaroon_path: str = None,
        tls_cert_path: str = None,
        network: str = defaultNetwork,
        grpc_host: str = defaultRPCHost,
        grpc_port: str = defaultRPCPort,
    ):

        self.lnd_dir = lnd_dir
        self.macaroon_path = macaroon_path
        self.tls_cert_path = tls_cert_path
        self.network = network
        self.grpc_host = grpc_host
        self.grpc_port = str(grpc_port)
        self.channel = None
        self.connection_status = None
        self.connection_status_change = False
        self.grpc_options = GRPC_OPTIONS

    @property
    def lnd_dir(self):
        """
        try automatically if not set as object init attribute
        :return: lnd_dir
        """
        if self._lnd_dir:
            return self._lnd_dir
        else:
            self._lnd_dir = get_lnd_dir()
            return self._lnd_dir

    @lnd_dir.setter
    def lnd_dir(self, path):
        self._lnd_dir = path

    @property
    def tls_cert_path(self):
        """
        :return: tls_cert_path
        """
        if self._tls_cert_path is None:
            self._tls_cert_path = Path(self.lnd_dir) / defaultTLSCertFilename
        return str(self._tls_cert_path)

    @tls_cert_path.setter
    def tls_cert_path(self, path):
        self._tls_cert_path = path

    @property
    def tls_cert(self) -> bytes:
        """
        :return: tls.cert as bytestring
        """
        try:
            with open(self.tls_cert_path, "rb") as r:
                _tls_cert = r.read()
        except FileNotFoundError:
            sys.stderr.write("TLS cert not found at %s" % self.tls_cert_path)
            raise
        if not _tls_cert.startswith(b"-----BEGIN CERTIFICATE-----"):
            sys.stderr.write(
                    "TLS cert at %s did not start with b'-----BEGIN CERTIFICATE-----')"
                    % self.tls_cert_path
            )
        return _tls_cert

    @property
    def macaroon_path(self) -> str:
        """
        :return: macaroon path
        """
        if not self._macaroon_path:
            self._macaroon_path = (
                Path(self.lnd_dir)
                / f"{defaultDataDirname}/{defaultChainSubDirname}/bitcoin/"
                f"{self.network}/{defaultAdminMacFilename}"
            )
            return str(self._macaroon_path)
        else:
            return self._macaroon_path

    @macaroon_path.setter
    def macaroon_path(self, path: str):
        self._macaroon_path = path

    @property
    def macaroon(self):
        """
        try to open the macaroon and return it as a byte string
        """
        try:
            with open(self.macaroon_path, "rb") as f:
                macaroon_bytes = f.read()
                macaroon = codecs.encode(macaroon_bytes, "hex")
                return macaroon
        except FileNotFoundError:
            sys.stderr.write(
                f"Could not find macaroon in {self.macaroon_path}. This might happen"
                f"in versions of lnd < v0.5-beta or those not using default"
                f"installation path. Set client object's macaroon_path attribute"
                f"manually."
            )

    def metadata_callback(self, context, callback):
        """
        automatically incorporate the macaroon into all requests
        :return: macaroon callback
        """
        callback([("macaroon", self.macaroon)], None)

    def connectivity_event_logger(self, channel_connectivity):
        """
        Channel connectivity callback logger
        """
        self.connection_status = channel_connectivity._name_
        if (
            self.connection_status == "SHUTDOWN"
            or self.connection_status == "TRANSIENT_FAILURE"
        ):
            self.connection_status_change = True

    @property
    def combined_credentials(self) -> grpc.CallCredentials:
        """
        Combine ssl and macaroon credentials
        :return: grpc.composite_channel_credentials
        """
        cert_creds = grpc.ssl_channel_credentials(self.tls_cert)
        auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
        return grpc.composite_channel_credentials(cert_creds, auth_creds)

    @property
    def grpc_address(self) -> str:
        return str(self.grpc_host + ":" + self.grpc_port)

    @staticmethod
    def channel_point_generator(funding_txid, output_index):
        """
        Generate a ln.ChannelPoint object from a funding_txid and output_index
        :return: ln.ChannelPoint
        """
        return ln.ChannelPoint(
            funding_txid_str=funding_txid, output_index=int(output_index)
        )

    @staticmethod
    def lightning_address(pubkey, host):
        """
        Generate a ln.LightningAddress object from a pubkey + host
        :return: ln.LightningAddress
        """
        return ln.LightningAddress(pubkey=pubkey, host=host)

    @staticmethod
    def hex_to_bytes(hex_string: str):
        return bytes.fromhex(hex_string)

    @staticmethod
    def bytes_to_hex(bytestring: bytes):
        return bytestring.hex()