#!/usr/bin/env python3
"""
Notification runner - executes user notification scripts and sends results.

Finds Python scripts in ~/workspace/notifications/, runs each one,
and sends notifications via the bot's unix socket API.

Usage:
    notify-runner
    # Or in crontab:
    */5 * * * * /usr/local/bin/notify-runner >> ~/.notifications/logs/cron.log 2>&1
"""

import json
import logging
import os
import re
import subprocess
import sys
import time
from pathlib import Path

import httpx

# Configuration
SOCKET_PATH = "/run/notify-bot/bot.sock"
WS_GATEWAY_SOCKET_PATH = "/run/ws-gateway/ws.sock"
SCRIPT_TIMEOUT_SECONDS = 60
NOTIFICATIONS_DIR = Path.home() / "user" / "notifications"
STATE_DIR = Path.home() / ".notifications" / "state"
LOG_DIR = Path.home() / ".notifications" / "logs"

COOLDOWN_MAP = {
    "1m": 60,
    "5m": 300,
    "10m": 600,
    "15m": 900,
    "30m": 1800,
    "1h": 3600,
    "2h": 7200,
    "4h": 14400,
    "6h": 21600,
    "12h": 43200,
    "1d": 86400,
}
DEFAULT_COOLDOWN = "1h"

# Setup logging
LOG_DIR.mkdir(parents=True, exist_ok=True)
STATE_DIR.mkdir(parents=True, exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [notify-runner] %(levelname)s: %(message)s",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler(LOG_DIR / "runner.log", mode="a"),
    ],
)
logger = logging.getLogger("notify-runner")


def get_username() -> str:
    """Get the current system username."""
    return os.environ.get("USER", os.environ.get("LOGNAME", "unknown"))


def parse_cooldown(cooldown_str: str) -> int:
    """Parse cooldown string like '30m', '1h', '1d' to seconds."""
    cooldown_str = cooldown_str.strip().lower()
    if cooldown_str in COOLDOWN_MAP:
        return COOLDOWN_MAP[cooldown_str]

    # Try parsing as Xm, Xh, Xd
    match = re.match(r"^(\d+)(m|h|d)$", cooldown_str)
    if match:
        value = int(match.group(1))
        unit = match.group(2)
        multiplier = {"m": 60, "h": 3600, "d": 86400}
        return value * multiplier[unit]

    return COOLDOWN_MAP[DEFAULT_COOLDOWN]


def check_cooldown(script_name: str, cooldown_seconds: int) -> bool:
    """Check if the script is still in cooldown. Returns True if OK to send."""
    state_file = STATE_DIR / f"{script_name}.json"
    try:
        with open(state_file, "r") as f:
            state = json.load(f)
        last_sent = state.get("last_sent", 0)
        return (time.time() - last_sent) >= cooldown_seconds
    except (FileNotFoundError, json.JSONDecodeError, KeyError):
        return True


def update_cooldown(script_name: str) -> None:
    """Record that a notification was sent."""
    state_file = STATE_DIR / f"{script_name}.json"
    with open(state_file, "w") as f:
        json.dump({"last_sent": time.time()}, f)


def run_script(script_path: Path) -> dict | None:
    """Run a notification script and parse its JSON output."""
    try:
        result = subprocess.run(
            [sys.executable, str(script_path)],
            capture_output=True,
            text=True,
            timeout=SCRIPT_TIMEOUT_SECONDS,
            cwd=str(Path.home()),
        )

        if result.returncode != 0:
            logger.warning(
                f"Script {script_path.name} exited with code {result.returncode}: "
                f"{result.stderr[:500]}"
            )
            return None

        if result.stderr:
            logger.debug(f"Script {script_path.name} stderr: {result.stderr[:500]}")

        stdout = result.stdout.strip()
        if not stdout:
            return None

        return json.loads(stdout)

    except subprocess.TimeoutExpired:
        logger.error(f"Script {script_path.name} timed out after {SCRIPT_TIMEOUT_SECONDS}s")
        return None
    except json.JSONDecodeError as e:
        logger.error(f"Script {script_path.name} returned invalid JSON: {e}")
        return None
    except Exception:
        logger.exception(f"Error running script {script_path.name}")
        return None


