"""
Streamlit Manager — Launch and manage Streamlit apps locally.

Port pool: 8501-8520 (20 simultaneous apps).
Apps persist until explicitly stopped or Runtime shuts down.
"""
import asyncio
import socket
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, field

from .config import RuntimeConfig
from .docker_executor import DockerExecutor


@dataclass
class StreamlitApp:
    """A running Streamlit app."""
    app_id: str
    session_id: str
    port: int
    container: str
    title: str = "Streamlit App"
    status: str = "running"


class StreamlitManager:
    """Manages Streamlit app lifecycle on localhost."""

    def __init__(self, config: RuntimeConfig, executor: DockerExecutor):
        self.config = config
        self.executor = executor
        self._apps: Dict[str, StreamlitApp] = {}  # app_id -> StreamlitApp

    def find_free_port(self) -> Optional[int]:
        """Find an available port in the configured range."""
        used_ports = {app.port for app in self._apps.values()}
        for port in range(self.config.streamlit_port_start, self.config.streamlit_port_end + 1):
            if port in used_ports:
                continue
            try:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                    s.bind(("localhost", port))
                    return port
            except OSError:
                continue
        return None

    async def launch(
        self,
        app_id: str,
        session_id: str,
        app_code: str,
        requirements: Optional[List[str]] = None,
        title: str = "Streamlit App",
    ) -> Dict[str, Any]:
        """Launch a Streamlit app in a Docker container."""
        port = self.find_free_port()
        if port is None:
            return {"success": False, "error": "No free ports available (max 20 apps)"}

        # Ensure container exists
        container = self.executor._containers.get(session_id)
        if not container:
            result = await self.executor.create_container(session_id, template="default")
            if not result["success"]:
                return result
            container = result["container"]

        # Install streamlit + requirements
        packages = ["streamlit", "plotly"] + (requirements or [])
        await self.executor._exec(
            container,
            f"pip install --quiet {' '.join(packages)}",
            timeout=120,
            network=True,
        )

        # Write app code
        proc = await asyncio.create_subprocess_exec(
            "docker", "exec", "-i", container,
            "bash", "-c", "cat > /home/sandbox/app.py",
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )
        await proc.communicate(input=app_code.encode())

        # Need to reconnect container with port mapping
        # Stop current container, restart with port
        await asyncio.create_subprocess_exec(
            "docker", "rm", "-f", container,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.DEVNULL,
        )

        # Recreate with port mapping
        image = "neura-sandbox-data"  # has streamlit pre-installed
        new_cmd = [
            "docker", "run", "-d",
            "--name", container,
            "-p", f"{port}:8501",
            f"--memory={self.config.docker_memory_limit}",
            f"--cpus={self.config.docker_cpu_limit}",
            "--security-opt", "no-new-privileges",
            image,
            "bash", "-c",
            f"pip install --quiet {' '.join(packages)} 2>/dev/null; "
            f"cat > /home/sandbox/app.py << 'NEURA_APP_EOF'\n{app_code}\nNEURA_APP_EOF\n"
            f"streamlit run /home/sandbox/app.py "
            f"--server.port=8501 --server.headless=true "
            f"--server.address=0.0.0.0 "
            f"--browser.gatherUsageStats=false",
        ]

        proc = await asyncio.create_subprocess_exec(
            *new_cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()

        if proc.returncode != 0:
            return {"success": False, "error": stderr.decode()}

        self.executor._containers[session_id] = container

        # Wait for Streamlit to start
        await asyncio.sleep(3)

        app = StreamlitApp(
            app_id=app_id,
            session_id=session_id,
            port=port,
            container=container,
            title=title,
        )
        self._apps[app_id] = app

        url = f"http://localhost:{port}"
        return {
            "success": True,
            "url": url,
            "port": port,
            "app_id": app_id,
            "message": f"Streamlit app '{title}' running at {url}",
        }

    async def stop(self, app_id: str) -> bool:
        """Stop a running Streamlit app."""
        app = self._apps.pop(app_id, None)
        if not app:
            return False

        await self.executor.destroy_container(app.session_id)
        return True

    async def stop_all(self):
        """Stop all running Streamlit apps."""
        for app_id in list(self._apps.keys()):
            await self.stop(app_id)

    def list_apps(self) -> List[Dict[str, Any]]:
        """List all running Streamlit apps."""
        return [
            {
                "app_id": app.app_id,
                "title": app.title,
                "url": f"http://localhost:{app.port}",
                "port": app.port,
                "status": app.status,
            }
            for app in self._apps.values()
        ]
