"""Sidecar health server that proxies health checks for non-SDK apps."""

import asyncio
import logging
import os
import time
from dataclasses import dataclass, field
from typing import Optional

import aiohttp
from aiohttp import web

logger = logging.getLogger("dory.sidecar")


@dataclass
class SidecarConfig:
    """Configuration for the sidecar server."""

    # Sidecar health server port
    health_port: int = 8080

    # Main app configuration (optional - for health forwarding)
    app_port: Optional[int] = None
    app_host: str = "localhost"
    app_health_path: Optional[str] = None  # e.g., "/health"
    app_ready_path: Optional[str] = None   # e.g., "/ready"
    app_prestop_path: Optional[str] = None # e.g., "/shutdown"

    # Timeouts
    app_check_timeout: float = 2.0
    prestop_timeout: float = 25.0

    # Behavior
    ready_requires_app: bool = False  # If True, /ready fails if app doesn't respond

    @classmethod
    def from_env(cls) -> "SidecarConfig":
        """Load configuration from environment variables."""
        return cls(
            health_port=int(os.getenv("DORY_HEALTH_PORT", "8080")),
            app_port=int(os.getenv("DORY_APP_PORT")) if os.getenv("DORY_APP_PORT") else None,
            app_host=os.getenv("DORY_APP_HOST", "localhost"),
            app_health_path=os.getenv("DORY_APP_HEALTH_PATH"),
            app_ready_path=os.getenv("DORY_APP_READY_PATH"),
            app_prestop_path=os.getenv("DORY_APP_PRESTOP_PATH"),
            app_check_timeout=float(os.getenv("DORY_APP_CHECK_TIMEOUT", "2.0")),
            prestop_timeout=float(os.getenv("DORY_PRESTOP_TIMEOUT", "25.0")),
            ready_requires_app=os.getenv("DORY_READY_REQUIRES_APP", "false").lower() == "true",
        )


