"""Notification router - matches alerts to rules and delivers via channels."""

import asyncio
import json
import logging
import os
import time
from datetime import datetime
from typing import Optional, TYPE_CHECKING

from .channels import create_channel_from_dict, NotificationChannel
from .events import NotificationPayload
from .summarizer import MessageSummarizer

if TYPE_CHECKING:
    from ..connector import MeshConnector

logger = logging.getLogger(__name__)

# Severity levels in order
SEVERITY_ORDER = ["routine", "priority", "immediate"]

# State file for rule statistics
RULE_STATS_FILE = "/data/rule_stats.json"


class NotificationRouter:
    """Routes alerts through matching rules to notification channels."""

    def __init__(
        self,
        config,
        connector: Optional["MeshConnector"] = None,
        llm_backend=None,
        timezone: str = "America/Boise",
    ):
        self._rules: list[dict] = []
        self._timezone = timezone
        self._recent: dict[tuple, float] = {}  # (rule_name, category, event_key) -> last_sent_time
        self._summarizer = MessageSummarizer(llm_backend) if llm_backend else None
        self._llm = llm_backend
        self._connector = connector
        self._config = config

        # Rule statistics: {rule_name: {last_fired, last_test, fire_count}}
        self._rule_stats = self._load_rule_stats()

        # Load rules from config
        rules_config = getattr(config, "rules", [])
        for rule in rules_config:
            if hasattr(rule, "__dict__"):
                rule_dict = {k: v for k, v in rule.__dict__.items() if not k.startswith("_")}
            else:
                rule_dict = dict(rule) if isinstance(rule, dict) else {}

            # Skip disabled rules
            if not rule_dict.get("enabled", True):
                continue

            # Only load condition-triggered rules (scheduled rules handled by scheduler)
            if rule_dict.get("trigger_type", "condition") == "condition":
                self._rules.append(rule_dict)

        logger.info("Notification router initialized: %d condition rules", len(self._rules))

    def _load_rule_stats(self) -> dict:
        """Load rule statistics from persistent storage."""
        try:
            if os.path.exists(RULE_STATS_FILE):
                with open(RULE_STATS_FILE, "r") as f:
                    return json.load(f)
        except Exception as e:
            logger.warning("Failed to load rule stats: %s", e)
        return {}

    def _save_rule_stats(self):
        """Save rule statistics to persistent storage."""
        try:
            os.makedirs(os.path.dirname(RULE_STATS_FILE), exist_ok=True)
            with open(RULE_STATS_FILE, "w") as f:
                json.dump(self._rule_stats, f, indent=2)
        except Exception as e:
            logger.warning("Failed to save rule stats: %s", e)

    def _record_fire(self, rule_name: str):
        """Record that a rule fired."""
        if rule_name not in self._rule_stats:
            self._rule_stats[rule_name] = {"last_fired": None, "last_test": None, "fire_count": 0}
        self._rule_stats[rule_name]["last_fired"] = time.time()
        self._rule_stats[rule_name]["fire_count"] = self._rule_stats[rule_name].get("fire_count", 0) + 1
        self._save_rule_stats()

    def _record_test(self, rule_name: str):
        """Record that a rule was tested."""
        if rule_name not in self._rule_stats:
            self._rule_stats[rule_name] = {"last_fired": None, "last_test": None, "fire_count": 0}
        self._rule_stats[rule_name]["last_test"] = time.time()
        self._save_rule_stats()

    def get_rule_stats(self, rule_name: str) -> dict:
        """Get statistics for a rule."""
        return self._rule_stats.get(rule_name, {"last_fired": None, "last_test": None, "fire_count": 0})

    def _create_channel_for_rule(self, rule: dict) -> Optional[NotificationChannel]:
        """Create a channel instance from a rule's inline delivery config."""
        delivery_type = rule.get("delivery_type", "")

        if not delivery_type:
            return None

        if delivery_type == "mesh_broadcast":
            config = {
                "type": "mesh_broadcast",
                "channel_index": rule.get("broadcast_channel", 0),
            }
        elif delivery_type == "mesh_dm":
            config = {
                "type": "mesh_dm",
                "node_ids": rule.get("node_ids", []),
            }
        elif delivery_type == "email":
            config = {
                "type": "email",
                "smtp_host": rule.get("smtp_host", ""),
                "smtp_port": rule.get("smtp_port", 587),
                "smtp_user": rule.get("smtp_user", ""),
                "smtp_password": rule.get("smtp_password", ""),
                "smtp_tls": rule.get("smtp_tls", True),
                "from_address": rule.get("from_address", ""),
                "recipients": rule.get("recipients", []),
            }
        elif delivery_type == "webhook":
            config = {
                "type": "webhook",
                "url": rule.get("webhook_url", ""),
                "headers": rule.get("webhook_headers", {}),
            }
        else:
            logger.warning("Unknown delivery type '%s' in rule '%s'", delivery_type, rule.get("name"))
            return None

        try:
            return create_channel_from_dict(config, self._connector)
        except Exception as e:
            logger.warning("Failed to create channel for rule '%s': %s", rule.get("name"), e)
            return None

    async def process_alert(self, alert: dict) -> bool:
        """Route an alert through matching rules."""
        category = alert.get("type", "")
        severity = alert.get("severity", "routine")
        delivered = False

        for rule in self._rules:
            rule_name = rule.get("name", "unnamed")

            rule_categories = rule.get("categories", [])
            if rule_categories and category not in rule_categories:
                continue

            min_severity = rule.get("min_severity", "routine")
            if not self._severity_meets(severity, min_severity):
                continue

            cooldown = rule.get("cooldown_minutes", 10) * 60
            event_id = alert.get("event_id", alert.get("message", "")[:50])
            dedup_key = (rule_name, category, event_id)
            now = time.time()
            if dedup_key in self._recent:
                if now - self._recent[dedup_key] < cooldown:
                    continue
            self._recent[dedup_key] = now

            logger.info("Rule '%s' matched alert: %s (%s)", rule_name, category, severity)

            delivery_type = rule.get("delivery_type", "")
            if not delivery_type:
                continue

            channel = self._create_channel_for_rule(rule)
            if not channel:
                continue

            try:
                delivery_alert = alert
                message = alert.get("message", "")
                if channel.channel_type in ("mesh_broadcast", "mesh_dm"):
                    _budget = getattr(self._connector, "max_chars", 200)
                    if len(message) > _budget:
                        if self._summarizer:
                            summary = await self._summarizer.summarize(message, max_chars=_budget - 5)
                            delivery_alert = {**alert, "message": summary}
                        else:
                            delivery_alert = {**alert, "message": message[:_budget - 5] + "..."}

                # Convert dict to NotificationPayload for channel interface
                payload = NotificationPayload(
                    message=delivery_alert.get("message", ""),
                    category=delivery_alert.get("type", "unknown"),
                    severity=delivery_alert.get("severity", "routine"),
                    timestamp=delivery_alert.get("timestamp", time.time()),
                    node_id=delivery_alert.get("node_id"),
                    node_name=delivery_alert.get("node_name"),
                    region=delivery_alert.get("region"),
                    event_type=delivery_alert.get("type"),
                )
                # Rule is a dict here; channels don't use it so we pass None
                # for the rule parameter (channels ignore it anyway)
                success = await channel.deliver(payload, None)
                if success:
                    delivered = True
                    self._record_fire(rule_name)
            except Exception as e:
                logger.warning("Rule '%s' delivery failed: %s", rule_name, e)

        return delivered

    def _severity_meets(self, actual: str, required: str) -> bool:
        """Check if actual severity meets or exceeds required severity."""
        try:
            actual_idx = SEVERITY_ORDER.index(actual.lower())
            required_idx = SEVERITY_ORDER.index(required.lower())
            return actual_idx >= required_idx
        except ValueError:
            return True

    def get_rules(self) -> list[dict]:
        """Get list of configured rules with stats."""
        rules_with_stats = []
        for rule in self._rules:
            rule_copy = dict(rule)
            stats = self.get_rule_stats(rule.get("name", ""))
            rule_copy["_stats"] = stats
            rules_with_stats.append(rule_copy)
        return rules_with_stats

    async def test_channel(self, channel_config: dict) -> dict:
        """Test a channel's connectivity.

        Args:
            channel_config: Channel configuration dict with type and settings

        Returns:
            {success, message, error, details}
        """
        try:
            channel = create_channel_from_dict(channel_config, self._connector)
            return await channel.test_connection()
        except ValueError as e:
            return {
                "success": False,
                "message": "Invalid channel configuration",
                "error": str(e),
                "details": {}
            }
        except Exception as e:
            return {
                "success": False,
                "message": "Channel test failed",
                "error": str(e),
                "details": {}
            }

    def get_source_health(self, rule_categories: list, env_store=None) -> dict:
        """Get health status of data sources for a rule's categories.

        Returns:
            {
                category_id: {
                    "enabled": bool,
                    "active_events": int,
                    "source": str,
                    "status": "ok" | "disabled" | "no_data"
                }
            }
        """
        # Map categories to their data sources
        category_sources = {
            "hf_blackout": "swpc",
            "geomagnetic_storm": "swpc",
            "tropospheric_ducting": "ducting",
            "weather_warning": "nws",
            # v0.5.7-fire: fire_proximity / wildfire_proximity removed.
            "wildfire_incident": "nifc",
            "wildfire_hotspot": "firms",
            "new_ignition": "firms",
            "stream_flood_warning": "usgs",
            "stream_high_water": "usgs",
            "road_closure": "roads511",
            "traffic_congestion": "traffic",
            "avalanche_warning": "avalanche",
            "avalanche_considerable": "avalanche",
            "infra_offline": "health",
            "critical_node_down": "health",
            "battery_warning": "health",
            "battery_critical": "health",
            "battery_emergency": "health",
            "mesh_score_low": "health",
            "high_utilization": "health",
            "infra_recovery": "health",
            "packet_flood": "health",
        }

        result = {}

        for cat_id in rule_categories:
            source = category_sources.get(cat_id, "unknown")

            if source == "health":
                # Mesh health is always available
                result[cat_id] = {
                    "enabled": True,
                    "active_events": 0,  # Would need health_engine to check
                    "source": "mesh_health",
                    "status": "ok"
                }
            elif env_store is None:
                result[cat_id] = {
                    "enabled": False,
                    "active_events": 0,
                    "source": source,
                    "status": "disabled"
                }
            else:
                # Check if source has an adapter
                adapters = getattr(env_store, '_adapters', {})
                if source in adapters:
                    events = env_store.get_active(source=source)
                    result[cat_id] = {
                        "enabled": True,
                        "active_events": len(events) if events else 0,
                        "source": source,
                        "status": "ok"
                    }
                else:
                    result[cat_id] = {
                        "enabled": False,
                        "active_events": 0,
                        "source": source,
                        "status": "disabled"
                    }

        return result

    async def test_rule_with_conditions(
        self,
        rule_index: int,
        alert_engine=None,
        env_store=None,
        health_engine=None,
        send: bool = False,
        action: str = "preview",
    ) -> dict:
        """Test a rule against current conditions with live data.

        Args:
            rule_index: Index of the rule to test
            alert_engine: AlertEngine instance for pending alerts
            env_store: EnvStore instance for environmental events
            health_engine: MeshHealthEngine for mesh status
            send: Legacy param - use action instead
            action: "preview", "send_test", "send_status", "send_live"
        """
        from .categories import get_category

        rules_config = getattr(self._config, "rules", [])
        if rule_index < 0 or rule_index >= len(rules_config):
            return {
                "conditions_matched": 0,
                "preview_messages": [],
                "is_example": False,
                "delivered": False,
                "delivery_method": "",
                "delivery_result": "Rule index out of range",
            }

        rule = rules_config[rule_index]
        if hasattr(rule, "__dict__"):
            rule_dict = {k: v for k, v in rule.__dict__.items() if not k.startswith("_")}
        else:
            rule_dict = dict(rule)

        rule_name = rule_dict.get("name", f"Rule {rule_index}")
        rule_categories = rule_dict.get("categories", [])
        min_severity = rule_dict.get("min_severity", "routine")
        delivery_type = rule_dict.get("delivery_type", "")

        # Legacy support
        if send and action == "preview":
            action = "send_test"

        # ============================================================
        # SECTION 1: Collect LIVE DATA for rule's categories
        # ============================================================
        live_data_lines = []
        feeds_not_enabled = []
        category_sources = {
            "hf_blackout": "swpc", "geomagnetic_storm": "swpc",
            "tropospheric_ducting": "ducting",
            "weather_warning": "nws",
            "wildfire_incident": "nifc", "wildfire_hotspot": "firms", "new_ignition": "firms",  # v0.5.7-fire
            "stream_flood_warning": "usgs", "stream_high_water": "usgs",
            "road_closure": "roads511", "traffic_congestion": "traffic",
            "avalanche_warning": "avalanche", "avalanche_considerable": "avalanche",
            "infra_offline": "health", "critical_node_down": "health",
            "battery_warning": "health", "battery_critical": "health",
            "mesh_score_low": "health", "high_utilization": "health",
        }

        sources_needed = set()
        for cat in rule_categories if rule_categories else []:
            if cat in category_sources:
                sources_needed.add(category_sources[cat])

        # Check which sources are available
        if env_store:
            adapters = getattr(env_store, '_adapters', {})

            if "swpc" in sources_needed:
                if "swpc" in adapters and hasattr(env_store, 'get_swpc_status'):
                    swpc = env_store.get_swpc_status()
                    if swpc:
                        kp = swpc.get("kp_current", "?")
                        sfi = swpc.get("sfi", "?")
                        r = swpc.get("r_scale", 0)
                        s = swpc.get("s_scale", 0)
                        g = swpc.get("g_scale", 0)
                        live_data_lines.append(f"RF: SFI {sfi}, Kp {kp}, R{r}/S{s}/G{g}")
                else:
                    feeds_not_enabled.append("SWPC")

            if "ducting" in sources_needed:
                if "ducting" in adapters and hasattr(env_store, 'get_ducting_status'):
                    ducting = env_store.get_ducting_status()
                    if ducting:
                        condition = ducting.get("condition", "unknown")
                        gradient = ducting.get("min_gradient", "?")
                        live_data_lines.append(f"Tropo: {condition}, dM/dz {gradient}")
                else:
                    feeds_not_enabled.append("Ducting")

            if "nws" in sources_needed:
                if "nws" in adapters:
                    nws = env_store.get_active(source="nws")
                    if nws:
                        live_data_lines.append(f"NWS: {len(nws)} active alert(s)")
                        for a in nws[:2]:
                            headline = a.get('headline', a.get('message', 'Alert'))[:60]
                            live_data_lines.append(f"  - {headline}")
                    else:
                        live_data_lines.append("NWS: No active alerts")
                else:
                    feeds_not_enabled.append("NWS")

            if "nifc" in sources_needed:
                if "nifc" in adapters:
                    fires = env_store.get_active(source="nifc")
                    if fires:
                        live_data_lines.append(f"Fires: {len(fires)} active")
                    else:
                        live_data_lines.append("Fires: None active")
                else:
                    feeds_not_enabled.append("NIFC")

            if "firms" in sources_needed:
                if "firms" in adapters:
                    hotspots = env_store.get_active(source="firms")
                    if hotspots:
                        live_data_lines.append(f"Hotspots: {len(hotspots)} detected")
                    else:
                        live_data_lines.append("Hotspots: None detected")
                else:
                    feeds_not_enabled.append("FIRMS")

            if "usgs" in sources_needed:
                if "usgs" in adapters:
                    streams = env_store.get_active(source="usgs")
                    if streams:
                        live_data_lines.append(f"Streams: {len(streams)} gauge(s) reporting")
                    else:
                        live_data_lines.append("Streams: No alerts")
                else:
                    feeds_not_enabled.append("USGS")

            if "traffic" in sources_needed:
                if "traffic" in adapters:
                    traffic = env_store.get_active(source="traffic")
                    if traffic:
                        live_data_lines.append(f"Traffic: {len(traffic)} corridor(s)")
                    else:
                        live_data_lines.append("Traffic: Normal")
                else:
                    feeds_not_enabled.append("Traffic")

            if "roads511" in sources_needed:
                if "roads511" in adapters:
                    roads = env_store.get_active(source="roads511")
                    if roads:
                        live_data_lines.append(f"Roads: {len(roads)} event(s)")
                    else:
                        live_data_lines.append("Roads: No closures")
                else:
                    feeds_not_enabled.append("511 Roads")
        elif sources_needed - {"health"}:
            feeds_not_enabled.append("Environmental feeds")

        if health_engine and "health" in sources_needed:
            mesh_health = getattr(health_engine, 'mesh_health', None)
            if mesh_health:
                score = mesh_health.score
                live_data_lines.append(f"Mesh: {score.composite:.0f}/100, {score.infra_online}/{score.infra_total} infra")

        # Add warning if feeds not enabled
        if feeds_not_enabled:
            live_data_lines.append(f"[!] Not enabled: {', '.join(feeds_not_enabled)}")

        # ============================================================
        # SECTION 2: Check for MATCHING and NEAR-MISS events
        # ============================================================
        matching_alerts = []
        below_threshold = []
        all_events = []

        if alert_engine and hasattr(alert_engine, "get_pending_alerts"):
            try:
                for alert in alert_engine.get_pending_alerts():
                    all_events.append({
                        "type": alert.get("type", ""),
                        "severity": alert.get("severity", "routine"),
                        "message": alert.get("message", ""),
                        "headline": alert.get("message", "")[:80],
                    })
            except Exception:
                pass

        if env_store and hasattr(env_store, "get_active"):
            try:
                for event in env_store.get_active():
                    all_events.append({
                        "type": event.get("type", event.get("category", "")),
                        "severity": event.get("severity", "routine"),
                        "message": event.get("message", event.get("headline", str(event))),
                        "headline": event.get("headline", event.get("message", "Event"))[:80],
                    })
            except Exception:
                pass

        for event in all_events:
            event_type = event["type"]
            severity = event["severity"]

            category_match = not rule_categories
            if not category_match:
                for cat in rule_categories:
                    if event_type.startswith(cat.rstrip("_")) or cat in event_type or event_type == cat:
                        category_match = True
                        break

            if category_match:
                if self._severity_meets(severity, min_severity):
                    matching_alerts.append(event)
                else:
                    below_threshold.append(event)

        # ============================================================
        # SECTION 3: Build response
        # ============================================================
        preview_messages = []
        is_example = False
        below_threshold_summary = ""
        below_threshold_events = []
        suggestion = ""

        if matching_alerts:
            for alert in matching_alerts[:5]:
                msg = alert.get("message", "")
                if len(msg) > 200 and delivery_type in ("mesh_broadcast", "mesh_dm"):
                    msg = msg[:195] + "..."
                preview_messages.append(msg)
        else:
            is_example = True
            if below_threshold:
                severity_counts = {}
                for evt in below_threshold:
                    sev = evt["severity"]
                    severity_counts[sev] = severity_counts.get(sev, 0) + 1
                parts = [f"{count} at '{sev}'" for sev, count in severity_counts.items()]
                below_threshold_summary = f"{len(below_threshold)} event(s) filtered by severity: {', '.join(parts)}. Rule requires '{min_severity}' or higher."
                suggestion = f"Lower severity threshold to '{list(severity_counts.keys())[0]}' to match these events"
                below_threshold_events = [{"headline": e["headline"], "severity": e["severity"]} for e in below_threshold[:5]]

            if rule_categories:
                for cat_id in rule_categories[:3]:
                    cat_info = get_category(cat_id)
                    preview_messages.append(f"[EXAMPLE] {cat_info.get('example_message', f'Alert: {cat_id}')}")
            else:
                cat_info = get_category("infra_offline")
                preview_messages.append(f"[EXAMPLE] {cat_info.get('example_message', 'Alert notification')}")

        # Get source health
        source_health = self.get_source_health(rule_categories, env_store)

        # ============================================================
        # SECTION 4: Handle delivery actions
        # ============================================================
        delivered = False
        delivery_result = "Preview only"
        delivery_error = ""

        if action != "preview":
            if not delivery_type:
                delivery_result = "No delivery method configured"
                delivery_error = "Configure a delivery method to send test messages"
            else:
                channel = self._create_channel_for_rule(rule_dict)
                if channel:
                    try:
                        if action == "send_status":
                            # Determine report type from rule categories
                            report_type = "all"
                            if rule_categories:
                                if any(c in rule_categories for c in ["hf_blackout", "geomagnetic_storm", "tropospheric_ducting"]):
                                    report_type = "rf_propagation"
                                elif any(c in rule_categories for c in ["infra_offline", "critical_node_down", "mesh_score_low", "battery_warning"]):
                                    report_type = "mesh_health"
                                elif any(c in rule_categories for c in ["weather_warning", "wildfire_incident", "wildfire_hotspot", "new_ignition"]):  # v0.5.7-fire
                                    report_type = "weather_fire"

                            status_msg = await self.generate_report(report_type, env_store, health_engine)
                            if len(status_msg) > 195:
                                status_msg = status_msg[:192] + "..."
                            success, result = await channel.deliver_test(status_msg)
                            delivered = success
                            delivery_result = result if success else f"Failed: {result}"
                            if not success:
                                delivery_error = result

                        elif action == "send_live" and matching_alerts:
                            live_msg = matching_alerts[0].get('message', '')
                            if len(live_msg) > 195:
                                live_msg = live_msg[:192] + "..."
                            success, result = await channel.deliver_test(live_msg)
                            delivered = success
                            delivery_result = result if success else f"Failed: {result}"
                            if not success:
                                delivery_error = result

                        elif action == "send_test":
                            if preview_messages:
                                test_msg = preview_messages[0]
                                if test_msg.startswith("[EXAMPLE]"):
                                    test_msg = test_msg.replace("[EXAMPLE]", "[TEST]")
                                elif not test_msg.startswith("["):
                                    test_msg = f"[TEST] {test_msg}"
                            else:
                                test_msg = "[TEST] MeshAI notification test"
                            success, result = await channel.deliver_test(test_msg)
                            delivered = success
                            delivery_result = result if success else f"Failed: {result}"
                            if not success:
                                delivery_error = result

                        # Record test
                        if action != "preview":
                            self._record_test(rule_name)

                    except Exception as e:
                        delivery_result = f"Delivery error"
                        delivery_error = str(e)

        # Get rule stats
        stats = self.get_rule_stats(rule_name)

        return {
            "live_data_summary": live_data_lines,
            "conditions_matched": len(matching_alerts),
            "preview_messages": preview_messages,
            "is_example": is_example,
            "conditions_below_threshold": len(below_threshold),
            "below_threshold_summary": below_threshold_summary,
            "below_threshold_events": below_threshold_events,
            "suggestion": suggestion,
            "delivered": delivered,
            "delivery_method": delivery_type,
            "delivery_result": delivery_result,
            "delivery_error": delivery_error,
            "can_send_live": len(matching_alerts) > 0,
            "source_health": source_health,
            "rule_stats": stats,
        }

    async def test_rule(self, rule_index: int) -> tuple[bool, str]:
        """Send a test alert through a specific rule (legacy method)."""
        result = await self.test_rule_with_conditions(rule_index, action="send_test")
        return result.get("delivered", False), result.get("delivery_result", "Unknown")

    async def preview_rule(self, rule_index: int) -> dict:
        """Preview what a rule would match right now."""
        rules_config = getattr(self._config, "rules", [])
        if rule_index < 0 or rule_index >= len(rules_config):
            return {"matches": False, "conditions": [], "preview": "Invalid rule index"}

        rule = rules_config[rule_index]
        if hasattr(rule, "__dict__"):
            rule_dict = {k: v for k, v in rule.__dict__.items() if not k.startswith("_")}
        else:
            rule_dict = dict(rule)

        if rule_dict.get("trigger_type", "condition") == "condition":
            from .categories import get_category
            categories = rule_dict.get("categories", [])

            if not categories:
                example = get_category("infra_offline")
                return {
                    "matches": True,
                    "conditions": ["All alert categories"],
                    "preview": example.get("example_message", "Alert notification"),
                }
            else:
                cat_info = get_category(categories[0])
                return {
                    "matches": True,
                    "conditions": [get_category(c)["name"] for c in categories],
                    "preview": cat_info.get("example_message", f"Alert: {categories[0]}"),
                }

        elif rule_dict.get("trigger_type") == "schedule":
            message_type = rule_dict.get("message_type", "mesh_health_summary")
            return {
                "matches": True,
                "conditions": [f"Scheduled: {rule_dict.get('schedule_frequency', 'daily')}"],
                "preview": f"[{message_type}] Report content would appear here",
            }

        return {"matches": False, "conditions": [], "preview": "Unknown rule type"}

    async def generate_report(self, report_type: str, env_store, health_engine) -> str:
        """Generate an LLM-summarized report from current data."""
        context_parts = []

        # For RF propagation, use deterministic formatter
        swpc_data = None
        ducting_data = None

        if report_type in ("rf_propagation", "all"):
            if env_store:
                adapters = getattr(env_store, '_adapters', {})
                if "swpc" in adapters and hasattr(env_store, 'get_swpc_status'):
                    swpc_data = env_store.get_swpc_status()
                if "ducting" in adapters and hasattr(env_store, 'get_ducting_status'):
                    ducting_data = env_store.get_ducting_status()

                # If this is an RF-only report, return deterministic format immediately
                if report_type == "rf_propagation" and swpc_data:
                    sfi = swpc_data.get('sfi', 100)
                    kp = swpc_data.get('kp_current', 3)
                    try:
                        band_conditions = self._compute_band_conditions(float(sfi), float(kp))
                        return self._format_propagation_report(swpc_data, ducting_data, band_conditions)
                    except Exception as e:
                        logger.warning("Band condition calc failed: %s", e)
                        # Fall through to context-based approach

                # For "all" report type, add RF data to context
                if swpc_data:
                    context_parts.append(
                        f"Solar/Geomagnetic: SFI {swpc_data.get('sfi')}, "
                        f"Kp {swpc_data.get('kp_current')}, "
                        f"R{swpc_data.get('r_scale', 0)}/S{swpc_data.get('s_scale', 0)}/G{swpc_data.get('g_scale', 0)}"
                    )
                if ducting_data:
                    context_parts.append(
                        f"Tropospheric: {ducting_data.get('condition', 'unknown')}, "
                        f"dM/dz {ducting_data.get('min_gradient', 'N/A')} M-units/km"
                    )

        if report_type in ("mesh_health", "all"):
            if health_engine:
                health = getattr(health_engine, 'mesh_health', None)
                if health and hasattr(health, 'score'):
                    score = health.score
                    context_parts.append(
                        f"Mesh: score {score.composite:.0f}/100, "
                        f"tier {score.tier}, "
                        f"{score.infra_online}/{score.infra_total} infra online, "
                        f"utilization {score.util_percent:.1f}%"
                    )

        if report_type in ("weather_fire", "all"):
            if env_store and hasattr(env_store, 'get_active'):
                nws = env_store.get_active(source="nws")
                fires = env_store.get_active(source="nifc")
                if nws:
                    headlines = [e.get("headline", "")[:80] for e in nws[:3]]
                    context_parts.append(f"Weather: {len(nws)} active alerts: {'; '.join(headlines)}")
                else:
                    context_parts.append("Weather: No active alerts")
                if fires:
                    context_parts.append(f"Fires: {len(fires)} active")
                else:
                    context_parts.append("Fires: None active")

        if report_type in ("environmental", "all"):
            if env_store and hasattr(env_store, 'get_active'):
                for source in ["usgs", "traffic", "roads511", "avalanche"]:
                    events = env_store.get_active(source=source)
                    if events:
                        context_parts.append(f"{source.upper()}: {len(events)} events")

        if not context_parts:
            # Return a graceful message for the specific report type
            no_data_messages = {
                "rf_propagation": "RF propagation data not available",
                "mesh_health": "Mesh health data not available",
                "weather_fire": "Weather/fire monitoring not configured",
                "environmental": "Environmental monitoring not configured",
                "all": "No monitoring data available",
            }
            return no_data_messages.get(report_type, "No data available")

        raw_data = "\n".join(context_parts)

        # Generate LLM summary
        if self._llm:
            prompt = self._build_report_prompt(report_type, raw_data)
            try:
                messages = [{"role": "user", "content": prompt}]
                summary = await self._llm.generate(
                    messages=messages,
                    system_prompt="You are a concise infrastructure status reporter. Format data clearly and briefly. Output only the formatted report, no preamble or explanation.",
                    
                )
                return summary.strip()
            except Exception as e:
                logger.warning("LLM report generation failed: %s", e)
                return raw_data
        else:
            return raw_data

    def _compute_band_conditions(self, sfi: float, kp: float) -> dict:
        """Deterministic band conditions from SFI and Kp."""
        from datetime import datetime, timezone
        hour_utc = datetime.now(timezone.utc).hour
        is_day = 12 <= hour_utc or hour_utc <= 3  # rough UTC daytime for US

        bands = {}

        # 10m (28 MHz) - needs high SFI, daytime only
        if sfi > 140 and kp <= 2:
            bands["10m"] = "Good"
        elif sfi > 100 and kp <= 4 and is_day:
            bands["10m"] = "Fair"
        else:
            bands["10m"] = "Poor"

        # 12m (24 MHz)
        if sfi > 120 and kp <= 3:
            bands["12m"] = "Good"
        elif sfi > 90 and kp <= 4 and is_day:
            bands["12m"] = "Fair"
        else:
            bands["12m"] = "Poor"

        # 15m (21 MHz)
        if sfi > 100 and kp <= 3:
            bands["15m"] = "Good"
        elif sfi > 80 and kp <= 5:
            bands["15m"] = "Fair"
        else:
            bands["15m"] = "Poor"

        # 17m (18 MHz)
        if sfi > 90 and kp <= 3:
            bands["17m"] = "Good"
        elif sfi > 70 and kp <= 5:
            bands["17m"] = "Fair"
        else:
            bands["17m"] = "Poor"

        # 20m (14 MHz) - almost always usable
        if sfi > 80 and kp <= 3:
            bands["20m"] = "Good"
        elif kp <= 6:
            bands["20m"] = "Fair"
        else:
            bands["20m"] = "Poor"

        # 30m (10 MHz) - very reliable
        if kp <= 4:
            bands["30m"] = "Good"
        elif kp <= 6:
            bands["30m"] = "Fair"
        else:
            bands["30m"] = "Poor"

        # 40m (7 MHz) - reliable, better at night
        if kp <= 4:
            bands["40m"] = "Good"
        elif kp <= 6:
            bands["40m"] = "Fair"
        else:
            bands["40m"] = "Poor"

        # 80m (3.5 MHz) - night band
        if kp <= 3:
            bands["80m"] = "Good"
        elif kp <= 5:
            bands["80m"] = "Fair"
        else:
            bands["80m"] = "Poor"

        return bands

    def _format_propagation_report(self, swpc: dict, ducting: dict, band_conditions: dict) -> str:
        """Format propagation report deterministically."""
        sfi = swpc.get('sfi', 'N/A')
        kp = swpc.get('kp_current', 'N/A')

        lines = [f"Band Conditions (SFI {sfi}, Kp {kp}):"]

        for band_list, label in [
            (["80m", "40m"], "80-40m"),
            (["30m", "20m"], "30-20m"),
            (["17m", "15m"], "17-15m"),
            (["12m", "10m"], "12-10m"),
        ]:
            # Take the better of the two bands in range
            ratings = [band_conditions.get(b, "Poor") for b in band_list]
            if "Good" in ratings:
                rating = "Good"
            elif "Fair" in ratings:
                rating = "Fair"
            else:
                rating = "Poor"
            lines.append(f"{label}: {rating}")

        if ducting and ducting.get("condition") and ducting.get("condition") != "normal":
            cond = ducting["condition"].replace("_", " ").title()
            lines.append(f"Tropo: {cond}")
        else:
            lines.append("Tropo: Normal")

        return "\n".join(lines)

    def _build_report_prompt(self, report_type: str, raw_data: str) -> str:
        """Build the LLM prompt for report generation."""
        prompts = {
            "rf_propagation": (
                "Format this propagation data as a band-by-band HF report. "
                "Output format:\n"
                "Band Conditions (SFI X, Kp Y):\n"
                "80-40m: [Good/Fair/Poor]\n"
                "30-20m: [Good/Fair/Poor]\n"
                "17-15m: [Good/Fair/Poor]\n"
                "12-10m: [Good/Fair/Poor]\n"
                "Tropo: [Normal/Enhanced/Ducting]\n\n"
                "Determine ratings from SFI and Kp values. "
                "Output ONLY the formatted report.\n\n"
                f"Data:\n{raw_data}"
            ),
            "mesh_health": (
                "Format this mesh network health data as a brief status. "
                "Include: score out of 100, tier name, infrastructure count, "
                "and any problems. If healthy, say 'Mesh healthy' with key stats. "
                "Example: 'Mesh healthy: 90/100, 16/16 infra online, 20% util'\n"
                "Keep it concise. Output ONLY the status line.\n\n"
                f"Data:\n{raw_data}"
            ),
            "weather_fire": (
                "Format these weather/fire alerts as a brief summary. "
                "List count of alerts and most severe conditions. "
                "Example: '3 weather alerts: Winter Storm Warning (ID), "
                "Wind Advisory (OR). No active fires.'\n"
                "Keep it concise. Output ONLY the summary.\n\n"
                f"Data:\n{raw_data}"
            ),
            "environmental": (
                "Summarize all environmental conditions briefly. "
                "Cover weather alerts, fires, streams, roads. "
                "Example: 'Weather clear, no fires, 2 streams elevated, "
                "roads open.'\n"
                "Keep it concise. Output ONLY the summary.\n\n"
                f"Data:\n{raw_data}"
            ),
            "all": (
                "Summarize all conditions for a mesh network operator. "
                "Cover: mesh health score, any infrastructure issues, "
                "weather alerts, fire status, propagation conditions. "
                "Be brief but complete.\n\n"
                f"Data:\n{raw_data}"
            ),
        }
        return prompts.get(report_type, prompts["all"])

    def cleanup_recent(self, max_age: int = 3600):
        """Clean up old entries from recent alerts cache."""
        now = time.time()
        self._recent = {k: v for k, v in self._recent.items() if now - v < max_age}
