"""Notification System for MCP Security Audit.

Provides notification infrastructure for scan results:
- Configurable notification channels with severity thresholds
- Event formatting in JSON, Markdown, and plain text
- Notification batching by time window and severity
- Deduplication via content fingerprinting
- Quiet hours support
- Notification history tracking

CLI: mcpaudit notify --config notify.json --format markdown

Zero external dependencies. Part of mcpaudit v2.1.0.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_SEVERITY_ORDER: dict[str, int] = {
    "CRITICAL": 0,
    "HIGH": 1,
    "MEDIUM": 2,
    "LOW": 3,
    "INFO": 4,
}

_VALID_SEVERITIES = frozenset(_SEVERITY_ORDER.keys())

_VALID_FORMATS = frozenset({"json", "markdown", "text"})


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass
class NotificationConfig:
    """Configuration for notification delivery."""

    webhook_url: str = ""
    severity_threshold: str = "MEDIUM"
    channels: list[str] = field(default_factory=list)
    quiet_hours_start: int = -1  # -1 means disabled
    quiet_hours_end: int = -1
    batch_window_seconds: int = 300
    enabled: bool = True

    def to_dict(self) -> dict[str, Any]:
        return {
            "webhook_url": self.webhook_url,
            "severity_threshold": self.severity_threshold,
            "channels": list(self.channels),
            "quiet_hours_start": self.quiet_hours_start,
            "quiet_hours_end": self.quiet_hours_end,
            "batch_window_seconds": self.batch_window_seconds,
            "enabled": self.enabled,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> NotificationConfig:
        return cls(
            webhook_url=data.get("webhook_url", ""),
            severity_threshold=data.get(
                "severity_threshold", "MEDIUM"
            ),
            channels=list(data.get("channels", [])),
            quiet_hours_start=data.get("quiet_hours_start", -1),
            quiet_hours_end=data.get("quiet_hours_end", -1),
            batch_window_seconds=data.get("batch_window_seconds", 300),
            enabled=data.get("enabled", True),
        )

    def validate(self) -> list[str]:
        """Validate the notification config."""
        errors: list[str] = []
        sev = self.severity_threshold.upper()
        if sev not in _VALID_SEVERITIES:
            errors.append(
                f"Invalid severity_threshold: '{self.severity_threshold}'"
            )
        if self.quiet_hours_start != -1:
            if not (0 <= self.quiet_hours_start <= 23):
                errors.append(
                    "quiet_hours_start must be 0-23 or -1 (disabled)"
                )
        if self.quiet_hours_end != -1:
            if not (0 <= self.quiet_hours_end <= 23):
                errors.append(
                    "quiet_hours_end must be 0-23 or -1 (disabled)"
                )
        if self.batch_window_seconds < 0:
            errors.append("batch_window_seconds must be >= 0")
        return errors


@dataclass
class NotificationEvent:
    """A notification event for a security finding."""

    event_id: str = ""
    rule_id: str = ""
    severity: str = "INFO"
    title: str = ""
    file: str = ""
    line: int = 0
    description: str = ""
    timestamp: str = ""

    def __post_init__(self) -> None:
        if not self.timestamp:
            self.timestamp = datetime.now(
                timezone.utc
            ).strftime("%Y-%m-%dT%H:%M:%SZ")

    def fingerprint(self) -> str:
        """Generate a content fingerprint for dedup."""
        content = f"{self.rule_id}:{self.severity}:{self.file}:{self.line}"
        return hashlib.sha256(
            content.encode("utf-8")
        ).hexdigest()[:16]

    def to_dict(self) -> dict[str, Any]:
        return {
            "event_id": self.event_id,
            "rule_id": self.rule_id,
            "severity": self.severity,
            "title": self.title,
            "file": self.file,
            "line": self.line,
            "description": self.description,
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> NotificationEvent:
        return cls(
            event_id=data.get("event_id", ""),
            rule_id=data.get("rule_id", ""),
            severity=data.get("severity", "INFO"),
            title=data.get("title", ""),
            file=data.get("file", ""),
            line=data.get("line", 0),
            description=data.get("description", ""),
            timestamp=data.get("timestamp", ""),
        )

    def meets_threshold(self, threshold: str) -> bool:
        """Check if this event meets the severity threshold."""
        event_order = _SEVERITY_ORDER.get(self.severity, 99)
        threshold_order = _SEVERITY_ORDER.get(threshold, 99)
        return event_order <= threshold_order


# ---------------------------------------------------------------------------
# Formatter
# ---------------------------------------------------------------------------

class NotificationFormatter:
    """Format notification events in various output formats."""

    @staticmethod
    def format_json(events: list[NotificationEvent]) -> str:
        """Format events as JSON."""
        data = {
            "notifications": [e.to_dict() for e in events],
            "count": len(events),
        }
        return json.dumps(data, indent=2, ensure_ascii=False)

    @staticmethod
    def format_markdown(events: list[NotificationEvent]) -> str:
        """Format events as Markdown."""
        if not events:
            return "No notifications.\n"

        lines = [f"## Security Notifications ({len(events)})"]
        lines.append("")
        for event in events:
            severity_icon = {
                "CRITICAL": "!!!",
                "HIGH": "!!",
                "MEDIUM": "!",
                "LOW": "-",
                "INFO": "i",
            }.get(event.severity, "?")

            lines.append(
                f"- **[{event.severity}]** {severity_icon} "
                f"{event.title}"
            )
            if event.file:
                lines.append(f"  - File: `{event.file}:{event.line}`")
            if event.rule_id:
                lines.append(f"  - Rule: `{event.rule_id}`")
            if event.description:
                lines.append(f"  - {event.description}")
            lines.append("")

        return "\n".join(lines)

    @staticmethod
    def format_text(events: list[NotificationEvent]) -> str:
        """Format events as plain text."""
        if not events:
            return "No notifications.\n"

        lines = [f"Security Notifications ({len(events)})"]
        lines.append("=" * 40)
        for event in events:
            lines.append(
                f"[{event.severity}] {event.title}"
            )
            if event.file:
                lines.append(f"  File: {event.file}:{event.line}")
            if event.rule_id:
                lines.append(f"  Rule: {event.rule_id}")
            lines.append("")

        return "\n".join(lines)

    @staticmethod
    def format(
        events: list[NotificationEvent],
        fmt: str = "text",
    ) -> str:
        """Format events in the specified format.

        Args:
            events: List of events to format.
            fmt: Output format (json, markdown, text).

        Returns:
            Formatted string.

        Raises:
            ValueError: If format is invalid.
        """
        if fmt not in _VALID_FORMATS:
            raise ValueError(
                f"Invalid format: '{fmt}'. "
                f"Valid: {', '.join(sorted(_VALID_FORMATS))}"
            )
        if fmt == "json":
            return NotificationFormatter.format_json(events)
        elif fmt == "markdown":
            return NotificationFormatter.format_markdown(events)
        else:
            return NotificationFormatter.format_text(events)


# ---------------------------------------------------------------------------
# Batcher
# ---------------------------------------------------------------------------

class NotificationBatcher:
    """Batch notifications by time window and severity."""

    def __init__(
        self,
        window_seconds: int = 300,
        severity_threshold: str = "INFO",
    ) -> None:
        self._window = timedelta(seconds=window_seconds)
        self._threshold = severity_threshold
        self._pending: list[NotificationEvent] = []
        self._batch_start: datetime | None = None

    @property
    def pending_count(self) -> int:
        return len(self._pending)

    def add(self, event: NotificationEvent) -> None:
        """Add an event to the batch."""
        if not event.meets_threshold(self._threshold):
            return
        if self._batch_start is None:
            self._batch_start = datetime.now(timezone.utc)
        self._pending.append(event)

    def is_ready(self, now: datetime | None = None) -> bool:
        """Check if the batch window has elapsed."""
        if not self._pending:
            return False
        if self._batch_start is None:
            return False
        current = now or datetime.now(timezone.utc)
        return (current - self._batch_start) >= self._window

    def flush(self) -> list[NotificationEvent]:
        """Return and clear pending events."""
        events = list(self._pending)
        self._pending.clear()
        self._batch_start = None
        return events

    def clear(self) -> None:
        """Discard all pending events."""
        self._pending.clear()
        self._batch_start = None


# ---------------------------------------------------------------------------
# History (dedup)
# ---------------------------------------------------------------------------

class NotificationHistory:
    """Track sent notifications with deduplication."""

    def __init__(self, max_entries: int = 10000) -> None:
        self._sent: list[dict[str, Any]] = []
        self._fingerprints: set[str] = set()
        self._max_entries = max_entries

    @property
    def count(self) -> int:
        return len(self._sent)

    def record(self, event: NotificationEvent) -> None:
        """Record a sent notification."""
        fp = event.fingerprint()
        self._fingerprints.add(fp)
        self._sent.append({
            "fingerprint": fp,
            "event": event.to_dict(),
            "sent_at": datetime.now(
                timezone.utc
            ).strftime("%Y-%m-%dT%H:%M:%SZ"),
        })
        # Evict oldest if over limit
        if len(self._sent) > self._max_entries:
            removed = self._sent.pop(0)
            # Don't remove fingerprint - it may still match
            # newer entries

    def is_duplicate(self, event: NotificationEvent) -> bool:
        """Check if an event has already been sent."""
        return event.fingerprint() in self._fingerprints

    def clear(self) -> None:
        """Clear all history."""
        self._sent.clear()
        self._fingerprints.clear()

    def to_dict(self) -> dict[str, Any]:
        return {
            "sent": list(self._sent),
            "total": self.count,
        }


# ---------------------------------------------------------------------------
# Quiet hours check
# ---------------------------------------------------------------------------

def is_quiet_hours(
    config: NotificationConfig,
    now: datetime | None = None,
) -> bool:
    """Check if current time is within quiet hours.

    Args:
        config: Notification config.
        now: Current time (defaults to UTC now).

    Returns:
        True if in quiet hours.
    """
    if config.quiet_hours_start == -1 or config.quiet_hours_end == -1:
        return False

    current = now or datetime.now(timezone.utc)
    hour = current.hour

    start = config.quiet_hours_start
    end = config.quiet_hours_end

    if start <= end:
        return start <= hour < end
    else:
        # Wraps midnight (e.g., 22-6)
        return hour >= start or hour < end


# ---------------------------------------------------------------------------
# Filter events
# ---------------------------------------------------------------------------

def filter_events(
    events: list[NotificationEvent],
    config: NotificationConfig,
    history: NotificationHistory | None = None,
    now: datetime | None = None,
) -> list[NotificationEvent]:
    """Filter events based on config, dedup, and quiet hours.

    Args:
        events: List of events.
        config: Notification config.
        history: Optional history for dedup.
        now: Current time.

    Returns:
        Filtered list of events.
    """
    if not config.enabled:
        return []

    if is_quiet_hours(config, now):
        return []

    result: list[NotificationEvent] = []
    for event in events:
        if not event.meets_threshold(config.severity_threshold):
            continue
        if history and history.is_duplicate(event):
            continue
        result.append(event)

    return result