class SidecarServer:
    """
    Lightweight sidecar server that provides Kubernetes health endpoints.

    This allows apps without SDK integration to run in Dory by providing
    the required health endpoints. Apps won't get state migration benefits,
    but they will run normally.

    Features:
    - Always responds to /healthz (sidecar is alive)
    - Optionally checks main app for /ready
    - Optionally calls main app on /prestop
    - Exposes basic /metrics
    """

    def __init__(self, config: Optional[SidecarConfig] = None):
        self.config = config or SidecarConfig.from_env()
        self._app: Optional[web.Application] = None
        self._runner: Optional[web.AppRunner] = None
        self._start_time = time.time()
        self._ready = True
        self._shutting_down = False
        self._request_count = 0
        self._app_check_failures = 0

    async def _check_app_endpoint(self, path: str) -> tuple[bool, str]:
        """Check if the main app responds on the given path."""
        if not self.config.app_port:
            return True, "No app port configured"

        url = f"http://{self.config.app_host}:{self.config.app_port}{path}"

        try:
            timeout = aiohttp.ClientTimeout(total=self.config.app_check_timeout)
            async with aiohttp.ClientSession(timeout=timeout) as session:
                async with session.get(url) as response:
                    if response.status < 400:
                        return True, f"App responded with {response.status}"
                    else:
                        return False, f"App responded with {response.status}"
        except asyncio.TimeoutError:
            return False, "App health check timed out"
        except aiohttp.ClientError as e:
            return False, f"App connection failed: {e}"
        except Exception as e:
            return False, f"App check error: {e}"

    async def _call_app_prestop(self) -> tuple[bool, str]:
        """Call the main app's prestop endpoint if configured."""
        if not self.config.app_port or not self.config.app_prestop_path:
            return True, "No app prestop path configured"

        url = f"http://{self.config.app_host}:{self.config.app_port}{self.config.app_prestop_path}"

        try:
            timeout = aiohttp.ClientTimeout(total=self.config.prestop_timeout)
            async with aiohttp.ClientSession(timeout=timeout) as session:
                async with session.get(url) as response:
                    body = await response.text()
                    return response.status < 400, f"App prestop returned {response.status}: {body}"
        except Exception as e:
            return False, f"App prestop failed: {e}"

    async def handle_healthz(self, request: web.Request) -> web.Response:
        """
        Liveness probe - returns 200 if sidecar is running.

        This always succeeds because if we can respond, we're alive.
        The main app's health is checked separately via /ready if configured.
        """
        self._request_count += 1

        return web.json_response({
            "status": "healthy",
            "sidecar": True,
            "uptime_seconds": int(time.time() - self._start_time),
        })

    async def handle_ready(self, request: web.Request) -> web.Response:
        """
        Readiness probe - optionally checks main app health.

        Behavior depends on configuration:
        - If app_ready_path is set, checks that endpoint
        - If app_health_path is set (and ready_path not), checks health endpoint
        - If app_port is set (no paths), checks if port is accepting connections
        - If ready_requires_app=true, fails if app check fails
        - Otherwise, always returns ready
        """
        self._request_count += 1

        if self._shutting_down:
            return web.json_response(
                {"status": "shutting_down", "ready": False},
                status=503
            )

        # Determine which path to check
        check_path = self.config.app_ready_path or self.config.app_health_path

        app_ok = True
        app_message = "No app check configured"

        if self.config.app_port:
            if check_path:
                app_ok, app_message = await self._check_app_endpoint(check_path)
            else:
                # Just check if port is open
                try:
                    _, writer = await asyncio.wait_for(
                        asyncio.open_connection(self.config.app_host, self.config.app_port),
                        timeout=self.config.app_check_timeout
                    )
                    writer.close()
                    await writer.wait_closed()
                    app_ok = True
                    app_message = "App port is accepting connections"
                except Exception as e:
                    app_ok = False
                    app_message = f"App port not responding: {e}"

        if not app_ok:
            self._app_check_failures += 1

        # Decide if we should report not ready
        if not app_ok and self.config.ready_requires_app:
            return web.json_response(
                {
                    "status": "not_ready",
                    "ready": False,
                    "app_status": app_message,
                    "sidecar": True,
                },
                status=503
            )

        return web.json_response({
            "status": "ready",
            "ready": True,
            "app_status": app_message,
            "app_ok": app_ok,
            "sidecar": True,
        })

    async def handle_prestop(self, request: web.Request) -> web.Response:
        """
        PreStop hook handler - signals graceful shutdown.

        If app_prestop_path is configured, calls that endpoint to give
        the main app a chance to clean up. Otherwise just marks as shutting down.

        Note: Without SDK integration, no state will be saved.
        """
        self._request_count += 1
        self._shutting_down = True
        self._ready = False

        logger.info("PreStop hook called - beginning graceful shutdown")

        # Call app's prestop if configured
        app_prestop_ok, app_prestop_message = await self._call_app_prestop()

        if not app_prestop_ok:
            logger.warning(f"App prestop failed: {app_prestop_message}")

        return web.json_response({
            "status": "shutting_down",
            "app_prestop": app_prestop_message,
            "state_saved": False,  # No state save without SDK
            "sidecar": True,
        })

    async def handle_metrics(self, request: web.Request) -> web.Response:
        """Prometheus metrics endpoint."""
        self._request_count += 1

        uptime = time.time() - self._start_time

        metrics = f"""# HELP dory_sidecar_up Sidecar is running
# TYPE dory_sidecar_up gauge
dory_sidecar_up 1

# HELP dory_sidecar_uptime_seconds Sidecar uptime in seconds
# TYPE dory_sidecar_uptime_seconds gauge
dory_sidecar_uptime_seconds {uptime:.2f}

# HELP dory_sidecar_requests_total Total requests handled
# TYPE dory_sidecar_requests_total counter
dory_sidecar_requests_total {self._request_count}

# HELP dory_sidecar_app_check_failures_total App health check failures
# TYPE dory_sidecar_app_check_failures_total counter
dory_sidecar_app_check_failures_total {self._app_check_failures}

# HELP dory_sidecar_shutting_down Sidecar is shutting down
# TYPE dory_sidecar_shutting_down gauge
dory_sidecar_shutting_down {1 if self._shutting_down else 0}

# HELP dory_sidecar_ready Sidecar ready status
# TYPE dory_sidecar_ready gauge
dory_sidecar_ready {1 if self._ready and not self._shutting_down else 0}
"""
        return web.Response(text=metrics, content_type="text/plain")

    async def handle_state(self, request: web.Request) -> web.Response:
        """
        State endpoint - returns empty state for non-SDK apps.

        This endpoint exists for compatibility but returns no state
        since the app doesn't use the SDK for state management.
        """
        self._request_count += 1

        if request.method == "GET":
            return web.json_response({
                "state": None,
                "message": "No state available - app does not use Dory SDK",
                "sidecar": True,
            })
        elif request.method == "POST":
            return web.json_response({
                "restored": False,
                "message": "Cannot restore state - app does not use Dory SDK",
                "sidecar": True,
            })
        else:
            return web.json_response(
                {"error": "Method not allowed"},
                status=405
            )

    def _create_app(self) -> web.Application:
        """Create the aiohttp application."""
        app = web.Application()

        app.router.add_get("/healthz", self.handle_healthz)
        app.router.add_get("/ready", self.handle_ready)
        app.router.add_get("/prestop", self.handle_prestop)
        app.router.add_get("/metrics", self.handle_metrics)
        app.router.add_get("/state", self.handle_state)
        app.router.add_post("/state", self.handle_state)

        return app

    async def start(self) -> None:
        """Start the sidecar server."""
        self._app = self._create_app()
        self._runner = web.AppRunner(self._app)
        await self._runner.setup()

        site = web.TCPSite(self._runner, "0.0.0.0", self.config.health_port)
        await site.start()

        logger.info(f"Dory sidecar started on port {self.config.health_port}")

        if self.config.app_port:
            logger.info(f"Monitoring app on {self.config.app_host}:{self.config.app_port}")
        else:
            logger.info("No app port configured - running in standalone mode")

    async def stop(self) -> None:
        """Stop the sidecar server."""
        if self._runner:
            await self._runner.cleanup()
            logger.info("Dory sidecar stopped")

    async def run_forever(self) -> None:
        """Run the sidecar server until interrupted."""
        await self.start()

        try:
            while True:
                await asyncio.sleep(3600)
        except asyncio.CancelledError:
            pass
        finally:
            await self.stop()
