import subprocess
import logging
import paramiko
import os
import inspect
from typing import Callable, Tuple, Dict, Any, Optional

logger = logging.getLogger(__name__)

# Module-level debug hook for command execution output.
# Set by ``set_executor_debug_hook`` (typically wired to ``Ui.debug`` at ``-vv``).
_debug_hook: Optional[Callable[[str], None]] = None


def set_executor_debug_hook(hook: Optional[Callable[[str], None]]) -> None:
    """Set (or clear) a callback for command-level debug output.

    When set, ``Executor.execute`` calls this with the command string,
    return code, and any stdout/stderr — regardless of whether the caller
    used ``run()``/``check()`` helpers or called ``execute()`` directly.
    """
    global _debug_hook
    _debug_hook = hook


class ExecutorError(Exception):
    """Custom exception for command execution errors."""
    pass

class Executor:
    """Handles execution of commands locally or remotely via SSH."""

    def __init__(self, server_config: Optional[Dict[str, Any]] = None):
        """
        Initializes the Executor.

        Args:
            server_config (Optional[Dict[str, Any]]): 
                Configuration for the target server. Expected keys:
                'host': Target host (IP or hostname). If 'localhost' or '127.0.0.1', 
                        commands run locally.
                'user': Username for SSH.
                'ssh_key_path': Path to the SSH private key.
                'port': SSH port (defaults to 22).
                If None, all commands are assumed to be local.
        """
        self.server_config = server_config if server_config else {}
        self.host = self.server_config.get("host", "localhost")
        self.user = self.server_config.get("user")
        self.ssh_key_path = self.server_config.get("ssh_key_path")
        self.ssh_key_passphrase = self.server_config.get("ssh_key_passphrase")
        self.password = self.server_config.get("password")
        if self.ssh_key_path:
            self.ssh_key_path = os.path.expanduser(self.ssh_key_path)
        self.port = self.server_config.get("port", 22)

        self.is_remote_execution = not (self.host == "localhost" or self.host == "127.0.0.1")
        self.ssh_client: Optional[paramiko.SSHClient] = None
        self._sftp_client: Optional[paramiko.SFTPClient] = None

        if self.is_remote_execution and not self.user:
            raise ExecutorError(
                f"For remote host '{self.host}', 'user' must be provided in server_config."
            )

        if self.is_remote_execution and not (self.ssh_key_path or self.password):
            raise ExecutorError(
                f"For remote host '{self.host}', provide either 'ssh_key_path' or 'password' in server_config."
            )
        
        logger.info(
            f"Executor initialized for {'remote' if self.is_remote_execution else 'local'} execution "
            f"on host '{self.host}'."
        )

    def __enter__(self):
        """Enter the context manager, establishing the connection."""
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Exit the context manager, closing the connection."""
        self.close()

    def connect(self) -> None:
        """
        Establishes an SSH connection if configured for remote execution and not already connected.
        """
        if not self.is_remote_execution:
            logger.debug("Connect called, but not in remote execution mode. No action taken.")
            return

        if self.ssh_client and self.ssh_client.get_transport() and self.ssh_client.get_transport().is_active():
            logger.debug("Connect called, but SSH client is already connected and active.")
            return

        try:
            self.ssh_client = paramiko.SSHClient()
            self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            
            auth_hint = "password" if self.password and not self.ssh_key_path else f"key {self.ssh_key_path}"
            logger.info(f"SSH: Attempting to connect to {self.user}@{self.host}:{self.port} with {auth_hint}")
            connect_kwargs: Dict[str, Any] = {
                "hostname": self.host,
                "port": self.port,
                "username": self.user,
                "key_filename": self.ssh_key_path,
                "password": self.password,
                "timeout": 10,  # Connection timeout
            }
            if self.ssh_key_passphrase:
                # Paramiko supports a dedicated 'passphrase' kwarg in newer versions.
                # If not supported, it falls back to using 'password' as passphrase
                # when key_filename points to an encrypted key.
                params = inspect.signature(self.ssh_client.connect).parameters
                if "passphrase" in params:
                    connect_kwargs["passphrase"] = self.ssh_key_passphrase
                else:
                    if connect_kwargs.get("password"):
                        raise ExecutorError(
                            "Paramiko version does not support 'passphrase' argument. "
                            "Cannot provide both SSH password and key passphrase."
                        )
                    connect_kwargs["password"] = self.ssh_key_passphrase

            self.ssh_client.connect(**connect_kwargs)
            logger.info(f"SSH: Successfully connected to {self.user}@{self.host}:{self.port}.")
        except paramiko.AuthenticationException as e:
            self.ssh_client = None
            # This can be caused by wrong username/key, missing key passphrase,
            # or server-side auth policies.
            msg = f"SSH: Authentication failed for {self.user}@{self.host} with key {self.ssh_key_path}: {e}"
            logger.error(msg)
            raise ExecutorError(msg) from e
        except paramiko.SSHException as e:
            self.ssh_client = None
            msg = f"SSH: Connection error to {self.host}: {e}"
            logger.error(msg)
            raise ExecutorError(msg) from e
        except Exception as e:
            self.ssh_client = None
            msg = f"SSH: An unexpected error occurred during connection to {self.host}: {e}"
            logger.error(msg, exc_info=True)
            raise ExecutorError(msg) from e

    def close(self) -> None:
        """Closes the SSH connection if it is open."""
        if self.is_remote_execution and self.ssh_client:
            logger.info(f"SSH: Closing connection to {self.user}@{self.host}:{self.port}.")
            try:
                if self._sftp_client:
                    logger.debug("SSH: Closing SFTP client.")
                    self._sftp_client.close()
                    self._sftp_client = None
                self.ssh_client.close()
            except Exception as e:
                logger.warning(f"SSH: Error while closing connection to {self.host}: {e}", exc_info=True)
            finally:
                self.ssh_client = None # Set to None even if close fails, to allow reconnect
        elif self.is_remote_execution:
            logger.debug("Close called, but SSH client was not initialized or already closed.")
        else:
            logger.debug("Close called, but not in remote execution mode. No action taken.")

    def _execute_local(
        self,
        command: str,
        timeout: Optional[int] = None,
        *,
        stdin_data: str | bytes | None = None,
    ) -> Tuple[int, str, str]:
        """Execute a command locally using subprocess.

        When ``_debug_hook`` is set (``-vv``), stdout and stderr lines
        are streamed to the hook as they arrive so the user sees live
        output -- identical to the remote-execution streaming path.
        """
        import threading

        try:
            logger.debug(f"Executing local command: {command}")

            # --- Non-streaming (default / -v): block until done ---
            if not _debug_hook:
                process = subprocess.Popen(
                    command,
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    stdin=subprocess.PIPE,
                    text=True,
                )
                stdout, stderr = process.communicate(
                    input=stdin_data, timeout=timeout,
                )
                return_code = process.returncode
                logger.debug(f"Local command '{command}' finished with code {return_code}.")
                if stdout:
                    logger.debug(f"Local stdout:\n{stdout.strip()}")
                if stderr:
                    logger.debug(f"Local stderr:\n{stderr.strip()}")
                return return_code, stdout, stderr

            # --- Streaming path (-vv): read line-by-line via threads ---
            process = subprocess.Popen(
                command,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                stdin=subprocess.PIPE,
                text=True,
            )

            # Feed stdin data and close stdin so the child doesn't block.
            if stdin_data is not None:
                try:
                    data = stdin_data if isinstance(stdin_data, str) else stdin_data.decode("utf-8")
                    process.stdin.write(data)
                    process.stdin.flush()
                except Exception:
                    pass
            try:
                process.stdin.close()
            except Exception:
                pass

            stdout_lines: list[str] = []
            stderr_lines: list[str] = []

            def _drain(pipe, bucket: list[str], label: str):
                """Read lines from *pipe* until EOF, streaming each to _debug_hook."""
                for line in iter(pipe.readline, ""):
                    stripped = line.rstrip("\n")
                    bucket.append(stripped)
                    if _debug_hook:
                        _debug_hook(f"{label}: {stripped}")
                pipe.close()

            t_out = threading.Thread(target=_drain, args=(process.stdout, stdout_lines, "stdout"), daemon=True)
            t_err = threading.Thread(target=_drain, args=(process.stderr, stderr_lines, "stderr"), daemon=True)
            t_out.start()
            t_err.start()

            # Wait for process to exit (respects timeout).
            try:
                process.wait(timeout=timeout)
            except subprocess.TimeoutExpired:
                logger.error(f"Local command '{command}' timed out after {timeout} seconds.")
                process.kill()
                process.wait()
                t_out.join(timeout=2)
                t_err.join(timeout=2)
                return -1, "\n".join(stdout_lines), f"Command timed out after {timeout} seconds."

            t_out.join()
            t_err.join()

            return_code = process.returncode
            logger.debug(f"Local command '{command}' finished with code {return_code}.")
            return return_code, "\n".join(stdout_lines), "\n".join(stderr_lines)

        except subprocess.TimeoutExpired:
            logger.error(f"Local command '{command}' timed out after {timeout} seconds.")
            process.kill()
            process.communicate()
            return -1, "", f"Command timed out after {timeout} seconds."
        except Exception as e:
            logger.error(f"Error executing local command '{command}': {e}", exc_info=True)
            return -1, "", str(e)

    def _execute_remote(
        self,
        command: str,
        timeout: Optional[int] = None,
        *,
        stdin_data: str | bytes | None = None,
    ) -> Tuple[int, str, str]:
        """Execute a command remotely via Paramiko SSH.

        When ``_debug_hook`` is set (``-vv``), stdout and stderr lines are
        streamed to the hook as they arrive so the user sees live output.
        The full stdout/stderr strings are still collected and returned.
        """
        if not self.is_remote_execution:
            raise ExecutorError("_execute_remote called for a local executor setup.")

        if not self.ssh_client or not self.ssh_client.get_transport() or not self.ssh_client.get_transport().is_active():
            msg = "SSH client is not connected. Call connect() before executing remote commands."
            logger.error(msg)
            raise ExecutorError(msg)

        try:
            logger.debug(f"SSH: Executing remote command on {self.host}: {command}")

            stdin, stdout_channel, stderr_channel = self.ssh_client.exec_command(command, timeout=timeout)
            channel = stdout_channel.channel

            if stdin_data is not None:
                if isinstance(stdin_data, bytes):
                    data = stdin_data
                else:
                    data = stdin_data.encode("utf-8")
                try:
                    stdin.write(data)
                    stdin.flush()
                finally:
                    # Signal EOF to the remote process.
                    try:
                        stdin.channel.shutdown_write()
                    except Exception:
                        pass
                    try:
                        stdin.close()
                    except Exception:
                        pass

            # If no debug hook, use the simple blocking read (original behaviour).
            if not _debug_hook:
                stdout = stdout_channel.read().decode('utf-8', errors='replace').strip()
                stderr = stderr_channel.read().decode('utf-8', errors='replace').strip()
                return_code = channel.recv_exit_status()
                logger.debug(f"Remote command finished with code {return_code}.")
                return return_code, stdout, stderr

            # --- Streaming path: read line-by-line and emit via _debug_hook ---
            import time as _time

            stdout_parts: list[str] = []
            stderr_parts: list[str] = []
            stdout_partial = ""
            stderr_partial = ""

            while True:
                # Drain any available stdout data.
                while channel.recv_ready():
                    chunk = channel.recv(4096).decode('utf-8', errors='replace')
                    combined = stdout_partial + chunk
                    lines = combined.split('\n')
                    # Last element is an incomplete line (or empty after trailing \n).
                    stdout_partial = lines.pop()
                    for line in lines:
                        stdout_parts.append(line)
                        _debug_hook(f"stdout: {line}")

                # Drain any available stderr data.
                while channel.recv_stderr_ready():
                    chunk = channel.recv_stderr(4096).decode('utf-8', errors='replace')
                    combined = stderr_partial + chunk
                    lines = combined.split('\n')
                    stderr_partial = lines.pop()
                    for line in lines:
                        stderr_parts.append(line)
                        _debug_hook(f"stderr: {line}")

                # If the remote process has exited AND no more data is buffered, break.
                if channel.exit_status_ready() and not channel.recv_ready() and not channel.recv_stderr_ready():
                    break

                _time.sleep(0.05)  # avoid busy-spin

            # Flush remaining partial lines.
            if stdout_partial:
                stdout_parts.append(stdout_partial)
                _debug_hook(f"stdout: {stdout_partial}")
            if stderr_partial:
                stderr_parts.append(stderr_partial)
                _debug_hook(f"stderr: {stderr_partial}")

            return_code = channel.recv_exit_status()

            stdout = '\n'.join(stdout_parts).strip()
            stderr = '\n'.join(stderr_parts).strip()

            logger.debug(f"Remote command finished with code {return_code}.")
            return return_code, stdout, stderr

        except paramiko.SSHException as e:
            msg = f"SSH: Communication error during command execution on {self.host}: {e}"
            logger.error(msg)
            raise ExecutorError(msg) from e
        except Exception as e:
            logger.error(f"Unexpected error during remote command '{command}' on {self.host}: {e}", exc_info=True)
            return -1, "", str(e)

    def execute(
        self,
        command: str,
        timeout: Optional[int] = None,
        use_sudo: bool = False,
        *,
        stdin_data: str | bytes | None = None,
    ) -> Tuple[int, str, str]:
        """
        Executes a command either locally or remotely.

        Args:
            command (str): The command string to execute.
            timeout (Optional[int]): Timeout in seconds for the command execution.
                                     Note: For remote commands, this timeout is for channel creation
                                     and might not strictly enforce command execution time.
            use_sudo (bool): If True, prepends 'sudo -n ' to the command. 
                             Assumes non-interactive sudo.

        Returns:
            Tuple[int, str, str]: A tuple containing (return_code, stdout, stderr).
                                    return_code = -1 usually indicates an Executor-level error (e.g., timeout, connection error).
        """
        if use_sudo:
            command = f"sudo -n {command}" # -n for non-interactive
        
        execution_location = f"on host '{self.host}'" if self.ssh_client else "locally"
        logger.info(f"Executing command: '{command}' {execution_location}")

        if _debug_hook:
            _debug_hook(f"$ {command}")

        if self.is_remote_execution:
            rc, out, err = self._execute_remote(command, timeout=timeout, stdin_data=stdin_data)
        else:
            rc, out, err = self._execute_local(command, timeout=timeout, stdin_data=stdin_data)

        if _debug_hook:
            _debug_hook(f"rc={rc}")
            # Both local and remote paths stream line-by-line when
            # _debug_hook is set, so we only emit collected output
            # here when there was no streaming (i.e. _debug_hook was
            # set *after* execution started -- unlikely but safe).
            # In the normal flow, output was already streamed inside
            # _execute_local() / _execute_remote().

        return rc, out, err

    def is_remote(self) -> bool:
        """Returns True if the executor is configured for remote execution."""
        return self.is_remote_execution 

    def _get_sftp_client(self) -> paramiko.SFTPClient:
        """Returns an active SFTP client, creating one if necessary."""
        if not self.is_remote_execution:
            raise ExecutorError("SFTP client cannot be obtained for local execution.")
        
        if not self.ssh_client or not self.ssh_client.get_transport() or not self.ssh_client.get_transport().is_active():
            logger.info("SSH client not connected. Attempting to reconnect before getting SFTP client.")
            self.connect()
            if not self.ssh_client:
                 raise ExecutorError("Failed to establish SSH connection for SFTP.")

        if self._sftp_client and \
           self.ssh_client and \
           self.ssh_client.get_transport() and \
           self.ssh_client.get_transport().is_active() and \
           self._sftp_client.get_channel() and \
           self._sftp_client.get_channel().get_transport() and \
           self._sftp_client.get_channel().get_transport().is_active():
            logger.debug("Returning existing active SFTP client.")
            return self._sftp_client

        try:
            logger.debug(f"SSH: Opening SFTP session to {self.host}.")
            self._sftp_client = self.ssh_client.open_sftp()
            logger.info(f"SSH: SFTP session opened successfully to {self.host}.")
            return self._sftp_client
        except Exception as e:
            msg = f"SSH: Failed to open SFTP session on {self.host}: {e}"
            logger.error(msg, exc_info=True)
            self._sftp_client = None
            raise ExecutorError(msg) from e

    def read_remote_file_content(self, remote_path: str, use_sudo: bool = False) -> str:
        """
        Reads the content of a remote file.
        Uses 'sudo cat' for privileged access if use_sudo is True.
        """
        if not self.is_remote_execution:
            raise ExecutorError("Cannot read remote file content in local execution mode.")
        
        logger.info(f"Reading remote file '{remote_path}' on {self.host} {'with sudo' if use_sudo else ''}.")
        
        if use_sudo:
            # Use 'sudo cat' for reading protected files
            command = f"cat {remote_path}"
            rc, stdout, stderr = self.execute(command, use_sudo=True)
            if rc != 0:
                raise ExecutorError(f"Failed to read remote file '{remote_path}' with sudo. Stderr: {stderr}")
            return stdout
        else:
            # Use SFTP for non-privileged reads
            sftp = self._get_sftp_client()
            try:
                with sftp.open(remote_path, 'r') as f:
                    content = f.read().decode('utf-8')
                logger.debug(f"SFTP: Successfully read content from '{remote_path}'.")
                return content
            except Exception as e:
                raise ExecutorError(f"SFTP: Failed to read file '{remote_path}': {e}") from e

    def write_remote_file_content(self, remote_path: str, content: str, use_sudo: bool = False) -> None:
        """
        Writes content to a remote file.
        Uses 'sudo tee' for privileged access if use_sudo is True.
        """
        if not self.is_remote_execution:
            raise ExecutorError("Cannot write remote file content in local execution mode.")

        logger.info(f"Writing content to remote file '{remote_path}' on {self.host} {'with sudo' if use_sudo else ''}.")

        if use_sudo:
            # Use 'sudo tee' to write to protected files.
            # This avoids issues with shell redirection and sudo.
            # The content is passed via stdin to the remote command.
            command = f"tee {remote_path}"
            rc, stdout, stderr = self._execute_remote_with_stdin(command, content, use_sudo=True)
            if rc != 0:
                raise ExecutorError(f"Failed to write to remote file '{remote_path}' with sudo. Stderr: {stderr}")
        else:
            # Use SFTP for non-privileged writes
            sftp = self._get_sftp_client()
            try:
                with sftp.open(remote_path, 'w') as f:
                    f.write(content)
                logger.debug(f"SFTP: Successfully wrote content to '{remote_path}'.")
            except Exception as e:
                raise ExecutorError(f"SFTP: Failed to write to file '{remote_path}': {e}") from e

    def _execute_remote_with_stdin(self, command: str, stdin_content: str, use_sudo: bool = False) -> Tuple[int, str, str]:
        """Executes a remote command, providing content to its stdin."""
        if use_sudo:
            command = f"sudo -n {command}"

        logger.debug(f"SSH: Executing remote command with stdin on {self.host}: {command}")
        
        stdin, stdout_channel, stderr_channel = self.ssh_client.exec_command(command)
        stdin.write(stdin_content)
        stdin.channel.shutdown_write()

        stdout = stdout_channel.read().decode('utf-8', errors='replace').strip()
        stderr = stderr_channel.read().decode('utf-8', errors='replace').strip()
        return_code = stdout_channel.channel.recv_exit_status()
        
        logger.debug(f"Remote command with stdin '{command}' on {self.host} finished with code {return_code}.")
        if stdout:
            logger.debug(f"Remote stdout:\n{stdout}")
        if stderr:
            logger.debug(f"Remote stderr:\n{stderr}")

        return return_code, stdout, stderr

    def ensure_remote_directory_exists(self, remote_dir_path: str, 
                                       owner_user: Optional[str] = None, 
                                       owner_group: Optional[str] = None, 
                                       permissions: Optional[str] = None,
                                       use_sudo_for_mkdir: bool = False,
                                       use_sudo_for_chown: bool = False,
                                       use_sudo_for_chmod: bool = False) -> None:
        """Ensures a remote directory exists, creating and setting ownership/permissions if necessary."""
        logger.info(f"Ensuring remote directory '{remote_dir_path}' exists on {self.host}.")

        sftp = self._get_sftp_client()
        try:
            sftp.stat(remote_dir_path)
            logger.debug(f"SFTP: Directory '{remote_dir_path}' already exists.")
        except FileNotFoundError:
            logger.info(f"SFTP: Directory '{remote_dir_path}' not found. Creating it.")
            mkdir_cmd = f"mkdir -p {remote_dir_path}"
            ret_code, stdout, stderr = self.execute(mkdir_cmd, use_sudo=use_sudo_for_mkdir)
            if ret_code != 0:
                msg = f"Failed to create directory '{remote_dir_path}' on {self.host}. stderr: {stderr}"
                logger.error(msg)
                raise ExecutorError(msg)
            logger.info(f"Successfully created directory '{remote_dir_path}'.")

        if owner_user or owner_group:
            self.set_remote_file_ownership_and_permissions(
                remote_path=remote_dir_path, 
                owner_user=owner_user, 
                owner_group=owner_group, 
                permissions=None,
                use_sudo_for_chown=use_sudo_for_chown,
                use_sudo_for_chmod=False
            )
        if permissions:
            self.set_remote_file_ownership_and_permissions(
                remote_path=remote_dir_path, 
                owner_user=None,
                owner_group=None,
                permissions=permissions, 
                use_sudo_for_chown=False,
                use_sudo_for_chmod=use_sudo_for_chmod
            )

    def set_remote_file_ownership_and_permissions(self, remote_path: str, 
                                                  owner_user: Optional[str] = None, 
                                                  owner_group: Optional[str] = None, 
                                                  permissions: Optional[str] = None,
                                                  use_sudo_for_chown: bool = False,
                                                  use_sudo_for_chmod: bool = False) -> None:
        """Sets ownership and/or permissions for a remote file or directory."""
        if not owner_user and not owner_group and not permissions:
            logger.debug(f"No ownership or permissions changes requested for '{remote_path}'.")
            return

        if owner_user or owner_group:
            owner_spec = ""
            if owner_user:
                owner_spec = owner_user
                if owner_group:
                    owner_spec += f":{owner_group}"
            elif owner_group:
                owner_spec = f":{owner_group}"
            
            if owner_spec:
                chown_cmd = f"chown {owner_spec} {remote_path}"
                logger.info(f"Setting ownership of '{remote_path}' to '{owner_spec}' on {self.host}.")
                ret, _, stderr = self.execute(chown_cmd, use_sudo=use_sudo_for_chown)
                if ret != 0:
                    raise ExecutorError(f"Failed to set ownership for '{remote_path}'. Stderr: {stderr}")

        if permissions:
            chmod_cmd = f"chmod {permissions} {remote_path}"
            logger.info(f"Setting permissions of '{remote_path}' to '{permissions}' on {self.host}.")
            ret, _, stderr = self.execute(chmod_cmd, use_sudo=use_sudo_for_chmod)
            if ret != 0:
                raise ExecutorError(f"Failed to set permissions for '{remote_path}'. Stderr: {stderr}")

    def upload_file(self, local_path: str, remote_path: str, permissions: Optional[str] = None, owner_spec: Optional[str] = None, use_sudo: bool = False):
        """
        Uploads a file to a remote server and optionally sets its permissions and ownership.

        Args:
            local_path: Path to the local file.
            remote_path: Path to the remote destination.
            permissions: Octal string for permissions (e.g., "600").
            owner_spec: Owner/group spec (e.g., "user:group" or "user").
            use_sudo: Whether to use sudo for chown/chmod.

        Raises:
            ExecutorError: If the upload or post-upload commands fail.
            FileNotFoundError: If the local_path does not exist.
        """
        if not self.is_remote():
            raise ExecutorError("upload_file is only supported for remote execution.")

        if not os.path.exists(local_path):
            raise FileNotFoundError(f"Local file not found: {local_path}")
            
        sftp = self._get_sftp_client()

        # Parse owner_spec into user and group
        owner_user, owner_group = None, None
        if owner_spec:
            if ':' in owner_spec:
                owner_user, owner_group = owner_spec.split(':', 1)
            else:
                owner_user = owner_spec

        try:
            logger.info(f"SFTP: Uploading '{local_path}' to '{self.host}:{remote_path}'")
            # After uploading to a temporary path, move it to the final destination with sudo
            # and then set final permissions/ownership. This handles cases where the target dir is not writable by the SSH user.
            if use_sudo:
                temp_remote_path = f"/tmp/{os.path.basename(local_path)}"
                logger.debug(f"Sudo required. Uploading to temporary path '{temp_remote_path}' first.")
                sftp.put(local_path, temp_remote_path)

                logger.debug(f"Moving temporary file from '{temp_remote_path}' to final destination '{remote_path}' with sudo.")
                ret, _, stderr = self.execute(f"mv {temp_remote_path} {remote_path}", use_sudo=True)
                if ret != 0:
                    # Cleanup the temp file on failure
                    sftp.remove(temp_remote_path)
                    raise ExecutorError(f"Failed to move file from temp to final destination. Stderr: {stderr}")
                
                # Now set permissions and ownership on the final file
                self.set_remote_file_ownership_and_permissions(
                    remote_path, 
                    owner_user=owner_user, 
                    owner_group=owner_group, 
                    permissions=permissions, 
                    use_sudo_for_chown=True, 
                    use_sudo_for_chmod=True
                )

            else:
                sftp.put(local_path, remote_path)
                logger.info(f"SFTP: Successfully uploaded '{local_path}' to '{remote_path}' on {self.host}.")
                # Set permissions and ownership on the final file
                self.set_remote_file_ownership_and_permissions(
                    remote_path, 
                    owner_user=owner_user, 
                    owner_group=owner_group, 
                    permissions=permissions
                )

        except Exception as e:
            msg = f"SFTP: File upload failed for '{local_path}': {e}"
            logger.error(msg, exc_info=True)
            raise ExecutorError(msg) from e 