import asyncio
import os
import pty
import re
import select
import subprocess
import threading
from collections import deque
from pathlib import Path
from typing import Optional

import httpx

from mosaygent.logger import get_logger

logger = get_logger(__name__)

EXPO_PORT = 8081


class ExpoService:
    """Manages the Expo development server process."""

    def __init__(self, app_dir: Path, port: int = EXPO_PORT):
        self.app_dir = app_dir
        self.port = port
        self.process: Optional[subprocess.Popen] = None
        self.master_fd: Optional[int] = None
        self.output_buffer: deque = deque(maxlen=100)
        self.output_thread: Optional[threading.Thread] = None
        self._stop_output_thread = False

    def _read_output(self):
        """Background thread to capture output from PTY."""
        logger.debug("Starting output capture thread")
        try:
            while not self._stop_output_thread and self.master_fd is not None:
                if self.process and self.process.poll() is not None:
                    break

                ready, _, _ = select.select([self.master_fd], [], [], 0.1)
                if ready:
                    try:
                        data = os.read(self.master_fd, 1024)
                        if data:
                            decoded = data.decode('utf-8', errors='replace')
                            for line in decoded.splitlines():
                                if line.strip():
                                    self.output_buffer.append(line.strip())
                                    logger.debug(f"Expo output: {line.strip()}")
                    except OSError:
                        break
        except Exception as e:
            logger.exception(f"Error in output capture thread: {e}")
        finally:
            logger.debug("Output capture thread stopped")

    def start(self) -> dict:
        """Start the Expo development server with PTY for interactive input."""
        if self.process and self.process.poll() is None:
            logger.info("Expo is already running")
            return {"message": "Expo is already running", "pid": self.process.pid}

        if not self.app_dir.exists():
            logger.error(f"App directory not found: {self.app_dir}")
            raise FileNotFoundError(f"App directory not found: {self.app_dir}")

        self.output_buffer.clear()
        self._stop_output_thread = False

        logger.info(f"Starting Expo dev server on port {self.port}")
        try:
            # Create a pseudo-terminal for interactive input
            master_fd, slave_fd = pty.openpty()
            self.master_fd = master_fd

            self.process = subprocess.Popen(
                ["npx", "expo", "start", f"--port={self.port}"],
                cwd=str(self.app_dir),
                stdin=slave_fd,
                stdout=slave_fd,
                stderr=slave_fd,
            )

            # Close slave in parent process
            os.close(slave_fd)

            logger.info(f"Expo process started with PID {self.process.pid}")

            self.output_thread = threading.Thread(target=self._read_output, daemon=True)
            self.output_thread.start()
            logger.info("Output capture enabled - use /expo/logs to check output")

            return {"message": "Expo process started", "pid": self.process.pid, "port": self.port}
        except Exception as e:
            logger.exception(f"Failed to start Expo: {e}")
            raise

    def stop(self) -> dict:
        """Stop the Expo development server."""
        if not self.process or self.process.poll() is not None:
            logger.info("Expo is not running")
            return {"message": "Expo is not running"}

        logger.info("Stopping Expo process")
        self._stop_output_thread = True

        try:
            self.process.terminate()
            self.process.wait(timeout=5)
            logger.info("Expo process stopped gracefully")
        except subprocess.TimeoutExpired:
            logger.warning("Expo process did not stop gracefully, forcing kill")
            self.process.kill()
            self.process.wait()
        except Exception as e:
            logger.exception(f"Failed to stop Expo: {e}")
            raise

        if self.output_thread and self.output_thread.is_alive():
            self.output_thread.join(timeout=2)

        if self.master_fd is not None:
            try:
                os.close(self.master_fd)
            except OSError:
                pass
            self.master_fd = None

        return {"message": "Expo process stopped"}

    def get_status(self) -> dict:
        """Get the current status of the Expo development server."""
        if not self.process:
            return {"running": False, "message": "Not started"}

        poll_result = self.process.poll()
        if poll_result is None:
            return {
                "running": True,
                "pid": self.process.pid,
                "port": self.port,
                "metro_url": self.get_metro_url(),
                "web_url": self.get_web_url(),
                "lan_address": self.get_lan_address(),
            }
        else:
            return {
                "running": False,
                "exit_code": poll_result
            }

    def trigger_reload(self) -> bool:
        """Trigger a reload by sending 'r' key to the Expo process."""
        if not self.process or self.process.poll() is not None:
            logger.warning("Cannot reload: Expo is not running")
            return False

        if self.master_fd is None:
            logger.warning("Cannot reload: PTY not available")
            return False

        logger.info("Triggering reload by sending 'r' key")
        try:
            os.write(self.master_fd, b'r')
            logger.info("Reload command sent")
            return True
        except Exception as e:
            logger.exception(f"Failed to trigger reload: {e}")
            return False

    def _check_ready_from_output(self) -> bool:
        """Check if Expo is ready based on output buffer messages."""
        ready_indicators = [
            "Logs for your project will appear below",
            "Metro waiting on",
            "Waiting on http://localhost",
        ]
        for line in self.output_buffer:
            for indicator in ready_indicators:
                if indicator in line:
                    return True
        return False

    async def wait_for_ready(self, timeout: int = 60) -> bool:
        """Wait for the Expo server to become ready."""
        logger.info(f"Waiting for Expo to be ready (timeout: {timeout}s)")

        async with httpx.AsyncClient() as client:
            for i in range(timeout):
                if self.process and self.process.poll() is not None:
                    logger.error(f"Expo process died with exit code {self.process.poll()}")
                    return False

                if self._check_ready_from_output():
                    logger.info("Expo server is ready (detected from output)")
                    return True

                try:
                    response = await client.get(
                        f"http://localhost:{self.port}/status",
                        timeout=1.0
                    )
                    if response.status_code == 200:
                        logger.info("Expo server is ready (HTTP check)")
                        return True
                except (httpx.RequestError, httpx.TimeoutException):
                    logger.debug(f"Expo not ready yet (attempt {i + 1}/{timeout})")

                await asyncio.sleep(1)

        logger.warning("Expo did not become ready in time")
        return False

    def get_logs(self) -> list:
        """Get all captured output messages."""
        return list(self.output_buffer)

    def get_metro_url(self) -> Optional[str]:
        """Extract the Metro URL from captured output logs."""
        for line in self.output_buffer:
            # Development client URL: exp+slug://expo-development-client/?url=http%3A%2F%2F192.168.1.102%3A8081
            if 'expo-development-client' in line:
                match = re.search(r'(exp\+[^:]+://expo-development-client/\?url=[^\s\x1b]+)', line)
                if match:
                    url = match.group(1)
                    logger.debug(f"Found development client URL: {url}")
                    return url

            # Standard exp:// URL format
            match = re.search(r'(exp://[\d.]+:\d+)', line)
            if match:
                url = match.group(1)
                logger.debug(f"Found Metro URL: {url}")
                return url

        logger.debug("Metro URL not found in output buffer")
        return None

    def get_web_url(self) -> Optional[str]:
        """Extract the web URL from captured output logs."""
        for line in self.output_buffer:
            if 'Web is waiting on' in line:
                match = re.search(r'(https?://[^\s\x1b]+)', line)
                if match:
                    url = match.group(1)
                    logger.debug(f"Found web URL: {url}")
                    return url
        return None

    def get_lan_address(self) -> Optional[str]:
        """Extract LAN IP address and port from the development client URL."""
        for line in self.output_buffer:
            if 'expo-development-client' in line:
                match = re.search(r'url=http%3A%2F%2F([\d.]+)%3A(\d+)', line)
                if match:
                    ip = match.group(1)
                    port = match.group(2)
                    logger.debug(f"Found LAN address: {ip}:{port}")
                    return f"{ip}:{port}"
        return None

    def get_connection_info(self) -> dict:
        """Get Metro connection information including URL and port."""
        running = self.process is not None and self.process.poll() is None
        metro_url = self.get_metro_url()
        web_url = self.get_web_url()
        lan_address = self.get_lan_address()

        info = {
            "running": running,
            "port": self.port,
            "metro_url": metro_url,
            "web_url": web_url,
            "lan_address": lan_address,
        }

        logger.debug(f"Connection info: {info}")
        return info
