"""SFTP connector for file operations with SFTP servers.

Uses paramiko wrapped in asyncio via a thread pool executor.
Maintains a persistent connection with automatic reconnection
protected by an asyncio.Lock to prevent thundering herd.
"""

import asyncio
import io
import logging
import os
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from typing import Any, Dict, List, Optional

import paramiko

logger = logging.getLogger(__name__)

_executor = ThreadPoolExecutor(max_workers=4)


class SFTPConnector:
    """Connector for SFTP file operations (list, download, upload, delete).

    Maintains a persistent connection that is reused across requests.
    Reconnection uses exponential backoff and is protected by an asyncio.Lock
    to prevent thundering herd when multiple concurrent requests detect a
    broken connection.

    Args:
        host: SFTP server hostname or IP address.
        port: SFTP server port (default: 22).
        username: SFTP login username.
        password: SFTP login password.
    """

    def __init__(
        self,
        host: str,
        port: int = 22,
        username: str = "",
        password: str = "",
    ):
        if not host:
            raise ValueError("SFTP host is required")
        if not password:
            raise ValueError("SFTP password is required")
        self.host = host
        self.port = port
        self.username = username
        self.password = password

        # Persistent connection state
        self._transport: Optional[paramiko.Transport] = None
        self._sftp: Optional[paramiko.SFTPClient] = None
        self._reconnect_lock = asyncio.Lock()

    def _is_connected(self) -> bool:
        """Check if the current connection is still alive."""
        if self._transport is None or self._sftp is None:
            return False
        return self._transport.is_active()

    def _connect_sync(self):
        """Establish a new SFTP connection with exponential backoff (up to 3 retries)."""
        self._close_sync()

        last_exception = None
        retry_delay = 2
        max_retries = 3

        for attempt in range(max_retries):
            try:
                logger.info(
                    "Attempting SFTP connection to %s:%d (attempt %d/%d)",
                    self.host, self.port, attempt + 1, max_retries,
                )
                transport = paramiko.Transport((self.host, self.port))
                transport.connect(username=self.username, password=self.password)
                sftp = paramiko.SFTPClient.from_transport(transport)

                self._transport = transport
                self._sftp = sftp
                logger.info("SFTP connection established successfully")
                return

            except Exception as e:
                last_exception = e
                logger.warning("SFTP connection attempt %d failed: %s", attempt + 1, e)
                if attempt < max_retries - 1:
                    logger.info("Retrying in %d seconds...", retry_delay)
                    time.sleep(retry_delay)
                    retry_delay *= 2

        logger.error("Failed to connect to SFTP after %d attempts: %s", max_retries, last_exception)
        raise last_exception

    async def _ensure_connection(self):
        """Ensure a connection exists, reconnecting with lock if needed."""
        # Fast path: connection exists and is active (no lock needed)
        if self._is_connected():
            return

        # Slow path: need to reconnect — lock prevents thundering herd
        async with self._reconnect_lock:
            # Double-check after acquiring lock (another coroutine may have reconnected)
            if self._is_connected():
                return
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(_executor, self._connect_sync)

    def _close_sync(self):
        """Safely close the persistent SFTP connection."""
        if self._sftp is not None:
            try:
                self._sftp.close()
            except Exception:
                pass
            self._sftp = None
        if self._transport is not None:
            try:
                self._transport.close()
            except Exception:
                pass
            self._transport = None

    def close(self):
        """Close the persistent SFTP connection (public API)."""
        self._close_sync()
        logger.info("SFTP connection closed")

    async def _run_sftp_operation(self, operation, *args):
        """Run an SFTP operation with automatic reconnection on failure."""
        await self._ensure_connection()
        sftp = self._sftp  # Capture reference

        def _run():
            return operation(sftp, *args)

        try:
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(_executor, _run)
        except Exception as e:
            logger.warning("SFTP operation failed, attempting reconnection: %s", e)
            # Mark connection as dead only if it's still the same one we used
            if self._sftp is sftp:
                self._close_sync()
            # Reconnect (lock prevents thundering herd)
            await self._ensure_connection()
            sftp = self._sftp  # Capture new reference

            def _retry():
                return operation(sftp, *args)

            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(_executor, _retry)

    async def list_files(self, remote_path: str = "/", extension_filter: str = "") -> List[Dict[str, Any]]:
        """List files in a remote directory.

        Args:
            remote_path: Remote directory path to list.
            extension_filter: Only return files with this extension (e.g., '.csv').
                Empty string returns all files.

        Returns:
            List of dicts with file information (name, size, modify).
        """
        logger.info("Listing files in %s on SFTP server %s", remote_path, self.host)

        def _op(sftp: paramiko.SFTPClient, path: str) -> List[Dict[str, Any]]:
            filenames = sftp.listdir(path)
            files = []
            for filename in filenames:
                if extension_filter and not filename.endswith(extension_filter):
                    continue
                files.append({
                    "name": filename,
                    "size": 0,
                    "modify": "",
                })
            logger.debug("Found %d files in %s", len(files), path)
            return files

        try:
            files = await self._run_sftp_operation(_op, remote_path)
            logger.info("Found %d files in %s", len(files), remote_path)
            return files
        except Exception as e:
            logger.error("Failed to list files in %s: %s", remote_path, e)
            raise

    async def download_file(self, remote_path: str, local_path: str) -> bool:
        """Download a file from the SFTP server.

        Args:
            remote_path: Full path to the remote file.
            local_path: Full local path where the file should be saved.

        Returns:
            True if download was successful, False otherwise.
        """
        logger.info("Downloading %s from SFTP server %s", remote_path, self.host)

        def _op(sftp: paramiko.SFTPClient, rpath: str, lpath: str) -> bool:
            local_dir = os.path.dirname(lpath)
            if local_dir:
                os.makedirs(local_dir, exist_ok=True)
            sftp.get(rpath, lpath)
            logger.debug("Downloaded %s to %s", rpath, lpath)
            return True

        try:
            result = await self._run_sftp_operation(_op, remote_path, local_path)
            if result:
                logger.info("Successfully downloaded %s to %s", remote_path, local_path)
            return result
        except Exception as e:
            logger.error("Failed to download %s: %s", remote_path, e)
            return False

    async def upload_content(self, content: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Upload string content directly to the SFTP server.

        Args:
            content: String content to upload.
            remote_path: Remote directory path (e.g., '/orders').
            remote_filename: Name for the file on the remote server.

        Returns:
            Dict containing upload result information.
        """
        logger.info("Uploading content to %s/%s on SFTP server %s", remote_path, remote_filename, self.host)

        def _op(sftp: paramiko.SFTPClient, data: str, rpath: str, rfilename: str) -> Dict[str, Any]:
            content_bytes = data.encode('utf-8')
            bio = io.BytesIO(content_bytes)
            full_remote_path = f"{rpath}/{rfilename}"
            sftp.putfo(bio, full_remote_path)
            logger.debug("Uploaded %s to %s", rfilename, rpath)
            return {
                "server": self.host,
                "path": rpath,
                "filename": rfilename,
                "status": "uploaded",
                "message": f"File {rfilename} successfully uploaded to {rpath}",
                "timestamp": datetime.now().isoformat(),
                "file_size": len(content_bytes),
            }

        try:
            result = await self._run_sftp_operation(_op, content, remote_path, remote_filename)
            logger.info("Successfully uploaded %s to %s", remote_filename, remote_path)
            return result
        except Exception as e:
            logger.error("Failed to upload content to %s/%s: %s", remote_path, remote_filename, e)
            raise

    async def upload_file(self, local_path: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Upload a local file to the SFTP server.

        Args:
            local_path: Path to the local file to upload.
            remote_path: Remote directory path.
            remote_filename: Name for the file on the remote server.

        Returns:
            Dict containing upload result information.
        """
        with open(local_path, 'r', encoding='utf-8') as f:
            content = f.read()
        return await self.upload_content(content, remote_path, remote_filename)

    async def delete_file(self, remote_path: str) -> bool:
        """Delete a file from the SFTP server.

        Args:
            remote_path: Full path to the remote file.

        Returns:
            True if deletion was successful (or file didn't exist), False otherwise.
        """
        logger.info("Deleting %s from SFTP server %s", remote_path, self.host)

        def _op(sftp: paramiko.SFTPClient, rpath: str) -> bool:
            try:
                sftp.remove(rpath)
                logger.debug("Deleted %s", rpath)
                return True
            except FileNotFoundError:
                logger.debug("File %s already deleted or doesn't exist", rpath)
                return True
            except IOError as e:
                if 'no such file' in str(e).lower():
                    logger.debug("File %s already deleted or doesn't exist: %s", rpath, e)
                    return True
                raise

        try:
            result = await self._run_sftp_operation(_op, remote_path)
            if result:
                logger.info("Successfully deleted %s", remote_path)
            return result
        except Exception as e:
            logger.error("Failed to delete %s: %s", remote_path, e)
            return False
