"""
Name: file_transfer_protocols
Title: File Transfer Protocols
Author: Cooper
Date: 31/01/2024

Desc:  This class contains the methods used for FTP, FTPS, and SFTP

FTP calls have been taken from the original wdm_client and now that client is oblivious to what protocol is being used.

SFTP is now supported but this is reliant on there being support for SFTP/SCP on the base OS.  This does not use any
python libraries but makes a series of subprocess calls.

"""
import os
import time
import logging
import subprocess

import ftplib
from ftplib import FTP
from ftplib import FTP_TLS

class FakeFile:
    def read(self, size=0):
        return ''

class FileTransfer(object):
    def __init__(self):
        self.host = None
        self.port = 21
        self.user = None
        self.password = None

        #sftp settings
        self.use_certificate = False
        self.certificate_path = None

        self.protocol = None
        self.ftps_en = False
        self.sftp_en = False

        self.ftp_client = None
        self.connected = False

    def update_ftp_configuration(self, new_configurations):
        """
        Allows the importing class to update the FTP server details and parses the host address if appropriate
        """
        pre_parsed_address = new_configurations["server_ip"]

        #If the path is prefixed with sftp:// then ditch it
        if pre_parsed_address[0:5] == "sftp:":
            address = pre_parsed_address[7:]
        else:
            address = pre_parsed_address

        #If the part contains the port number, separate it:
        if ":" in address:
            address, port = address.split(":")
            self.host = address

            try:
                self.port = int(port)
            except ValueError:
                self.port = 2022

        else:
            self.host = address

            try:
                self.port = int(new_configurations["server_port"])
            except (KeyError, ValueError):
                self.port = 21

        self.user = new_configurations["username"]
        self.password = new_configurations["password"]

    def return_ftp_configuration(self):
        """
        Returns the current FTP configuration in a convenient dictionary
        """
        ftp_configurations = {
            "host": self.host,
            "port": self.port,
            "user": self.user,
            "protocol": self.protocol
        }

        return ftp_configurations

class FTP_Client(FileTransfer):
    def __init__(self):
        super().__init__()

    def connectFTP(self) -> int:
        """
        Connects to an FTP server using the IP address and credentials provided by the console
        :return: 0 if connection failure, 1 if success
        """
        try:
            if self.ftps_en:
                logging.info("FTP: Connecting FTPS %s:%s" % (self.host, self.port))
                self.ftp_client = FTP_TLS(self.host)
                self.protocol = "FTPS"
            else:
                logging.info("FTP: Connecting FTP %s:%s" % (self.host, self.port))
                self.ftp_client = FTP(self.host)
                self.protocol = "FTP"
        except OSError:
            return 0

        try:
            self.ftp_client.connect(self.host, self.port)
            self.ftp_client.login(self.user, self.password)
            if self.ftps_en:
                self.ftp_client.prot_p()
        except ftplib.all_errors as e:
            logging.info(e)
            logging.info("\t...Connection failure")
            return 0
        else:
            logging.info("\t...Connected")
            return 1

    def close_ftp(self):
        """
        Closes the FTP connection
        """
        if self.ftp_client:
            try:
                self.ftp_client.quit()
            except Exception:
                pass

    def ftp_change_directory(self, path):
        """
        This sets the current directory on the server, there isn't generally a need to change directories, if you know
        where the file exists then just enter it's path.

        This is not used.
        :param path: FTP parth
        :return: 0 if failure, 1 if success
        """
        try:
            self.ftp_client.cwd(path)
        except ftplib.all_errors as e:
            logging.info(e)
            return 0
        else:
            return 1

    def ftp_list_directory(self, path):
        """
        Lists the contents of the current directory

        This is not used
        :param path: FTP path
        :return: 0 if failure, contents of the directory if success
        """

        try:
            dir_contents = self.ftp_client.dir(path)
        except ftplib.all_errors as e:
            return 0
        else:
            return dir_contents

    def ftp_get_file_size(self, srcpath):
        """
        Returns the file size of a file on the FTP server
        :param srcpath: Path of the file on the server
        :return: None if error, size in success
        """

        try:
            filesize = self.ftp_client.size(srcpath)
        except ftplib.all_errors as e:
            return None
        else:
            return filesize

    def download_file(self, srcpath, destpath, retries=5):
        """
        Downloads a file from the FTP server
        :param srcpath: Path of the file on the FTP server
        :param destpath: Path where the file is saved locally
        :return: 0 if failure, 1 if success
        """
        logging.info("FTP: Downloading %s to %s" % (srcpath, destpath))
        error = ""

        with open(destpath, "wb") as localfile:
            for retry in range(retries):
                try:
                    self.ftp_client.retrbinary("RETR " + srcpath, localfile.write)
                except ftplib.all_errors as e:
                    error = e
                    time.sleep(0.1)
                    continue
                else:
                    logging.info("\tSuccess")
                    return 0

        logging.warning("\tDownload failed :(")
        logging.warning(error)
        return 1

    def upload_file(self, destpath, srcpath, retries=5):
        """
        Uploads a file to the FTP server
        """
        logging.info("FTP: Uploading %s to %s" % (srcpath, destpath))
        error = ""

        for retry in range(retries):
            try:
                self.ftp_client.storbinary("STOR %s" % destpath, srcpath)
            except OSError:
                # There is a weird bug where even when a successful flag is sent, it raises an exception Errno 0
                pass
            except ftplib.all_errors as e:
                error = e
                time.sleep(0.1)
                continue
            except AttributeError as e:
                error = e
            else:
                logging.info("\tSuccess")
                return 0

        logging.warning(error)
        logging.warning("\tUpload failed :(")
        return 1

