"""
Modal Deployment Manager - Handles deployment orchestration with optional locking.

This module separates deployment logic from the main sandbox execution,
making it easier to understand and optionally disable locking/version tracking.
"""

import hashlib
from typing import Tuple

import modal

from letta.log import get_logger
from letta.schemas.sandbox_config import SandboxConfig
from letta.schemas.tool import Tool
from letta.services.tool_sandbox.modal_constants import VERSION_HASH_LENGTH
from letta.services.tool_sandbox.modal_version_manager import ModalVersionManager, get_version_manager

logger = get_logger(__name__)


class ModalDeploymentManager:
    """Manages Modal app deployments with optional locking and version tracking."""

    def __init__(
        self,
        tool: Tool,
        version_manager: ModalVersionManager | None = None,
        use_locking: bool = True,
        use_version_tracking: bool = True,
    ):
        """
        Initialize deployment manager.

        Args:
            tool: The tool to deploy
            version_manager: Version manager for tracking deployments (optional)
            use_locking: Whether to use locking for coordinated deployments
            use_version_tracking: Whether to track and reuse existing deployments
        """
        self.tool = tool
        self.version_manager = version_manager or get_version_manager() if (use_locking or use_version_tracking) else None
        self.use_locking = use_locking
        self.use_version_tracking = use_version_tracking
        self._app_name = self._generate_app_name()

    def _generate_app_name(self) -> str:
        """Generate app name based on tool ID."""
        return self.tool.id[:40]

    def calculate_version_hash(self, sbx_config: SandboxConfig) -> str:
        """Calculate version hash for the current configuration."""
        components = (
            self.tool.source_code,
            str(self.tool.pip_requirements) if self.tool.pip_requirements else "",
            str(self.tool.npm_requirements) if self.tool.npm_requirements else "",
            sbx_config.fingerprint(),
        )
        combined = "|".join(components)
        return hashlib.sha256(combined.encode()).hexdigest()[:VERSION_HASH_LENGTH]

    def get_full_app_name(self, version_hash: str) -> str:
        """Get the full app name including version."""
        app_full_name = f"{self._app_name}-{version_hash}"
        # Ensure total length is under 64 characters
        if len(app_full_name) > 63:
            max_id_len = 63 - len(version_hash) - 1
            app_full_name = f"{self._app_name[:max_id_len]}-{version_hash}"
        return app_full_name

    async def get_or_deploy_app(
        self,
        sbx_config: SandboxConfig,
        user,
        create_app_func,
    ) -> Tuple[modal.App, str]:
        """
        Get existing app or deploy new one.

        Args:
            sbx_config: Sandbox configuration
            user: User/actor for permissions
            create_app_func: Function to create and deploy the app

        Returns:
            Tuple of (Modal app, version hash)
        """
        version_hash = self.calculate_version_hash(sbx_config)

        # Simple path: no version tracking or locking
        if not self.use_version_tracking:
            logger.info(f"Deploying Modal app {self._app_name} (version tracking disabled)")
            app = await create_app_func(sbx_config, version_hash)
            return app, version_hash

        # Try to use existing deployment
        if self.use_version_tracking:
            existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
            if existing_app:
                return existing_app, version_hash

        # Need to deploy - with or without locking
        if self.use_locking:
            return await self._deploy_with_locking(sbx_config, version_hash, user, create_app_func)
        else:
            return await self._deploy_without_locking(sbx_config, version_hash, user, create_app_func)

    async def _try_get_existing_app(
        self,
        sbx_config: SandboxConfig,
        version_hash: str,
        user,
    ) -> modal.App | None:
        """Try to get an existing deployed app."""
        if not self.version_manager:
            return None

        deployment = await self.version_manager.get_deployment(
            tool_id=self.tool.id, sandbox_config_id=sbx_config.id if sbx_config else None, actor=user
        )

        if deployment and deployment.version_hash == version_hash:
            app_full_name = self.get_full_app_name(version_hash)
            logger.info(f"Checking for existing Modal app {app_full_name}")

            try:
                app = await modal.App.lookup.aio(app_full_name)
                logger.info(f"Found existing Modal app {app_full_name}")
                return app
            except Exception:
                logger.info(f"Modal app {app_full_name} not found in Modal, will redeploy")
                return None

        return None

    async def _deploy_without_locking(
        self,
        sbx_config: SandboxConfig,
        version_hash: str,
        user,
        create_app_func,
    ) -> Tuple[modal.App, str]:
        """Deploy without locking - simpler but may have race conditions."""
        app_full_name = self.get_full_app_name(version_hash)
        logger.info(f"Deploying Modal app {app_full_name} (no locking)")

        # Deploy the app
        app = await create_app_func(sbx_config, version_hash)

        # Register deployment if tracking is enabled
        if self.use_version_tracking and self.version_manager:
            await self._register_deployment(sbx_config, version_hash, app, user)

        return app, version_hash

    async def _deploy_with_locking(
        self,
        sbx_config: SandboxConfig,
        version_hash: str,
        user,
        create_app_func,
    ) -> Tuple[modal.App, str]:
        """Deploy with locking to prevent concurrent deployments."""
        cache_key = f"{self.tool.id}:{sbx_config.id if sbx_config else 'default'}"
        deployment_lock = self.version_manager.get_deployment_lock(cache_key)

        async with deployment_lock:
            # Double-check after acquiring lock
            existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
            if existing_app:
                return existing_app, version_hash

            # Check if another process is deploying
            if self.version_manager.is_deployment_in_progress(cache_key, version_hash):
                logger.info(f"Another process is deploying {self._app_name} v{version_hash}, waiting...")
                # Release lock and wait
                deployment_lock = None

        # Wait for other deployment if needed
        if deployment_lock is None:
            success = await self.version_manager.wait_for_deployment(cache_key, version_hash, timeout=120)
            if success:
                existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
                if existing_app:
                    return existing_app, version_hash
                raise RuntimeError("Deployment completed but app not found")
            else:
                raise RuntimeError("Timeout waiting for deployment")

        # We're deploying - mark as in progress
        deployment_key = None
        async with deployment_lock:
            deployment_key = self.version_manager.mark_deployment_in_progress(cache_key, version_hash)

        try:
            app_full_name = self.get_full_app_name(version_hash)
            logger.info(f"Deploying Modal app {app_full_name} with locking")

            # Deploy the app
            app = await create_app_func(sbx_config, version_hash)

            # Mark deployment complete
            if deployment_key:
                self.version_manager.complete_deployment(deployment_key)

            # Register deployment
            if self.use_version_tracking:
                await self._register_deployment(sbx_config, version_hash, app, user)

            return app, version_hash

        except Exception:
            if deployment_key:
                self.version_manager.complete_deployment(deployment_key)
            raise

    async def _register_deployment(
        self,
        sbx_config: SandboxConfig,
        version_hash: str,
        app: modal.App,
        user,
    ):
        if not self.version_manager:
            return

        dependencies = set()
        if self.tool.pip_requirements:
            dependencies.update(str(req) for req in self.tool.pip_requirements)
        modal_config = sbx_config.get_modal_config()
        if modal_config.pip_requirements:
            dependencies.update(str(req) for req in modal_config.pip_requirements)

        await self.version_manager.register_deployment(
            tool_id=self.tool.id,
            app_name=self._app_name,
            version_hash=version_hash,
            app=app,
            dependencies=dependencies,
            sandbox_config_id=sbx_config.id if sbx_config else None,
            actor=user,
        )
