import logging
import textwrap
import time
from typing import List, Tuple, Union, Dict, Any, Optional

import os
import paramiko

FlagConf = List[Dict[str, Any]]
LimitCreds = Optional[List[str]]
SUDO_OPT = Optional[str]
logger = logging.getLogger(__name__)


def find_flags(ssh: paramiko.SSHClient, base_dir: str = '/root') -> List[Tuple[str, str]]:
    search_glob = os.path.join(base_dir, '*flag*')

    _, stdout, stderr = ssh.exec_command('ls {}'.format(search_glob))

    files = []
    found = []
    for line in stdout.read().splitlines():
        files.append(line.decode('utf-8'))

    for file in files:
        contents = get_file_contents(ssh, file)
        if 10 < len(contents) < 60:
            found.append((file, contents))

    return found


def get_file_contents(ssh: paramiko.SSHClient, file: str, sudo: SUDO_OPT = None) -> Union[str, bool]:
    """
    Retrieve the contents of file from the remote server.

    This does ***NOT*** use SFTP since teams might disallow SFTP. Granted if you are
    smart enough to disallow SFTP, you probably don't have default creds either, but
    sometimes people mess up.

    :param ssh: The current ssh session to use
    :param file: The full path to the file to retrieve
    :param sudo: The sudo password to use if the current session can sudo
    """
    ret = get_file(ssh, file, sudo)
    if ret:
        ret = ret.decode('utf-8').strip()
    return ret


def get_file(ssh: paramiko.SSHClient, file: str, sudo: SUDO_OPT = None) -> Union[bytes, bool]:
    """
    Retrieve a file from the remote server.

    This does ***NOT*** use SFTP since teams might disallow SFTP. Granted if you are
    smart enough to disallow SFTP, you probably don't have default creds either, but
    sometimes people mess up.

    :param ssh: The current ssh session to use
    :param file: The full path to the file to retrieve
    :param sudo: The sudo password to use if the current session can sudo
    """
    if sudo:
        logger.debug("Using sudo")
        _, stdout, stderr = run_sudo(ssh, 'cat {}'.format(file), sudo)
    else:
        _, stdout, stderr = ssh.exec_command('cat {}'.format(file))

    err = stderr.read().decode('utf-8').strip()
    if len(err) > 0:
        log_error('There was an error getting the file', file, err)
        return False

    return stdout.read()


def get_directory(ssh: paramiko.SSHClient, path: str, sudo: Optional[str] = None) -> Union[List[str], bool]:
    """
    Retrieve a directory listing on a remote server.

    This can optionally be run with sudo.

    :param ssh: The current ssh session to use
    :param path: The full path of the directory to list
    :param sudo: The sudo password to use if the current session can sudo
    :return: A list of all files in the directory
    """
    if sudo:
        logger.debug("Using sudo")
        _, stdout, stderr = run_sudo(ssh, 'ls {}'.format(path), sudo)
    else:
        _, stdout, stderr = ssh.exec_command('ls {}'.format(path))

    err = stderr.read().decode('utf-8').strip()
    if len(err) > 0:
        log_error('THere was an error getting the directory', path, err)
        return False

    return stdout.readlines()


def expand_wildcard(ssh: paramiko.SSHClient, path: str, sudo: Optional[str] = None) -> Optional[List[str]]:
    """
    Expand a wildcard on the remote system.

    This can optionally be run with sudo.

    This is equivalent to running ``ls -1dp PATH`` on the remote system.

    Example:

    >>> expand_wildcard(ssh, '/etc/krb*')
    ... ['/etc/krb5.conf', '/etc/krb5.keytab']
    >>> expand_wildcard(ssh, '/etc/cron.*')
    ... ['/etc/cron.d/', '/etc/cron.daily/']

    :param ssh: The current ssh session to use
    :param path: The full path of the directory to list
    :param sudo: The sudo password to use if the current session can sudo
    :return: A list of all expanded file paths
    """
    command = 'ls -1dp {}'.format(path)
    if sudo:
        logger.debug("Using sudo")
        _, stdout, stderr = run_sudo(ssh, command, sudo)
    else:
        _, stdout, stderr = ssh.exec_command(command)

    err = stderr.read().decode('utf-8').strip()
    if len(err) > 0:
        log_error('There was an error expanding the wildcard', path, err)
        return None

    return stdout.readlines()


def run_command(ssh: paramiko.SSHClient, command: str) -> str:
    _, stdout, _ = ssh.exec_command(command)
    return stdout.read().decode('utf-8').strip()


def get_system_info(ssh: paramiko.SSHClient) -> str:
    hostname = run_command(ssh, 'hostname -f')
    uname = run_command(ssh, 'uname -a')
    lsb_release = run_command(ssh, 'lst_release -a')
    whoami = run_command(ssh, 'whoami')

    sysinfo = [
        'Hostname: {}'.format(hostname),
        'Username: {}'.format(whoami),
        'Uname: {}'.format(uname),
    ]

    if len(lsb_release) > 0:
        sysinfo.append('LSB Release:\n{}'.format(textwrap.indent(lsb_release, ' > ')))
    return '\n'.join(sysinfo)


def can_sudo(ssh: paramiko.SSHClient, password: str) -> bool:
    logger.debug("Attempting sudo")
    command = "sudo -S -p '' whoami"
    stdin, stdout, _ = ssh.exec_command(command)
    stdin.write(password + '\n')
    stdin.flush()
    user = stdout.read().decode('utf-8').strip()
    ret = user == "root"
    if ret:
        logger.debug("<<<SUDO FOUND>>>")
    return ret


CHAN_FILE_T = Tuple[paramiko.ChannelFile, paramiko.ChannelFile, paramiko.ChannelFile]


def run_sudo(ssh: paramiko.SSHClient, command: str, password: str) -> CHAN_FILE_T:
    stdin, stdout, stderr = ssh.exec_command("sudo -S -p ' ' {}".format(command))
    stdin.write(password + '\n')
    stdin.flush()
    return stdin, stdout, stderr


FILTERED_ERRORS = (
    'No such file or directory',
    'Permission denied',
)
"""
Remote file errors that we will not log to the user.

These errors are usually expected, especially when attempting to access files
that we don't know for sure exist. Or when we don't necessarily have access to
a sufficiently privileged user to access the found files.
"""


def log_error(msg: str, path: str, text: str):
    for err in FILTERED_ERRORS:
        if err in text:
            return
    logger.error("%s %s: %s", msg, path, text)
