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

import paramiko

from app.tenant_config import TenantSettings

logger = logging.getLogger(__name__)

# Thread pool for running synchronous SFTP operations
_executor = ThreadPoolExecutor(max_workers=4)


class SFTPConnector:
    """
    Connector for SFTP operations with Smurfit Westrock SFTP server.
    Uses paramiko wrapped in asyncio, mirroring the FTPConnector interface.

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

    def __init__(self, settings: TenantSettings):
        self.settings = settings
        self._validate_config()

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

    def _validate_config(self):
        """Validate that required SFTP configuration is present."""
        if not self.settings.SMURFIT_SFTP_SERVER:
            raise ValueError(
                "SMURFIT_SFTP_SERVER is not configured. "
                "Please set the SFTP server hostname in your environment variables."
            )
        if not self.settings.SMURFIT_SFTP_PASSWORD:
            raise ValueError(
                "SMURFIT_SFTP_PASSWORD is not configured. "
                "Please set the SFTP password in your environment variables."
            )

    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. Called only under self._reconnect_lock
        via the executor.  Uses exponential backoff with up to 3 retries.
        """
        # Close any stale connection first
        self._close_sync()

        last_exception = None
        retry_delay = 2
        max_retries = 3

        for attempt in range(max_retries):
            try:
                logger.info(
                    f"Attempting SFTP connection to {self.settings.SMURFIT_SFTP_SERVER}:"
                    f"{self.settings.SMURFIT_SFTP_PORT} (attempt {attempt + 1}/{max_retries})"
                )
                transport = paramiko.Transport((
                    self.settings.SMURFIT_SFTP_SERVER,
                    self.settings.SMURFIT_SFTP_PORT,
                ))
                transport.connect(
                    username=self.settings.SMURFIT_SFTP_USERNAME,
                    password=self.settings.SMURFIT_SFTP_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(f"SFTP connection attempt {attempt + 1} failed: {e}")
                if attempt < max_retries - 1:
                    import time
                    logger.info(f"Retrying in {retry_delay} seconds...")
                    time.sleep(retry_delay)
                    retry_delay *= 2

        logger.error(f"Failed to connect to SFTP after {max_retries} attempts: {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 using the persistent connection.
        On failure, marks connection as dead, reconnects once, and retries.
        """
        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(f"SFTP operation failed, attempting reconnection: {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)

            return await loop.run_in_executor(_executor, _retry)

    # ── list_files ───────────────────────────────────────────────────────

    async def list_files(self, remote_path: str = "/") -> List[Dict[str, Any]]:
        """List CSV files in a remote directory."""
        logger.info(f"Listing files in {remote_path} on SFTP server {self.settings.SMURFIT_SFTP_SERVER}")

        def _op(sftp: paramiko.SFTPClient, path: str) -> List[Dict[str, Any]]:
            filenames = sftp.listdir(path)
            files = []
            for filename in filenames:
                if filename.endswith('.csv'):
                    files.append({
                        "name": filename,
                        "size": 0,
                        "modify": "",
                    })
            logger.debug(f"Found {len(files)} CSV files in {path}")
            return files

        try:
            files = await self._run_sftp_operation(_op, remote_path)
            logger.info(f"Found {len(files)} files in {remote_path}")
            return files
        except Exception as e:
            logger.error(f"Failed to list files in {remote_path}: {e}")
            raise Exception(f"SFTP list failed: {str(e)}")

    # ── download_file ────────────────────────────────────────────────────

    async def download_file(self, remote_path: str, local_path: str) -> bool:
        """Download a file from the SFTP server."""
        logger.info(f"Downloading {remote_path} from SFTP server {self.settings.SMURFIT_SFTP_SERVER}")

        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(f"Downloaded {rpath} to {lpath}")
            return True

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

    # ── upload_content ───────────────────────────────────────────────────

    async def upload_content(self, content: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Upload string content directly to the SFTP server."""
        logger.info(f"Uploading content to {remote_path}/{remote_filename} on SFTP server {self.settings.SMURFIT_SFTP_SERVER}")

        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(f"Uploaded {rfilename} to {rpath}")
            return {
                "server": self.settings.SMURFIT_SFTP_SERVER,
                "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(f"Successfully uploaded {remote_filename} to {remote_path}")
            return result
        except Exception as e:
            logger.error(f"Failed to upload content to {remote_path}/{remote_filename}: {e}")
            raise Exception(f"SFTP upload failed: {str(e)}")

    # ── upload_file ──────────────────────────────────────────────────────

    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."""
        with open(local_path, 'r', encoding='utf-8') as f:
            content = f.read()
        return await self.upload_content(content, remote_path, remote_filename)

    # ── delete_file ──────────────────────────────────────────────────────

    async def delete_file(self, remote_path: str) -> bool:
        """Delete a file from the SFTP server."""
        logger.info(f"Deleting {remote_path} from SFTP server {self.settings.SMURFIT_SFTP_SERVER}")

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

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