"""
Phase 45: Notification Preferences — User-Configurable Notification Settings.

Advanced, per-agent notification preferences that control what notifications
arrive, how they are delivered, and when to mute. Builds on Phase 29
(notifications.py) and Phase 32 (webhooks.py).

Features:
  - PREFERENCES: Per-agent channel, event, and delivery configuration
  - SHOULD_NOTIFY: Decision engine checking quiet hours, mutes, DND, etc.
  - MUTE/UNMUTE: Temporary or permanent muting of channels and agents
  - DIGEST: Queue notifications for periodic digest delivery
  - DND: Do Not Disturb mode blocks all notifications
  - DEFAULTS: Platform-wide default config with per-agent reset

Storage:
  state/broker/notification_prefs/
    {agent_id}.json                  — agent preference config
    digests/{agent_id}.jsonl         — queued digest notifications
"""

import json
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    _write_json_sync,
    _read_jsonl_sync,
    _append_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

PREFS_DIR = BROKER_DIR / "notification_prefs"
DIGESTS_DIR = PREFS_DIR / "digests"

for _d in (PREFS_DIR, DIGESTS_DIR):
    _d.mkdir(parents=True, exist_ok=True)


# ── Default Preferences ───────────────────────────────────────────────────


def get_default_preferences() -> dict:
    """Return the platform default notification preferences.

    Every field has a sensible default so agents can start receiving
    notifications immediately without any configuration.
    """
    return {
        "channels": {},                 # channel_name -> {enabled, priority_only}
        "events": {},                   # event_type -> {enabled, delivery}
        "quiet_hours": {
            "enabled": False,
            "start": "22:00",
            "end": "08:00",
            "timezone": "UTC",
        },
        "digest_interval_hours": 4,
        "muted_agents": [],             # list of agent IDs to ignore
        "muted_channels": {},           # channel -> {until: iso|null}
        "delivery_methods": ["broker", "webhook", "email"],
        "dnd": {
            "enabled": False,
            "until": None,              # ISO timestamp or None for permanent
        },
    }


# ── Set / Get / Reset ─────────────────────────────────────────────────────


def set_preferences(agent_id: str, prefs: dict) -> dict:
    """Save notification preferences for an agent.

    Merges the supplied *prefs* dict on top of defaults so that any
    missing keys are filled in automatically.

    Args:
        agent_id: The agent whose preferences to set.
        prefs: Partial or full preference dict.  Unset keys default.

    Returns:
        The merged, persisted preference dict.
    """
    defaults = get_default_preferences()

    merged = {**defaults}
    for key in defaults:
        if key in prefs:
            # For nested dicts, merge one level deep
            if isinstance(defaults[key], dict) and isinstance(prefs[key], dict):
                merged[key] = {**defaults[key], **prefs[key]}
            else:
                merged[key] = prefs[key]

    merged["agent_id"] = agent_id
    merged["updated_at"] = datetime.now(timezone.utc).isoformat()

    _write_json_sync(PREFS_DIR / f"{agent_id}.json", merged)
    return merged


def get_preferences(agent_id: str) -> dict:
    """Return an agent's notification preferences, with defaults for unset values.

    If the agent has no saved preferences file, the full default config
    is returned (not persisted).
    """
    path = PREFS_DIR / f"{agent_id}.json"
    defaults = get_default_preferences()

    try:
        saved = json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        result = {**defaults, "agent_id": agent_id}
        return result

    # Merge defaults under saved so missing keys are filled
    merged = {**defaults}
    for key in saved:
        if key in defaults and isinstance(defaults[key], dict) and isinstance(saved[key], dict):
            merged[key] = {**defaults[key], **saved[key]}
        else:
            merged[key] = saved[key]

    merged["agent_id"] = agent_id
    return merged


def reset_preferences(agent_id: str) -> dict:
    """Reset an agent's preferences to platform defaults.

    Deletes any saved file and returns the fresh defaults.
    """
    path = PREFS_DIR / f"{agent_id}.json"
    try:
        path.unlink(missing_ok=True)
    except OSError:
        pass

    defaults = get_default_preferences()
    defaults["agent_id"] = agent_id
    return defaults


# ── Should Notify (decision engine) ───────────────────────────────────────