class SFTP_Client(FileTransfer):
    """
    This uses the OS' built in SCP client.  For this to work there must be a functional SCP client installed.
    This also uses sshpass to pass the password forwards.

    sshpass -p "password" scp user@example.com:/some/remote/path /some/local/path
    """
    def __init__(self):
        super().__init__()
        self.protocol = "SFTP"

    def process_command(self, command: list):
        """
        As above but this one returns the output
        """
        # print(command)
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output, error = process.communicate()
        rc = process.returncode

        return rc, output.decode("utf-8"), error.decode("utf-8")
    def generate_sshpass_commands(self):
        """
        This creates the sshpass command needed for accessing the remote host if a password is used instead of a cert
        """
        sshpass_command = [
            "sshpass",
            "-p",
            self.password
        ]

        return sshpass_command

    def generate_scp_command(self, src, dst):
        """
        This generates the scp command used to transfer files back and forth
        """
        if self.use_certificate:
            scp_command = [
                "scp",
                # "-oStrictHostKeyChecking=no",
                "-i",
                self.certificate_path,
                "-P",
                self.port,
                src,
                dst
            ]
        else:
            scp_command = self.generate_sshpass_commands() + [
                "scp",
                "-o",
                "StrictHostKeyChecking=no",
                "-P",
                str(self.port),
                src,
                dst
            ]

        return scp_command

    def generate_ssh_command(self, command):
        """
        This generates a ssh command to run remotely and then return the output of said command
        """

        ssh_command = self.generate_sshpass_commands() + [
            "ssh",
            "%s@%s" % (self.user, self.host),
            "-p",
            str(self.port),
            "-r",
            command
        ]

    def connectFTP(self) -> int:
        """
        Connects to an FTP server using the IP address and credentials provided by the console
        :return: 0 if connection failure, 1 if success

        This may not be needed at all in sftp mode as it is a call to the native scp client
        """
        return 1

    def close_ftp(self):
        """
        Closes the FTP connection

        This is redundant in this mode but needed so wdm doesnt break
        """
        return 1

    def ftp_change_directory(self, path):
        """
        This sets the current directory on the server, there isn't generally a need to change directories, if you know
        where the file exists then just enter it's path.
        :param path: FTP parth
        :return: 0 if failure, 1 if success

        This is not used
        """
        pass

    def ftp_list_directory(self, path):
        """
        Lists the contents of the current directory
        :param path: FTP path
        :return: 0 if failure, contents of the directory if success

        This is not used
        """
        pass

    def ftp_get_file_size(self, srcpath):
        """
        This function is probably redundant as we can just download the file and if it fails then it doesnt exist init.

        :param filepath: Path of the file on the server
        :return: None if error, size in success (giggity)
        """
        destpath = r"/tmp"

        status = self.download_file(srcpath, destpath)
        if status == 1:
            #Status 1 means that there was some other error obtaining the file
            return 1
        elif status == 2:
            #Status 2 means the file doesnt exist
            return 2
        else:
            return 0

    def download_file(self, srcpath, destpath, retries=5):
        """
        Downloads a file from the FTP server
        :param filepath: Path of the file on the FTP server
        :param destpath: Path where the file is saved locally
        :return: 0 if success, 1 if failure, 2 if file doesnt exist
        """
        src_file_path = "%s@%s:%s" % (self.user, self.host, srcpath.replace(" ", "\ "))
        logging.info("SFTP: Downloading: %s" % src_file_path)
        commands = self.generate_scp_command(src_file_path, destpath)

        for retry in range(retries):
            status, output, error = self.process_command(commands)

            if status == 0:
                logging.info("SFTP: success")
                break
            else:
                logging.error("SFTP: %s" % error.rstrip())
                if "no such file" in error.lower():
                    status = 2
                    break
                else:
                    #each failed process will increase the wait time by 1 second
                    time.sleep(retry)

        return status

    def upload_file(self, destpath, srcpath, retries=5):
        """
        Uploads a file to the FTP server
        :param filepath: Path of the file on the FTP server
        :param destpath: Path where the file is saved locally
        :return: 0 if success, 1 if failure, 2 if file doesnt exist
        """
        dest_file_path = "%s@%s:%s" % (self.user, self.host, destpath.replace(" ", "\ "))
        logging.info("SFTP: Uploading: %s to %s" % (srcpath, dest_file_path))
        commands = self.generate_scp_command(srcpath, dest_file_path)

        for retry in range(retries):
            status, output, error = self.process_command(commands)

            if status == 0:
                logging.info("SFTP: success")
                break
            else:
                #TODO find the errors
                logging.error("SFTP: %s" % error.rstrip())
                if "no such file" in error.lower():
                    status = 2
                    break
                else:
                    time.sleep(retry)

        return status

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
    ftp = SFTP_Client()

    sftp_config = {
        "server_ip": "sftp.hanover.cloud",
        "server_port": 2022,
        "username": "1030_200",
        "password": "Ec9qO08s",
        "protocol": "sftp"
    }

    ftp.update_ftp_configuration(sftp_config)

    for x in range(1):
        ftp.upload_file("/vehicle/VENGA 1/", r"/tmp/dummy_file.txt",)
        time.sleep(3)
        ftp.download_file("/vehicle/VENGA 1/dummy_file_.txt", r"/tmp/dummy_file.txt")
        time.sleep(3)
