"""Real-time monitoring dashboard for connection pools.

This module provides a web-based dashboard for monitoring connection pool
health, performance metrics, and alerts. It integrates with the metrics
collection system to provide real-time visualization.

Features:
- Real-time pool statistics with WebSocket updates
- Health score visualization with history
- Alert configuration and notifications
- Historical trend analysis with charts
- Export capabilities for reports

Example:
    >>> dashboard = ConnectionDashboardNode(
    ...     name="pool_monitor",
    ...     port=8080,
    ...     update_interval=1.0,
    ...     retention_hours=24
    ... )
    >>>
    >>> # Start dashboard server
    >>> await dashboard.start()
    >>>
    >>> # Access at http://localhost:8080
"""

import asyncio
import json
import logging
import time
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any, Deque, Dict, List, Optional, Set

import aiohttp_cors
from aiohttp import web

from kailash.nodes.base import Node, NodeParameter, register_node

logger = logging.getLogger(__name__)


@dataclass
class AlertRule:
    """Alert rule configuration."""

    id: str
    name: str
    condition: str  # e.g., "pool_utilization > 0.9"
    threshold: float
    duration_seconds: int = 60  # How long condition must be true
    cooldown_seconds: int = 300  # Prevent alert spam
    severity: str = "warning"  # info, warning, error, critical
    enabled: bool = True
    last_triggered: Optional[float] = None

    def is_in_cooldown(self) -> bool:
        """Check if alert is in cooldown period."""
        if not self.last_triggered:
            return False
        return (time.time() - self.last_triggered) < self.cooldown_seconds


@dataclass
class Alert:
    """Active alert instance."""

    rule_id: str
    triggered_at: float
    severity: str
    message: str
    metric_value: float
    resolved: bool = False
    resolved_at: Optional[float] = None

    def duration(self) -> float:
        """Get alert duration in seconds."""
        end_time = self.resolved_at or time.time()
        return end_time - self.triggered_at


class MetricsCache:
    """Cache for metrics data with time-based expiration."""

    def __init__(self, retention_hours: int = 24):
        """Initialize metrics cache."""
        self.retention_hours = retention_hours
        self._data: Dict[str, List[Dict[str, Any]]] = {}
        self._last_cleanup = time.time()

    _MAX_ENTRIES_PER_METRIC = 3600  # 1 hour at 1-second intervals

    def add(self, metric_name: str, value: Dict[str, Any]):
        """Add metric value to cache."""
        if metric_name not in self._data:
            self._data[metric_name] = []

        value["timestamp"] = time.time()
        self._data[metric_name].append(value)

        # Cap list at max entries
        if len(self._data[metric_name]) > self._MAX_ENTRIES_PER_METRIC:
            self._data[metric_name] = self._data[metric_name][
                -self._MAX_ENTRIES_PER_METRIC :
            ]

        # Periodic cleanup
        if time.time() - self._last_cleanup > 3600:  # Every hour
            self._cleanup()

    def get_recent(self, metric_name: str, minutes: int = 60) -> List[Dict[str, Any]]:
        """Get recent metric values."""
        if metric_name not in self._data:
            return []

        cutoff = time.time() - (minutes * 60)
        return [v for v in self._data[metric_name] if v["timestamp"] >= cutoff]

    def _cleanup(self):
        """Remove old data."""
        cutoff = time.time() - (self.retention_hours * 3600)

        for metric_name in list(self._data.keys()):
            self._data[metric_name] = [
                v for v in self._data[metric_name] if v["timestamp"] >= cutoff
            ]

            # Remove empty metrics
            if not self._data[metric_name]:
                del self._data[metric_name]

        self._last_cleanup = time.time()