def should_notify(
    agent_id: str,
    event_type: str,
    channel: Optional[str] = None,
    sender_id: Optional[str] = None,
) -> tuple:
    """Determine whether a notification should be delivered.

    Checks (in order):
        1. Do Not Disturb — blocks everything
        2. Quiet hours — blocks if current time is within quiet window
        3. Muted agents — blocks if sender is muted
        4. Muted channels — blocks if channel is muted
        5. Channel preferences — checks enabled / priority_only
        6. Event preferences — checks enabled / delivery mode

    Returns:
        (should_send: bool, delivery_method: str, reason: str)
    """
    prefs = get_preferences(agent_id)

    # 1. DND
    dnd = prefs.get("dnd", {})
    if dnd.get("enabled"):
        until = dnd.get("until")
        if until is None:
            return (False, "", "dnd_permanent")
        try:
            until_dt = datetime.fromisoformat(until)
            if datetime.now(timezone.utc) < until_dt:
                return (False, "", "dnd_active")
            # DND expired — fall through
        except (ValueError, TypeError):
            return (False, "", "dnd_active")

    # 2. Quiet hours
    qh = prefs.get("quiet_hours", {})
    if qh.get("enabled"):
        if _in_quiet_hours(qh):
            return (False, "", "quiet_hours")

    # 3. Muted agents
    if sender_id and sender_id in prefs.get("muted_agents", []):
        return (False, "", "sender_muted")

    # 4. Muted channels
    if channel:
        muted_channels = prefs.get("muted_channels", {})
        if channel in muted_channels:
            mute_info = muted_channels[channel]
            until = mute_info.get("until") if isinstance(mute_info, dict) else None
            if until is None:
                return (False, "", "channel_muted")
            try:
                until_dt = datetime.fromisoformat(until)
                if datetime.now(timezone.utc) < until_dt:
                    return (False, "", "channel_muted")
                # Mute expired — fall through
            except (ValueError, TypeError):
                return (False, "", "channel_muted")

    # 5. Channel preferences
    if channel:
        ch_prefs = prefs.get("channels", {}).get(channel)
        if ch_prefs and isinstance(ch_prefs, dict):
            if not ch_prefs.get("enabled", True):
                return (False, "", "channel_disabled")

    # 6. Event preferences
    ev_prefs = prefs.get("events", {}).get(event_type)
    if ev_prefs and isinstance(ev_prefs, dict):
        if not ev_prefs.get("enabled", True):
            return (False, "", "event_disabled")
        delivery = ev_prefs.get("delivery", "instant")
        if delivery == "off":
            return (False, "", "event_off")
        if delivery == "digest":
            methods = prefs.get("delivery_methods", ["broker"])
            return (True, methods[0] if methods else "broker", "digest")

    # All checks passed — deliver normally
    methods = prefs.get("delivery_methods", ["broker"])
    return (True, methods[0] if methods else "broker", "allowed")


def _in_quiet_hours(qh: dict) -> bool:
    """Check if the current time falls within the configured quiet-hours window.

    Handles windows that cross midnight (e.g. 22:00-08:00).
    """
    try:
        start_h, start_m = map(int, qh["start"].split(":"))
        end_h, end_m = map(int, qh["end"].split(":"))
    except (KeyError, ValueError, AttributeError):
        return False

    now = datetime.now(timezone.utc)
    current_minutes = now.hour * 60 + now.minute
    start_minutes = start_h * 60 + start_m
    end_minutes = end_h * 60 + end_m

    if start_minutes <= end_minutes:
        # Same-day window (e.g. 08:00-17:00)
        return start_minutes <= current_minutes < end_minutes
    else:
        # Cross-midnight window (e.g. 22:00-08:00)
        return current_minutes >= start_minutes or current_minutes < end_minutes


# ── Mute / Unmute Channels ────────────────────────────────────────────────


def mute_channel(
    agent_id: str, channel: str, duration_hours: Optional[float] = None
) -> dict:
    """Mute a channel temporarily or permanently.

    Args:
        agent_id: The agent.
        channel: Channel name to mute.
        duration_hours: Hours to mute for. ``None`` means permanent.

    Returns:
        Updated muted_channels dict.
    """
    prefs = get_preferences(agent_id)
    muted = prefs.get("muted_channels", {})

    until = None
    if duration_hours is not None:
        until = (datetime.now(timezone.utc) + timedelta(hours=duration_hours)).isoformat()

    muted[channel] = {"until": until, "muted_at": datetime.now(timezone.utc).isoformat()}
    prefs["muted_channels"] = muted

    set_preferences(agent_id, prefs)
    return muted


def unmute_channel(agent_id: str, channel: str) -> dict:
    """Unmute a previously muted channel.

    Returns:
        Updated muted_channels dict.
    """
    prefs = get_preferences(agent_id)
    muted = prefs.get("muted_channels", {})
    muted.pop(channel, None)
    prefs["muted_channels"] = muted

    set_preferences(agent_id, prefs)
    return muted


