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

Uses ftplib (stdlib) wrapped in asyncio via a thread pool executor.
Reliable with Windows FTP servers that may not support modern FTP extensions.
No optional dependencies required — uses only Python stdlib.

Usage::

    from entr_adapter_core.connectors.ftp import FTPConnector, FTPConnectorConfig

    config = FTPConnectorConfig(
        host="ftp.example.com",
        username="user",
        password="pass",
    )
    connector = FTPConnector(config)
    files = await connector.list_files("/invoices", extension_filter=".csv")

Or with direct arguments (backward-compatible)::

    connector = FTPConnector(host="ftp.example.com", username="user", password="pass")
"""

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

from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

_executor = ThreadPoolExecutor(max_workers=4)


class FTPConnectorConfig(BaseModel):
    """Configuration for the FTP connector.

    Tenant-specific settings for connecting to an FTP server.
    All fields map to standard FTP connection parameters.

    Attributes:
        host: FTP server hostname or IP address.
        port: FTP server port. Standard FTP uses port 21.
        username: FTP login username. Defaults to 'anonymous' for
            public FTP servers.
        password: FTP login password. Empty string for anonymous access.
    """

    host: str = Field(
        ...,
        description="FTP server hostname or IP address",
    )
    port: int = Field(
        default=21,
        ge=1,
        le=65535,
        description="FTP server port (default: 21)",
    )
    username: str = Field(
        default="anonymous",
        description="FTP login username (default: anonymous)",
    )
    password: str = Field(
        default="",
        description="FTP login password",
    )


class FTPConnector:
    """Connector for FTP file operations (list, download, upload, delete).

    All operations are async wrappers around synchronous ftplib calls,
    run in a thread pool to avoid blocking the event loop.

    Accepts either an ``FTPConnectorConfig`` instance or direct keyword
    arguments for backward compatibility.

    Args:
        config_or_host: An ``FTPConnectorConfig`` instance, or the FTP
            server hostname as a string.
        port: FTP server port (default: 21). Ignored when config is provided.
        username: FTP login username (default: anonymous). Ignored when config is provided.
        password: FTP login password. Ignored when config is provided.

    Example::

        config = FTPConnectorConfig(host="ftp.example.com", username="user", password="pass")
        connector = FTPConnector(config)
        files = await connector.list_files("/invoices")
    """

    def __init__(
        self,
        config_or_host: Union[FTPConnectorConfig, str] = None,
        port: int = 21,
        username: str = "anonymous",
        password: str = "",
        *,
        host: str = None,
    ):
        if isinstance(config_or_host, FTPConnectorConfig):
            self.host = config_or_host.host
            self.port = config_or_host.port
            self.username = config_or_host.username
            self.password = config_or_host.password
        elif isinstance(config_or_host, str):
            self.host = config_or_host
            self.port = port
            self.username = username
            self.password = password
        elif host is not None:
            self.host = host
            self.port = port
            self.username = username
            self.password = password
        else:
            raise ValueError("FTP host is required: pass an FTPConnectorConfig or host string")
        if not self.host:
            raise ValueError("FTP host is required")

    def _connect(self) -> FTP:
        """Create and authenticate a new FTP connection."""
        ftp = FTP()
        ftp.connect(self.host, self.port)
        ftp.login(self.username, self.password)
        return ftp

    def _list_files_sync(self, remote_path: str, extension_filter: str = "") -> List[Dict[str, Any]]:
        """Synchronous FTP file listing using NLST (most reliable for Windows FTP)."""
        ftp = None
        try:
            ftp = self._connect()
            if remote_path and remote_path != "/":
                ftp.cwd(remote_path)

            filenames = ftp.nlst()
            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), remote_path)
            return files
        except Exception as e:
            logger.error("FTP list operation failed: %s", e)
            raise
        finally:
            if ftp:
                try:
                    ftp.quit()
                except Exception:
                    pass

    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 FTP server %s", remote_path, self.host)
        loop = asyncio.get_running_loop()
        files = await loop.run_in_executor(_executor, self._list_files_sync, remote_path, extension_filter)
        logger.info("Found %d files in %s", len(files), remote_path)
        return files

    def _download_file_sync(self, remote_path: str, local_path: str) -> bool:
        """Synchronous FTP file download."""
        ftp = None
        try:
            local_dir = os.path.dirname(local_path)
            if local_dir:
                os.makedirs(local_dir, exist_ok=True)

            ftp = self._connect()
            with open(local_path, 'wb') as f:
                ftp.retrbinary(f'RETR {remote_path}', f.write)

            logger.debug("Downloaded %s to %s", remote_path, local_path)
            return True
        except Exception as e:
            logger.error("FTP download operation failed: %s", e)
            return False
        finally:
            if ftp:
                try:
                    ftp.quit()
                except Exception:
                    pass

    async def download_file(self, remote_path: str, local_path: str) -> bool:
        """Download a file from the FTP 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 FTP server %s", remote_path, self.host)
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(_executor, self._download_file_sync, remote_path, local_path)
        if result:
            logger.info("Successfully downloaded %s to %s", remote_path, local_path)
        return result

    def _upload_content_sync(self, content: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Synchronous FTP content upload."""
        ftp = None
        try:
            ftp = self._connect()
            if remote_path and remote_path != "/":
                ftp.cwd(remote_path)

            content_bytes = content.encode('utf-8')
            bio = io.BytesIO(content_bytes)
            ftp.storbinary(f'STOR {remote_filename}', bio)

            logger.debug("Uploaded %s to %s", remote_filename, remote_path)
            return {
                "server": self.host,
                "path": remote_path,
                "filename": remote_filename,
                "status": "uploaded",
                "message": f"File {remote_filename} successfully uploaded to {remote_path}",
                "timestamp": datetime.now().isoformat(),
                "file_size": len(content_bytes),
            }
        except Exception as e:
            logger.error("FTP upload operation failed: %s", e)
            raise
        finally:
            if ftp:
                try:
                    ftp.quit()
                except Exception:
                    pass

    async def upload_content(self, content: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Upload string content directly to the FTP 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 FTP server %s", remote_path, remote_filename, self.host)
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(_executor, self._upload_content_sync, content, remote_path, remote_filename)
        logger.info("Successfully uploaded %s to %s", remote_filename, remote_path)
        return result

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

        Supports both text and binary files. The file is read in binary
        mode and uploaded directly.

        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.
        """
        return await self.upload_file_binary(local_path, remote_path, remote_filename)

    def _upload_binary_sync(self, file_bytes: bytes, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Synchronous FTP binary upload."""
        ftp = None
        try:
            ftp = self._connect()
            if remote_path and remote_path != "/":
                ftp.cwd(remote_path)

            bio = io.BytesIO(file_bytes)
            ftp.storbinary(f'STOR {remote_filename}', bio)

            logger.debug("Uploaded %s to %s", remote_filename, remote_path)
            return {
                "server": self.host,
                "path": remote_path,
                "filename": remote_filename,
                "status": "uploaded",
                "message": f"File {remote_filename} successfully uploaded to {remote_path}",
                "timestamp": datetime.now().isoformat(),
                "file_size": len(file_bytes),
            }
        except Exception as e:
            logger.error("FTP binary upload operation failed: %s", e)
            raise
        finally:
            if ftp:
                try:
                    ftp.quit()
                except Exception:
                    pass

    async def upload_file_binary(self, local_path: str, remote_path: str, remote_filename: str) -> Dict[str, Any]:
        """Upload a local file (binary-safe) to the FTP 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, 'rb') as f:
            file_bytes = f.read()
        logger.info("Uploading binary file %s to %s/%s on FTP server %s", local_path, remote_path, remote_filename, self.host)
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(_executor, self._upload_binary_sync, file_bytes, remote_path, remote_filename)
        logger.info("Successfully uploaded %s to %s", remote_filename, remote_path)
        return result

    def _delete_file_sync(self, remote_path: str) -> bool:
        """Synchronous FTP file deletion with verification."""
        ftp = None
        try:
            ftp = self._connect()
            response = ftp.delete(remote_path)
            logger.debug("FTP DELE response for %s: %s", remote_path, response)

            # Verify deletion
            if '/' in remote_path:
                directory = '/'.join(remote_path.split('/')[:-1])
                filename = remote_path.split('/')[-1]
            else:
                directory = ''
                filename = remote_path

            if directory:
                ftp.cwd(directory)
            remaining_files = ftp.nlst()
            if filename in remaining_files:
                logger.error("File %s still exists after DELETE — possible permissions issue", filename)
                return False

            logger.debug("Verified deletion of %s", remote_path)
            return True
        except Exception as e:
            error_str = str(e).lower()
            if 'no such file' in error_str or '550' in error_str:
                logger.debug("File %s already deleted or doesn't exist: %s", remote_path, e)
                return True
            logger.error("FTP delete operation failed for %s: %s", remote_path, e)
            return False
        finally:
            if ftp:
                try:
                    ftp.quit()
                except Exception:
                    pass

    async def delete_file(self, remote_path: str) -> bool:
        """Delete a file from the FTP 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 FTP server %s", remote_path, self.host)
        loop = asyncio.get_running_loop()
        result = await loop.run_in_executor(_executor, self._delete_file_sync, remote_path)
        if result:
            logger.info("Successfully deleted %s", remote_path)
        return result
