"""SSH key management component for FCP provider.

This module provides clean separation of concerns for SSH key operations,
including automatic key provisioning and error handling.
"""

import json
import logging
import os
import platform
import shutil
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional, Tuple

from flow.core.interfaces import IHttpClient
from flow.errors import FlowError

from ..api.types import SSHKeyModel as SSHKey

logger = logging.getLogger(__name__)


class SSHKeyError(FlowError):
    """Base error for SSH key operations."""

    pass


class SSHKeyNotFoundError(SSHKeyError):
    """Raised when SSH keys cannot be found or created."""

    pass


class SSHKeyManager:
    """Manages SSH keys with automatic provisioning and caching."""

    def __init__(self, http_client: IHttpClient, project_id: Optional[str] = None):
        """Initialize SSH key manager.

        Args:
            http_client: HTTP client for API requests
            project_id: Optional project ID for scoped operations
        """
        self.http = http_client
        self.project_id = project_id
        self._keys_cache: Optional[List[SSHKey]] = None

    def ensure_keys(self, requested_keys: Optional[List[str]] = None) -> List[str]:
        """Ensure SSH keys are available for use.

        This method follows a fallback strategy:
        1. Use explicitly provided key IDs if given
        2. Use existing keys from the project
        3. Optionally create a default key if none exist

        Args:
            requested_keys: Optional list of specific SSH key IDs to use

        Returns:
            List of SSH key IDs ready for use

        Raises:
            SSHKeyNotFoundError: If no keys can be obtained
        """
        # Use explicitly provided keys if given
        if requested_keys:
            logger.debug(f"Using {len(requested_keys)} explicitly provided SSH keys")
            return requested_keys

        # Get existing keys
        existing_keys = self.list_keys()
        if existing_keys:
            key_ids = [key.fid for key in existing_keys]
            logger.info(f"Using {len(key_ids)} existing SSH keys from project")
            return key_ids

        # No keys available
        logger.warning("No SSH keys available for the project")

        # Optionally try to create a default key from environment
        if default_key := self._try_create_default_key():
            return [default_key]

        # Return empty list - let the caller decide if this is an error
        return []

    def list_keys(self) -> List[SSHKey]:
        """List all SSH keys for the project.

        Returns:
            List of SSHKey objects
        """
        if self._keys_cache is not None:
            return self._keys_cache

        try:
            params = {}
            if self.project_id:
                params["project"] = self.project_id  # API expects 'project', not 'project_id'

            response = self.http.request(
                method="GET",
                url="/v2/ssh-keys",
                params=params,
            )

            # API returns list directly
            keys_data = response if isinstance(response, list) else []

            self._keys_cache = [
                SSHKey(
                    fid=k["fid"],
                    name=k["name"],
                    public_key=k.get("public_key", ""),
                    created_at=k.get("created_at"),
                )
                for k in keys_data
                if "fid" in k and "name" in k
            ]

            logger.debug(f"Loaded {len(self._keys_cache)} SSH keys from API")
            return self._keys_cache

        except Exception as e:
            logger.error(f"Failed to fetch SSH keys: {e}")
            return []

    def create_key(self, name: str, public_key: str) -> str:
        """Create a new SSH key.

        Args:
            name: Key name
            public_key: SSH public key content

        Returns:
            ID of the created key

        Raises:
            SSHKeyError: If key creation fails
        """
        payload = {
            "name": name,
            "public_key": public_key.strip(),
        }

        if self.project_id:
            payload["project"] = self.project_id

        try:
            response = self.http.request(
                method="POST",
                url="/v2/ssh-keys",
                json=payload,
            )

            key_id = response.get("fid")
            if not key_id:
                raise SSHKeyError(f"No key ID returned in response: {response}")

            # Invalidate cache
            self._keys_cache = None

            logger.info(f"Created SSH key '{name}' with ID: {key_id}")
            return key_id

        except Exception as e:
            raise SSHKeyError(f"Failed to create SSH key '{name}': {e}") from e

    def delete_key(self, key_id: str) -> bool:
        """Delete an SSH key.

        Args:
            key_id: SSH key ID to delete

        Returns:
            True if successful, False otherwise
        """
        try:
            self.http.request(
                method="DELETE",
                url=f"/v2/ssh-keys/{key_id}",
            )

            # Invalidate cache
            self._keys_cache = None

            logger.info(f"Deleted SSH key: {key_id}")
            return True

        except Exception as e:
            logger.error(f"Failed to delete SSH key {key_id}: {e}")
            return False

    def get_key(self, key_id: str) -> Optional[SSHKey]:
        """Get a specific SSH key by ID.

        Args:
            key_id: SSH key ID

        Returns:
            SSHKey if found, None otherwise
        """
        keys = self.list_keys()
        for key in keys:
            if key.fid == key_id:
                return key
        return None

    def invalidate_cache(self):
        """Clear the cache, forcing fresh lookups."""
        self._keys_cache = None
        logger.debug("SSH key cache invalidated")

    def _try_create_default_key(self) -> Optional[str]:
        """Try to create a default SSH key from common locations.

        Returns:
            Key ID if created, None otherwise
        """
        # Common SSH key locations
        key_paths = [
            Path.home() / ".ssh" / "id_rsa.pub",
            Path.home() / ".ssh" / "id_ed25519.pub",
            Path.home() / ".ssh" / "id_ecdsa.pub",
        ]

        for key_path in key_paths:
            if key_path.exists():
                try:
                    # Check private key permissions if it exists
                    private_key_path = key_path.with_suffix("")
                    if private_key_path.exists():
                        from flow.utils.security import check_ssh_key_permissions

                        try:
                            check_ssh_key_permissions(private_key_path)
                        except Exception as e:
                            logger.warning(f"SSH key has insecure permissions: {e}")
                            continue

                    public_key = key_path.read_text().strip()
                    if public_key:
                        key_name = f"flow-default-{key_path.stem}"
                        logger.info(f"Creating default SSH key from {key_path}")
                        return self.create_key(key_name, public_key)
                except Exception as e:
                    logger.debug(f"Failed to read {key_path}: {e}")

        # Check environment variable for public key content
        if public_key := os.environ.get("FCP_SSH_PUBLIC_KEY"):
            try:
                logger.info("Creating default SSH key from FCP_SSH_PUBLIC_KEY")
                return self.create_key("flow-env-key", public_key)
            except Exception as e:
                logger.debug(f"Failed to create key from environment: {e}")

        # Check for FCP_SSH_KEY pointing to a key file
        if key_file := os.environ.get("FCP_SSH_KEY"):
            try:
                key_path = Path(key_file)
                if key_path.exists():
                    # Check if it's a private key, find the public key
                    if not key_file.endswith(".pub"):
                        pub_key_path = key_path.with_suffix(".pub")
                        if pub_key_path.exists():
                            # Check private key permissions
                            from flow.utils.security import check_ssh_key_permissions

                            check_ssh_key_permissions(key_path)

                            public_key = pub_key_path.read_text().strip()
                            logger.info(
                                f"Creating default SSH key from FCP_SSH_KEY: {pub_key_path}"
                            )
                            return self.create_key("flow-fcp-key", public_key)
                    else:
                        # It's already a public key
                        public_key = key_path.read_text().strip()
                        logger.info(f"Creating default SSH key from FCP_SSH_KEY: {key_path}")
                        return self.create_key("flow-fcp-key", public_key)
            except Exception as e:
                logger.debug(f"Failed to create key from FCP_SSH_KEY: {e}")

        # Check for previously auto-generated keys
        auto_key = self._get_cached_auto_key()
        if auto_key:
            return auto_key

        # Generate a new key if none exist
        return self._generate_ssh_key()

    def _check_ssh_keygen_available(self) -> bool:
        """Check if ssh-keygen is available on the system.

        Returns:
            bool: True if ssh-keygen is found in PATH, False otherwise.
        """
        if shutil.which("ssh-keygen"):
            return True

        logger.warning(
            "ssh-keygen not found. SSH keys cannot be auto-generated. "
            "Install OpenSSH or manually create keys."
        )
        return False

    def _generate_ssh_key(self) -> Optional[str]:
        """Generate SSH key pair locally and register with FCP.

        Creates an ed25519 key pair using ssh-keygen, stores it in ~/.flow/keys,
        registers the public key with FCP API, and tracks metadata locally.

        Returns:
            Optional[str]: SSH key ID if successful, None if generation failed.
        """
        # Check if ssh-keygen is available
        if not self._check_ssh_keygen_available():
            return None

        try:
            # Generate unique name with timestamp and random component
            import random

            timestamp = int(time.time())
            random_suffix = random.randint(1000, 9999)
            key_name = f"flow-auto-{timestamp}-{random_suffix}"

            # Generate key pair locally
            private_path, public_path = self._create_key_pair(key_name)

            # Read public key
            public_key = public_path.read_text().strip()

            # Register with FCP API
            response = self.http.request(
                method="POST",
                url="/v2/ssh-keys",
                json={"name": key_name, "project": self.project_id, "public_key": public_key},
            )

            # Store metadata
            key_id = response["fid"]
            self._store_key_metadata(key_id, key_name, private_path)

            logger.info(f"Auto-generated SSH key: {key_id}")
            return key_id

        except Exception as e:
            logger.debug(f"Failed to auto-generate SSH key: {e}")
            return None

    def _create_key_pair(self, key_name: str) -> Tuple[Path, Path]:
        """Create SSH key pair using ssh-keygen.

        Args:
            key_name: Base name for the key files (without extension).

        Returns:
            Tuple[Path, Path]: Paths to (private_key, public_key).

        Raises:
            SSHKeyError: If ssh-keygen fails or returns non-zero exit code.
        """
        key_dir = Path.home() / ".flow" / "keys"
        key_dir.mkdir(parents=True, exist_ok=True)

        private_path = key_dir / key_name
        public_path = private_path.with_suffix(".pub")

        # Build ssh-keygen command with secure defaults
        cmd = [
            "ssh-keygen",
            "-t",
            "ed25519",  # Modern elliptic curve algorithm
            "-f",
            str(private_path),
            "-N",
            "",  # Empty passphrase for automation
            "-C",
            f"flow-auto@{platform.node()}",
        ]

        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)  # Prevent hanging
        if result.returncode != 0:
            raise SSHKeyError(f"ssh-keygen failed: {result.stderr}")

        # Ensure correct permissions
        self._set_key_permissions(private_path)

        return private_path, public_path

    def _store_key_metadata(self, key_id: str, key_name: str, private_path: Path) -> None:
        """Store metadata about auto-generated SSH key.

        Persists key information to ~/.flow/keys/metadata.json for future reuse.
        Updates existing metadata file if present, preserving other entries.

        Args:
            key_id: FCP-assigned SSH key ID.
            key_name: Human-readable key name.
            private_path: Path to the private key file.
        """
        key_dir = private_path.parent

        # Store metadata
        metadata = {
            "key_id": key_id,
            "key_name": key_name,
            "private_key_path": str(private_path),
            "created_at": datetime.now(timezone.utc).isoformat(),
            "project": self.project_id,
            "auto_generated": True,
        }

        metadata_path = key_dir / "metadata.json"
        existing = {}
        if metadata_path.exists():
            try:
                existing = json.loads(metadata_path.read_text())
            except Exception:
                pass

        existing[key_id] = metadata
        metadata_path.write_text(json.dumps(existing, indent=2))
        self._set_key_permissions(metadata_path)

    def _get_cached_auto_key(self) -> Optional[str]:
        """Check for previously auto-generated keys.

        Searches metadata.json for auto-generated keys belonging to the current
        project. Returns the most recently created key if multiple exist.

        Returns:
            Optional[str]: SSH key ID of most recent auto-generated key, or None.
        """
        metadata_path = Path.home() / ".flow" / "keys" / "metadata.json"
        if not metadata_path.exists():
            return None

        try:
            metadata = json.loads(metadata_path.read_text())
            # Filter keys by project and auto-generated flag
            project_keys = [
                (k, v)
                for k, v in metadata.items()
                if v.get("project") == self.project_id and v.get("auto_generated")
            ]
            if project_keys:
                # Sort by timestamp descending, return newest
                project_keys.sort(key=lambda x: x[1]["created_at"], reverse=True)
                return project_keys[0][0]
        except Exception:
            pass

        return None

    def _set_key_permissions(self, key_path: Path) -> None:
        """Set secure permissions on private key.

        Sets file permissions to 0600 (read/write for owner only) to meet
        SSH security requirements. Continues silently if permission setting
        fails (e.g., on non-Unix systems).

        Args:
            key_path: Path to the file requiring secure permissions.
        """
        try:
            key_path.chmod(0o600)
        except Exception as e:
            logger.debug(f"Could not set key permissions: {e}")
            # Continue execution - functional key with suboptimal permissions
            # is preferable to complete failure