# ── Mute / Unmute Agents ──────────────────────────────────────────────────


def mute_agent(agent_id: str, target_id: str) -> list:
    """Mute notifications from a specific agent.

    Args:
        agent_id: The agent whose prefs to modify.
        target_id: The agent to mute.

    Returns:
        Updated muted_agents list.
    """
    prefs = get_preferences(agent_id)
    muted = prefs.get("muted_agents", [])
    if target_id not in muted:
        muted.append(target_id)
    prefs["muted_agents"] = muted

    set_preferences(agent_id, prefs)
    return muted


def unmute_agent(agent_id: str, target_id: str) -> list:
    """Unmute a previously muted agent.

    Returns:
        Updated muted_agents list.
    """
    prefs = get_preferences(agent_id)
    muted = prefs.get("muted_agents", [])
    if target_id in muted:
        muted.remove(target_id)
    prefs["muted_agents"] = muted

    set_preferences(agent_id, prefs)
    return muted


def list_muted(agent_id: str) -> dict:
    """Return all muted channels and agents for an agent.

    Returns:
        {"muted_channels": {...}, "muted_agents": [...]}
    """
    prefs = get_preferences(agent_id)
    return {
        "muted_channels": prefs.get("muted_channels", {}),
        "muted_agents": prefs.get("muted_agents", []),
    }


# ── Digest Queue ──────────────────────────────────────────────────────────


def queue_for_digest(agent_id: str, notification: dict) -> dict:
    """Add a notification to the agent's digest queue.

    Instead of instant delivery, the notification will be batched and
    sent at the next digest interval.

    Args:
        agent_id: The agent.
        notification: The notification payload dict.

    Returns:
        Confirmation dict.
    """
    path = DIGESTS_DIR / f"{agent_id}.jsonl"
    entry = {
        **notification,
        "queued_at": datetime.now(timezone.utc).isoformat(),
    }
    _append_jsonl_sync(path, entry)
    return {"status": "queued_for_digest", "agent_id": agent_id}


def get_digest(agent_id: str, clear: bool = True) -> list:
    """Return all queued digest notifications for an agent.

    Args:
        agent_id: The agent.
        clear: If True, clears the queue after reading.

    Returns:
        List of queued notification dicts.
    """
    path = DIGESTS_DIR / f"{agent_id}.jsonl"
    entries = _read_jsonl_sync(path)

    if clear and entries:
        try:
            path.unlink(missing_ok=True)
        except OSError:
            pass

    return entries


def get_digest_summary(agent_id: str) -> dict:
    """Return a summary of the agent's digest queue without clearing it.

    Returns:
        {"count": int, "preview": list[str]}
    """
    path = DIGESTS_DIR / f"{agent_id}.jsonl"
    entries = _read_jsonl_sync(path)

    preview = []
    for entry in entries[:5]:
        title = entry.get("title", entry.get("event_type", "notification"))
        preview.append(title)

    return {
        "count": len(entries),
        "preview": preview,
    }


# ── Do Not Disturb ────────────────────────────────────────────────────────


def enable_dnd(agent_id: str, duration_hours: Optional[float] = None) -> dict:
    """Enable Do Not Disturb — blocks all notifications.

    Args:
        agent_id: The agent.
        duration_hours: How long DND lasts. ``None`` means indefinite.

    Returns:
        Updated DND config.
    """
    prefs = get_preferences(agent_id)

    until = None
    if duration_hours is not None:
        until = (datetime.now(timezone.utc) + timedelta(hours=duration_hours)).isoformat()

    prefs["dnd"] = {
        "enabled": True,
        "until": until,
        "enabled_at": datetime.now(timezone.utc).isoformat(),
    }

    set_preferences(agent_id, prefs)
    return prefs["dnd"]


def disable_dnd(agent_id: str) -> dict:
    """Disable Do Not Disturb.

    Returns:
        Updated DND config.
    """
    prefs = get_preferences(agent_id)
    prefs["dnd"] = {
        "enabled": False,
        "until": None,
    }

    set_preferences(agent_id, prefs)
    return prefs["dnd"]


def is_dnd(agent_id: str) -> bool:
    """Check if an agent currently has DND active.

    Accounts for expired timed-DND windows.
    """
    prefs = get_preferences(agent_id)
    dnd = prefs.get("dnd", {})

    if not dnd.get("enabled"):
        return False

    until = dnd.get("until")
    if until is None:
        return True  # Permanent DND

    try:
        until_dt = datetime.fromisoformat(until)
        return datetime.now(timezone.utc) < until_dt
    except (ValueError, TypeError):
        return True
