# dput/methods/sftp.py
# Part of ‘dput’, a Debian package upload toolkit.
#
# Copyright © 2006-2018 Canonical Ltd.
# Copyright © 2006 Robey Pointer <robey@lag.net>
#                  (parts of ProcessAsChannelAdapter)
# Copyright 2025 Simon Quigley <tsimonq2@ubuntu.com>
#
# Authors: Cody A.W. Somerville <cody.somerville@canonical.com>
#          Julian Andres Klode <julian.klode@canonical.com>
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3 of that license or any later version.
# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.

import errno
import os
import random
import sys
import socket
import subprocess
import time

from ..helper import dputhelper  # Required for progress tracking

BUFFER_SIZE = 512 * 1024

# Detect if terminal supports colors
USE_COLOR = sys.stdout.isatty()


def colorize(text, color_code):
    """Wrap text with ANSI color codes if terminal supports it."""
    if USE_COLOR:
        return f"\033[{color_code}m{text}\033[0m"
    return text


class ProcessAsChannelAdapter:
    """Wrap a process as a paramiko Channel."""

    def __init__(self, argv):
        """Initialize SSH process and create a socket pair."""
        self.__socket, subproc_sock = socket.socketpair()
        self.__proc = subprocess.Popen(argv, stdin=subproc_sock,
                                       stdout=subproc_sock, bufsize=0)

    def get_name(self):
        """Return a simple name for the adapter."""
        return "ProcessAsChannelAdapter"

    def send(self, data):
        """Send bytes to the subprocess."""
        return self.__socket.send(data)

    def recv(self, num_bytes):
        """Receive bytes from the subprocess."""
        try:
            return self.__socket.recv(num_bytes)
        except socket.error as e:
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED, errno.EBADF):
                return b''  # Paramiko expects an empty string when the connection is closed.
            raise

    def close(self):
        """Close the connection and terminate the process."""
        self.__socket.close()
        self.__proc.terminate()
        self.__proc.wait()


def get_ssh_command_line(login, fqdn, port):
    """Build SSH command line arguments."""
    return [
        "ssh",
        "-oForwardX11=no",
        "-oForwardAgent=no",
        "-oPermitLocalCommand=no",
        "-oClearAllForwardings=yes",
        "-oProtocol=2",
        "-oNoHostAuthenticationForLocalhost=yes",
        "-p", port,
        "-l", login,
        "-s", "--", fqdn, "sftp"
    ]


def human_readable_size(size_bytes):
    """Convert bytes to human-readable format (B, KB, MB, GB, TB)."""
    units = ["B", "KB", "MB", "GB", "TB"]
    size = float(size_bytes)
    for unit in units:
        if size < 1024:
            return f"{size:.2f} {unit}"
        size /= 1024
    return f"{size:.2f} PB"


def copy_file(sftp_client, local_path, remote_path, debug, progress):
    """Upload a single file with detailed progress tracking."""
    try:
        size = os.stat(local_path).st_size
    except Exception:
        size = -1
        if debug:
            sys.stdout.write(f"{colorize('D: Determining size of file failed', '31')}\n")

    file_buffer = min(BUFFER_SIZE, size) if size > 0 else BUFFER_SIZE
    filename = os.path.basename(local_path)

    with open(local_path, 'rb') as fileobj:
        if progress:
            fileobj = dputhelper.FileWithProgress(fileobj, ptype=progress,
                                                  progressf=sys.stdout,
                                                  size=size)

        tmp_path = f"{remote_path}.tmp.{time.time():.9f}.{os.getpid()}.{random.randint(0, 0x7FFFFFFF)}"

        try:
            sys.stdout.write(f"  Uploading {colorize(filename, '36;1')}: {colorize('0.00% (0.00 B / ' + human_readable_size(size) + ')', '32')} ")
            sys.stdout.flush()

            with sftp_client.file(tmp_path, "w", bufsize=0) as remote_fileobj:
                uploaded = 0
                while True:
                    data = fileobj.read(file_buffer)
                    if not data:
                        break
                    remote_fileobj.write(data)
                    uploaded += len(data)

                    percent_complete = (uploaded / size) * 100 if size > 0 else 100
                    progress_str = f"{human_readable_size(uploaded)} / {human_readable_size(size)} ({percent_complete:.2f}%)"
                    sys.stdout.write(f"\r  Uploading {colorize(filename, '36;1')}: {colorize(progress_str, '33')} ")
                    sys.stdout.flush()

            sftp_client.rename(tmp_path, remote_path)

            sys.stdout.write(f"\r  Uploading {colorize(filename, '36;1')}: {colorize(human_readable_size(size) + ' / ' + human_readable_size(size) + ' (100.00%) - done.', '92')}\n")
            sys.stdout.flush()

        except Exception as e:
            sftp_client.remove(tmp_path)
            sys.stderr.write(f"\n{colorize(f'E: Error uploading {filename}: {e}', '31')}\n")
            raise e


def upload(fqdn, login, incoming, files, debug, compress, progress):
    """Upload multiple files via SFTP, with correct progress tracking."""

    try:
        import paramiko.sftp_client
    except ImportError:
        sys.stderr.write(colorize("E: Paramiko must be installed to use SFTP transport.\n", "31"))
        sys.exit(1)

    if ':' in fqdn:
        fqdn, port = fqdn.rsplit(":", 1)
    else:
        port = "22"

    if not login or login == '*':
        login = os.getenv("USER")

    if not incoming.endswith("/"):
        incoming += "/"

    try:
        channel = ProcessAsChannelAdapter(get_ssh_command_line(login, fqdn, port))
        sftp_client = paramiko.sftp_client.SFTPClient(channel)
    except Exception as e:
        sys.stderr.write(f"\n{colorize(f'E: Error connecting to remote host: {e}', '31')}\n")
        sys.exit(1)

    try:
        for local_path in files:
            remote_path = os.path.join(incoming, os.path.basename(local_path))
            try:
                copy_file(sftp_client, local_path, remote_path, debug, progress)
            except Exception as e:
                sys.stderr.write(f"\n{colorize(f'E: Error uploading {os.path.basename(local_path)}: {e}', '31')}\n")
                sys.exit(1)
    finally:
        channel.close()