def send_text(username: str, text: str, parse_mode: str = "Markdown") -> bool:
    """Send a text notification via bot socket."""
    try:
        transport = httpx.HTTPTransport(uds=SOCKET_PATH)
        with httpx.Client(transport=transport, timeout=30) as client:
            resp = client.post(
                "http://localhost/send",
                json={
                    "user": username,
                    "text": text,
                    "parse_mode": parse_mode,
                },
            )
            return resp.status_code == 200
    except Exception:
        logger.exception("Failed to send text notification")
        return False


def send_photo(username: str, photo_path: str, caption: str = "") -> bool:
    """Send a photo notification via bot socket."""
    try:
        transport = httpx.HTTPTransport(uds=SOCKET_PATH)
        with httpx.Client(transport=transport, timeout=60) as client:
            resp = client.post(
                "http://localhost/send_photo",
                json={
                    "user": username,
                    "photo_path": photo_path,
                    "caption": caption,
                },
            )
            return resp.status_code == 200
    except Exception:
        logger.exception("Failed to send photo notification")
        return False


def dispatch_to_ws_gateway(username: str, output: dict) -> None:
    """Dispatch notification to WebSocket gateway for desktop app clients."""
    if not os.path.exists(WS_GATEWAY_SOCKET_PATH):
        return
    try:
        import uuid

        transport = httpx.HTTPTransport(uds=WS_GATEWAY_SOCKET_PATH)
        with httpx.Client(transport=transport, timeout=10) as client:
            notification = {
                "id": str(uuid.uuid4()),
                "title": output.get("title", ""),
                "message": output.get("message", ""),
                "script": Path(output.get("_script_name", "")).stem if output.get("_script_name") else None,
                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            }
            image_path = output.get("image_path", "")
            if image_path and os.path.isfile(image_path):
                filename = os.path.basename(image_path)
                notification["image_url"] = f"/api/notifications/images/{filename}"
            client.post(
                "http://localhost/dispatch",
                json={"user": username, "notification": notification},
            )
    except Exception:
        logger.debug("WS gateway dispatch failed (gateway may not be running)")


def format_message(output: dict) -> str:
    """Format notification output into a Telegram message."""
    parts = []
    title = output.get("title", "")
    message = output.get("message", "")

    if title:
        parts.append(f"*{title}*")
    if message:
        parts.append(message)

    return "\n".join(parts) if parts else ""


def process_script(script_path: Path, username: str) -> None:
    """Run a single notification script and handle its output."""
    script_name = script_path.stem
    logger.info(f"Running {script_name}...")

    output = run_script(script_path)
    if output is None:
        return

    if not output.get("notify", False):
        logger.info(f"{script_name}: notify=false, skipping")
        return

    # Check cooldown
    cooldown_str = output.get("cooldown", DEFAULT_COOLDOWN)
    cooldown_seconds = parse_cooldown(cooldown_str)

    if not check_cooldown(script_name, cooldown_seconds):
        logger.info(f"{script_name}: in cooldown, skipping")
        return

    # Send notification
    text = format_message(output)
    image_path = output.get("image_path", "")

    sent = False

    if image_path and os.path.isfile(image_path):
        # Send photo with caption
        sent = send_photo(username, image_path, caption=text)
    elif text:
        sent = send_text(username, text)
    else:
        logger.warning(f"{script_name}: notify=true but no message or image")
        return

    if sent:
        update_cooldown(script_name)
        logger.info(f"{script_name}: notification sent")
        # Also dispatch to WebSocket gateway for desktop app
        dispatch_to_ws_gateway(username, output)
    else:
        logger.error(f"{script_name}: failed to send notification")


def main() -> None:
    """Main entry point."""
    username = get_username()
    logger.info(f"Starting notify-runner for user '{username}'")

    if not NOTIFICATIONS_DIR.is_dir():
        logger.info(f"No notifications directory at {NOTIFICATIONS_DIR}")
        return

    scripts = sorted(NOTIFICATIONS_DIR.glob("*.py"))
    if not scripts:
        logger.info("No notification scripts found")
        return

    logger.info(f"Found {len(scripts)} script(s)")

    for script_path in scripts:
        process_script(script_path, username)

    logger.info("Done")


if __name__ == "__main__":
    main()
