"""SSH helpers"""
import asyncio
import logging

# STM-9317 suppress warnings until STM-5490 is fixed.
import warnings
from cryptography.utils import CryptographyDeprecationWarning

warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)

import asyncssh

asyncssh.set_log_level(logging.ERROR)


class NoBridgeError(Exception):
    pass


class SshClient():
    """Helper class for a SSH client.  User is responsible for retry/errors."""

    def __init__(self,
                 host,
                 port=None,
                 username=None,
                 password=None,
                 tunnel=None,
                 init_wait=0,
                 verbose=0,
                 record=False):
        self.host = host
        self.port = port or 22
        self.username = username
        self.password = password
        self.init_wait = init_wait
        self._tun = tunnel
        self.record = [] if record else None

        self.verbose = verbose
        self._conn = None
        self.stdin = self.stdout = self.stderr = None
        self.lock = None
        self.sem = asyncio.Semaphore(5)

    async def _open(self):
        if self._conn is None:
            if not self.lock:
                self.lock = asyncio.Lock()
            async with self.lock:
                if self._conn is None:
                    self._conn = await asyncio.wait_for(
                        asyncssh.connect(self.host,
                                         port=self.port,
                                         known_hosts=None,
                                         encryption_algs='+3des-cbc',
                                         kex_algs='+diffie-hellman-group1-sha1',
                                         server_host_key_algs=[
                                             'ssh-ed25519', 'rsa-sha2-256',
                                             'rsa-sha2-512', 'ssh-rsa', 'ssh-dss'
                                         ],
                                         tunnel=self._tun,
                                         username=self.username,
                                         password=self.password,
                                         preferred_auth=['password']),  # PPA-1420
                        timeout=5.0)

    async def close(self):
        """Close the connection and free any resources."""
        if self._conn:
            self._conn.close()
            self._conn = self.stdin = self.stdout = self.stderr = None

    async def tunnel(self, host, port=None, username=None, password=None):
        """Create a new client tunnelled through this one."""
        if self._conn is None:
            await self._open()
        return SshClient(host,
                         port,
                         username=username,
                         password=password,
                         tunnel=self._conn,
                         verbose=self.verbose)

    async def cmd(self, cmd, timeout=5.0, delimiter=None, term_type=None):
        """Send a command.  The response is returned."""
        await self._open()
        if self.stdin is None:
            try:
                self.stdin, self.stdout, self.stderr = await self._conn.open_session(
                    term_type=term_type, encoding='latin-1')  # PPA-1457
                if not delimiter:
                    # this sometimes helps "kick" a connection so the first
                    # command works when it wouldn't otherwise.  saw this with
                    # some ignitenet dropbear servers.  only done when
                    # the default delimiter is used because it can break the
                    # regular delimiter.
                    self.stdin.write('\n')
                    await asyncio.sleep(self.init_wait)
                    await self.stdout.read(2048)
                else:
                    r = await asyncio.wait_for(self.stdout.readuntil(delimiter),
                                               timeout=timeout)
            except asyncio.CancelledError:
                raise
            except Exception as err:
                logging.warning('Error establishing session to %s: %s', self.host, err)
                raise

        if self.verbose > 2:
            print('#!>{}'.format(cmd))
        token = delimiter or '--PRESEEM--'
        if delimiter is None:  # default behavior is we add a token to output
            cmd += '; echo -n {}\n'.format(token)
        self.stdin.write(cmd)
        r = await asyncio.wait_for(self.stdout.readuntil(token), timeout=timeout)
        if not delimiter:
            r = r[:-11]
        if self.verbose > 2:
            print('#!<{}'.format(r))
        if self.record is not None:
            rr = r[:-len(delimiter)] if delimiter else r
            self.record.append([cmd, rr])
        return r

    async def scmd(self, cmd, timeout=5.0):
        """Simpler version of cmd: Send a command, the response is returned."""
        async with self.sem:
            await self._open()
            r = await self._conn.run(cmd, timeout=timeout)
            if self.record is not None:
                self.record.append([cmd, r.stdout])
            return r.stdout

    async def get_brmacs(self, br_name):
        """Dict of Intf:[list of Brmacs] pairs is returned."""
        port_map = {}
        intf_brmacs = {}

        if not br_name:
            raise ValueError("br_name must not be empty")
        if not isinstance(br_name, str):
            raise TypeError("br_name must be a string")

        # Get port_map
        showstp = await self.scmd(rf"brctl showstp {br_name} | grep '^\w* (\w)'")
        showstp = showstp.split('\n') if showstp else []

        if not showstp:
            raise NoBridgeError(f'Invalid bridge {br_name}')

        for port in showstp:
            data = port.split() if port else []
            if len(data) == 2:
                vir_id = data[1].strip('()')
                port_map[vir_id] = data[0]
                intf_brmacs[data[0]] = []

        # Get brmacs
        showmacs = await self.scmd(f"brctl showmacs {br_name}")
        showmacs = showmacs.split('\n')[1:] if showmacs else []
        for row in showmacs:
            row = row.split() if row else []
            if row:
                # Sample row:       ['2', '60:22:32:39:4d:32', 'no', '0.83']
                if row[2] == 'no':  # not local
                    intf = port_map.get(row[0])
                    if intf:
                        intf_brmacs.get(intf).append(row[1])

        return intf_brmacs