@register_node()
class ConnectionDashboardNode(Node):
    """Web-based monitoring dashboard for connection pools.

    Provides real-time visualization of connection pool metrics,
    health scores, and alerts through a web interface.
    """

    def __init__(self, **config):
        """Initialize dashboard node.

        Args:
            port: Web server port (default: 8080)
            host: Web server host (default: localhost)
            update_interval: Metric update interval in seconds (default: 1.0)
            retention_hours: How long to keep historical data (default: 24)
            enable_alerts: Enable alert system (default: True)
        """
        self.port = config.get("port", 8080)
        self.host = config.get("host", "localhost")
        self.update_interval = config.get("update_interval", 1.0)
        self.retention_hours = config.get("retention_hours", 24)
        self.enable_alerts = config.get("enable_alerts", True)

        super().__init__(**config)

        # Web server
        self.app = None
        self.runner = None
        self.site = None

        # WebSocket connections
        self._websockets: Set[web.WebSocketResponse] = set()

        # Metrics cache
        self._cache = MetricsCache(self.retention_hours)

        # Alert system
        self._alert_rules: Dict[str, AlertRule] = {}
        self._active_alerts: Dict[str, Alert] = {}
        self._alert_history: Deque[Alert] = deque(maxlen=1000)

        # Update task
        self._update_task: Optional[asyncio.Task] = None

        # Initialize default alert rules
        self._init_default_alerts()

    def get_parameters(self) -> Dict[str, NodeParameter]:
        """Get node parameters."""
        return {
            "port": NodeParameter(
                name="port", type=int, default=8080, description="Web server port"
            ),
            "host": NodeParameter(
                name="host",
                type=str,
                default="localhost",
                description="Web server host",
            ),
            "update_interval": NodeParameter(
                name="update_interval",
                type=float,
                default=1.0,
                description="Metric update interval in seconds",
            ),
            "retention_hours": NodeParameter(
                name="retention_hours",
                type=int,
                default=24,
                description="Historical data retention in hours",
            ),
            "enable_alerts": NodeParameter(
                name="enable_alerts",
                type=bool,
                default=True,
                description="Enable alert system",
            ),
            "action": NodeParameter(
                name="action",
                type=str,
                required=False,
                description="Dashboard action",
                choices=["start", "stop", "status"],
            ),
        }

    async def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:  # type: ignore[override]
        """Execute dashboard action.

        Actions:
        - start: Start the dashboard server
        - stop: Stop the dashboard server
        - status: Get dashboard status
        """
        action = input_data.get("action", "status")

        if action == "start":
            await self.start()
            return {
                "status": "started",
                "url": f"http://{self.host}:{self.port}",
                "websocket": f"ws://{self.host}:{self.port}/ws",
            }

        elif action == "stop":
            await self.stop()
            return {"status": "stopped"}

        else:
            return self.get_status()

    async def start(self):
        """Start the dashboard web server."""
        if self.app:
            logger.warning("Dashboard already running")
            return

        # Create web app
        self.app = web.Application()

        # Setup CORS
        cors = aiohttp_cors.setup(
            self.app,
            defaults={
                "*": aiohttp_cors.ResourceOptions(
                    allow_credentials=True, expose_headers="*", allow_headers="*"
                )
            },
        )

        # Add routes
        self.app.router.add_get("/", self._handle_index)
        self.app.router.add_get("/api/metrics", self._handle_metrics)
        self.app.router.add_get("/api/pools", self._handle_pools)
        self.app.router.add_get("/api/alerts", self._handle_alerts)
        self.app.router.add_post("/api/alerts", self._handle_create_alert)
        self.app.router.add_delete("/api/alerts/{alert_id}", self._handle_delete_alert)
        self.app.router.add_get("/api/history/{metric_name}", self._handle_history)
        self.app.router.add_get("/ws", self._handle_websocket)

        # Configure CORS for all routes
        for route in list(self.app.router.routes()):
            cors.add(route)

        # Start server
        self.runner = web.AppRunner(self.app)
        await self.runner.setup()
        self.site = web.TCPSite(self.runner, self.host, self.port)
        await self.site.start()

        # Start update task
        self._update_task = asyncio.create_task(self._update_loop())

        logger.info(f"Dashboard started at http://{self.host}:{self.port}")

    async def stop(self):
        """Stop the dashboard web server."""
        # Stop update task
        if self._update_task:
            self._update_task.cancel()
            try:
                await self._update_task
            except asyncio.CancelledError:
                pass

        # Close WebSocket connections
        for ws in list(self._websockets):
            await ws.close()

        # Stop web server
        if self.site:
            await self.site.stop()
        if self.runner:
            await self.runner.cleanup()

        self.app = None
        self.runner = None
        self.site = None

        logger.info("Dashboard stopped")

    async def _handle_index(self, request: web.Request) -> web.Response:
        """Serve dashboard HTML."""
        html = """
<!DOCTYPE html>
<html>
<head>
    <title>Connection Pool Dashboard</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; }
        .pool-card { background: white; padding: 20px; margin: 10px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .metric { display: inline-block; margin: 10px 20px 10px 0; }
        .metric-label { color: #666; font-size: 12px; }
        .metric-value { font-size: 24px; font-weight: bold; }
        .health-bar { width: 100%; height: 20px; background: #ddd; border-radius: 10px; overflow: hidden; }
        .health-fill { height: 100%; transition: width 0.3s, background-color 0.3s; }
        .health-good { background: #4caf50; }
        .health-warning { background: #ff9800; }
        .health-critical { background: #f44336; }
        .alert { padding: 10px; margin: 5px 0; border-radius: 4px; }
        .alert-warning { background: #fff3cd; border: 1px solid #ffeeba; }
        .alert-error { background: #f8d7da; border: 1px solid #f5c6cb; }
        .alert-critical { background: #d1ecf1; border: 1px solid #bee5eb; }
        .chart { width: 100%; height: 200px; margin: 20px 0; }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <div class="container">
        <h1>Connection Pool Dashboard</h1>

        <div id="alerts"></div>

        <h2>Connection Pools</h2>
        <div id="pools"></div>

        <h2>Metrics History</h2>
        <canvas id="metricsChart" class="chart"></canvas>
    </div>

    <script>
        // WebSocket connection
        const ws = new WebSocket(`ws://${window.location.host}/ws`);

        // Chart setup
        const ctx = document.getElementById('metricsChart').getContext('2d');
        const chart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: 'Pool Utilization',
                    data: [],
                    borderColor: 'rgb(75, 192, 192)',
                    tension: 0.1
                }]
            },
            options: {
                responsive: true,
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 1
                    }
                }
            }
        });

        // Helper to safely set text content
        function setText(el, value) { el.textContent = String(value); }

        // Update functions
        function updatePools(pools) {
            const container = document.getElementById('pools');
            container.innerHTML = '';

            for (const [name, pool] of Object.entries(pools)) {
                const healthClass = pool.health_score > 80 ? 'health-good' :
                                  pool.health_score > 60 ? 'health-warning' : 'health-critical';

                const card = document.createElement('div');
                card.className = 'pool-card';

                const title = document.createElement('h3');
                setText(title, name);
                card.appendChild(title);

                const healthBar = document.createElement('div');
                healthBar.className = 'health-bar';
                const healthFill = document.createElement('div');
                healthFill.className = 'health-fill ' + healthClass;
                healthFill.style.width = pool.health_score + '%';
                healthBar.appendChild(healthFill);
                card.appendChild(healthBar);

                const metrics = [
                    ['Active Connections', pool.active_connections],
                    ['Total Connections', pool.total_connections],
                    ['Utilization', (pool.utilization * 100).toFixed(1) + '%'],
                    ['Queries/sec', pool.queries_per_second.toFixed(1)],
                    ['Avg Query Time', pool.avg_query_time_ms.toFixed(1) + 'ms'],
                    ['Error Rate', (pool.error_rate * 100).toFixed(2) + '%'],
                ];
                for (const [label, value] of metrics) {
                    const metricDiv = document.createElement('div');
                    metricDiv.className = 'metric';
                    const labelDiv = document.createElement('div');
                    labelDiv.className = 'metric-label';
                    setText(labelDiv, label);
                    const valueDiv = document.createElement('div');
                    valueDiv.className = 'metric-value';
                    setText(valueDiv, value);
                    metricDiv.appendChild(labelDiv);
                    metricDiv.appendChild(valueDiv);
                    card.appendChild(metricDiv);
                }

                container.appendChild(card);
            }
        }

        function updateAlerts(alerts) {
            const container = document.getElementById('alerts');
            container.innerHTML = '';

            if (alerts.length === 0) return;

            const heading = document.createElement('h2');
            setText(heading, 'Active Alerts');
            container.appendChild(heading);

            for (const alert of alerts) {
                const div = document.createElement('div');
                div.className = 'alert alert-' + alert.severity;
                const strong = document.createElement('strong');
                setText(strong, alert.message);
                div.appendChild(strong);
                const time = document.createTextNode(
                    ' - ' + new Date(alert.triggered_at * 1000).toLocaleTimeString()
                );
                div.appendChild(time);
                container.appendChild(div);
            }
        }

        function updateChart(data) {
            // Update chart with latest data
            if (data.timestamp && data.utilization !== undefined) {
                const time = new Date(data.timestamp * 1000).toLocaleTimeString();

                chart.data.labels.push(time);
                chart.data.datasets[0].data.push(data.utilization);

                // Keep only last 60 points
                if (chart.data.labels.length > 60) {
                    chart.data.labels.shift();
                    chart.data.datasets[0].data.shift();
                }

                chart.update('none');  // No animation for real-time
            }
        }

        // WebSocket handlers
        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);

            if (data.type === 'pools') {
                updatePools(data.pools);
            } else if (data.type === 'alerts') {
                updateAlerts(data.alerts);
            } else if (data.type === 'metrics') {
                updateChart(data.data);
            }
        };

        ws.onerror = (error) => {
            console.error('WebSocket error:', error);
        };

        // Initial load
        fetch('/api/pools').then(r => r.json()).then(data => updatePools(data));
        fetch('/api/alerts').then(r => r.json()).then(data => updateAlerts(data.active));
    </script>
</body>
</html>
"""
        return web.Response(text=html, content_type="text/html")

    async def _handle_metrics(self, request: web.Request) -> web.Response:
        """Get current metrics."""
        metrics = await self._collect_metrics()
        return web.json_response(metrics)

    async def _handle_pools(self, request: web.Request) -> web.Response:
        """Get pool information."""
        pools = await self._get_pool_info()
        return web.json_response(pools)

    async def _handle_alerts(self, request: web.Request) -> web.Response:
        """Get alerts."""
        return web.json_response(
            {
                "active": [
                    {
                        "rule_id": alert.rule_id,
                        "triggered_at": alert.triggered_at,
                        "severity": alert.severity,
                        "message": alert.message,
                        "duration": alert.duration(),
                    }
                    for alert in self._active_alerts.values()
                    if not alert.resolved
                ],
                "rules": [
                    {
                        "id": rule.id,
                        "name": rule.name,
                        "condition": rule.condition,
                        "threshold": rule.threshold,
                        "severity": rule.severity,
                        "enabled": rule.enabled,
                    }
                    for rule in self._alert_rules.values()
                ],
                "history": [
                    {
                        "rule_id": alert.rule_id,
                        "triggered_at": alert.triggered_at,
                        "resolved_at": alert.resolved_at,
                        "severity": alert.severity,
                        "message": alert.message,
                        "duration": alert.duration(),
                    }
                    for alert in list(self._alert_history)[-20:]  # Last 20 alerts
                ],
            }
        )

    async def _handle_create_alert(self, request: web.Request) -> web.Response:
        """Create new alert rule."""
        data = await request.json()

        rule = AlertRule(
            id=f"rule_{len(self._alert_rules)}",
            name=data["name"],
            condition=data["condition"],
            threshold=data["threshold"],
            duration_seconds=data.get("duration_seconds", 60),
            severity=data.get("severity", "warning"),
        )

        self._alert_rules[rule.id] = rule

        return web.json_response({"id": rule.id})

    async def _handle_delete_alert(self, request: web.Request) -> web.Response:
        """Delete alert rule."""
        alert_id = request.match_info["alert_id"]

        if alert_id in self._alert_rules:
            del self._alert_rules[alert_id]
            return web.json_response({"deleted": True})

        return web.json_response({"error": "Alert not found"}, status=404)

    async def _handle_history(self, request: web.Request) -> web.Response:
        """Get metric history."""
        metric_name = request.match_info["metric_name"]
        minutes = int(request.query.get("minutes", 60))

        history = self._cache.get_recent(metric_name, minutes)

        return web.json_response(history)

    async def _handle_websocket(self, request: web.Request) -> web.WebSocketResponse:
        """Handle WebSocket connection."""
        ws = web.WebSocketResponse()
        await ws.prepare(request)

        self._websockets.add(ws)

        try:
            # Send initial data
            pools = await self._get_pool_info()
            await ws.send_json({"type": "pools", "pools": pools})

            # Keep connection alive
            async for msg in ws:
                if msg.type == web.WSMsgType.TEXT:
                    # Handle client messages if needed
                    pass
                elif msg.type == web.WSMsgType.ERROR:
                    logger.error(f"WebSocket error: {ws.exception()}")

        finally:
            self._websockets.discard(ws)

        return ws

    async def _update_loop(self):
        """Periodically update metrics and check alerts."""
        while True:
            try:
                # Collect metrics
                metrics = await self._collect_metrics()
                pools = await self._get_pool_info()

                # Update cache
                for pool_name, pool_data in pools.items():
                    self._cache.add(
                        f"{pool_name}_utilization",
                        {"value": pool_data["utilization"], "pool": pool_name},
                    )

                # Check alerts
                if self.enable_alerts:
                    await self._check_alerts(pools)

                # Broadcast to WebSocket clients
                await self._broadcast({"type": "pools", "pools": pools})

                # Send sample metrics for chart
                if pools:
                    first_pool = next(iter(pools.values()))
                    await self._broadcast(
                        {
                            "type": "metrics",
                            "data": {
                                "timestamp": time.time(),
                                "utilization": first_pool["utilization"],
                            },
                        }
                    )

                # Send active alerts
                active_alerts = [
                    {
                        "rule_id": alert.rule_id,
                        "triggered_at": alert.triggered_at,
                        "severity": alert.severity,
                        "message": alert.message,
                    }
                    for alert in self._active_alerts.values()
                    if not alert.resolved
                ]

                if active_alerts:
                    await self._broadcast({"type": "alerts", "alerts": active_alerts})

            except Exception as e:
                logger.error(f"Error in update loop: {e}")

            await asyncio.sleep(self.update_interval)

    async def _collect_metrics(self) -> Dict[str, Any]:
        """Collect metrics from all pools."""
        # This would integrate with the MetricsAggregator
        # For now, return sample data
        return {"timestamp": time.time(), "pools": await self._get_pool_info()}

    async def _get_pool_info(self) -> Dict[str, Dict[str, Any]]:
        """Get information about all pools."""
        # This would get real data from connection pools
        # For now, return sample data

        # Try to get real pools from resource registry
        pools_info = {}

        if hasattr(self, "runtime") and hasattr(self.runtime, "resource_registry"):  # type: ignore[attr-defined]
            resources = self.runtime.resource_registry.list_resources()  # type: ignore[attr-defined]

            for name, resource in resources.items():
                if hasattr(resource, "get_pool_statistics"):
                    try:
                        stats = await resource.get_pool_statistics()
                        pools_info[name] = {
                            "health_score": stats.get("health_score", 100),
                            "active_connections": stats.get("active_connections", 0),
                            "total_connections": stats.get("total_connections", 0),
                            "utilization": stats.get("utilization", 0.0),
                            "queries_per_second": stats.get("queries_per_second", 0.0),
                            "avg_query_time_ms": stats.get("avg_query_time_ms", 0.0),
                            "error_rate": stats.get("error_rate", 0.0),
                        }
                    except Exception as e:
                        logger.error(f"Error getting stats for pool {name}: {e}")

        # If no real pools, return sample data
        if not pools_info:
            pools_info = {
                "main_pool": {
                    "health_score": 85,
                    "active_connections": 8,
                    "total_connections": 10,
                    "utilization": 0.8,
                    "queries_per_second": 150.5,
                    "avg_query_time_ms": 12.3,
                    "error_rate": 0.002,
                }
            }

        return pools_info

    async def _check_alerts(self, pools: Dict[str, Dict[str, Any]]):
        """Check alert conditions."""
        for rule in self._alert_rules.values():
            if not rule.enabled or rule.is_in_cooldown():
                continue

            # Simple condition evaluation (in production, use proper parser)
            triggered = False
            metric_value = 0.0

            for pool_name, pool_data in pools.items():
                if (
                    "utilization" in rule.condition
                    and pool_data["utilization"] > rule.threshold
                ):
                    triggered = True
                    metric_value = pool_data["utilization"]
                    break
                elif (
                    "error_rate" in rule.condition
                    and pool_data["error_rate"] > rule.threshold
                ):
                    triggered = True
                    metric_value = pool_data["error_rate"]
                    break

            # Check if alert should be triggered
            alert_key = f"{rule.id}_{int(time.time() / rule.duration_seconds)}"

            if triggered:
                if alert_key not in self._active_alerts:
                    alert = Alert(
                        rule_id=rule.id,
                        triggered_at=time.time(),
                        severity=rule.severity,
                        message=f"{rule.name}: {rule.condition} (value: {metric_value:.2f})",
                        metric_value=metric_value,
                    )

                    self._active_alerts[alert_key] = alert
                    self._alert_history.append(alert)
                    rule.last_triggered = time.time()

                    logger.warning(f"Alert triggered: {alert.message}")
            else:
                # Resolve alert if condition no longer met
                if alert_key in self._active_alerts:
                    alert = self._active_alerts[alert_key]
                    alert.resolved = True
                    alert.resolved_at = time.time()

                    logger.info(f"Alert resolved: {rule.name}")

        # Prune resolved alerts older than 1 hour from _active_alerts
        cutoff = time.time() - 3600
        stale_keys = [
            k
            for k, a in self._active_alerts.items()
            if a.resolved and a.resolved_at is not None and a.resolved_at < cutoff
        ]
        for k in stale_keys:
            del self._active_alerts[k]

    async def _broadcast(self, data: Dict[str, Any]):
        """Broadcast data to all WebSocket clients."""
        if not self._websockets:
            return

        # Send to all connected clients
        disconnected = set()

        for ws in self._websockets:
            try:
                await ws.send_json(data)
            except ConnectionResetError:
                disconnected.add(ws)

        # Remove disconnected clients
        self._websockets -= disconnected

    def _init_default_alerts(self):
        """Initialize default alert rules."""
        default_rules = [
            AlertRule(
                id="high_utilization",
                name="High Pool Utilization",
                condition="pool_utilization > 0.9",
                threshold=0.9,
                duration_seconds=60,
                severity="warning",
            ),
            AlertRule(
                id="high_error_rate",
                name="High Error Rate",
                condition="error_rate > 0.05",
                threshold=0.05,
                duration_seconds=30,
                severity="error",
            ),
            AlertRule(
                id="pool_exhausted",
                name="Pool Exhausted",
                condition="pool_utilization >= 1.0",
                threshold=1.0,
                duration_seconds=10,
                severity="critical",
            ),
        ]

        for rule in default_rules:
            self._alert_rules[rule.id] = rule

    def get_status(self) -> Dict[str, Any]:
        """Get dashboard status."""
        return {
            "running": self.app is not None,
            "url": f"http://{self.host}:{self.port}" if self.app else None,
            "websocket_clients": len(self._websockets),
            "active_alerts": len(
                [a for a in self._active_alerts.values() if not a.resolved]
            ),
            "alert_rules": len(self._alert_rules),
            "cached_metrics": len(self._cache._data),
            "update_interval": self.update_interval,
            "retention_hours": self.retention_hours,
        }
