#!/usr/bin/env python3
"""
content-monitor.py — Monitor Anthropic content sources for new material.

Sources:
  - platform.claude.com/cookbooks  (via GitHub API on anthropics/claude-cookbooks)
  - anthropic.skilljar.com
  - www.anthropic.com/learn
  - claude.com/resources/tutorials

Sends a Telegram message to the configured channel when new content appears.
State is persisted to ../data/content-state.json between runs.

Usage:
  python3 content-monitor.py            # normal run
  python3 content-monitor.py --dry-run  # print what would be sent, don't send or save state
  python3 content-monitor.py --reset    # clear saved state (forces re-baseline on next run)
"""

import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from urllib import request, error

# ── Paths ─────────────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).resolve().parent
DATA_DIR = SCRIPT_DIR.parent / "data"
STATE_FILE = DATA_DIR / "content-state.json"
ENV_FILE = SCRIPT_DIR.parent / ".env"

DRY_RUN = "--dry-run" in sys.argv
RESET = "--reset" in sys.argv


# ── Environment ───────────────────────────────────────────────────────────────
def load_env(path: Path) -> None:
    """Load key=value pairs from a .env file into os.environ (won't overwrite)."""
    if not path.exists():
        return
    for line in path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _, val = line.partition("=")
        key = key.strip()
        val = val.strip().strip('"').strip("'")
        if key:
            os.environ.setdefault(key, val)


load_env(ENV_FILE)

BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
CHANNEL_ID = os.environ.get("TELEGRAM_CHANNEL_ID", "")
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")


# ── State persistence ─────────────────────────────────────────────────────────
def load_state() -> dict:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text())
        except Exception:
            pass
    return {}


def save_state(state: dict) -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ── HTTP ──────────────────────────────────────────────────────────────────────
def fetch_url(url: str, headers: dict = None) -> str:
    headers = headers or {}
    headers.setdefault("User-Agent", "betterSkills-monitor/1.0 (+https://github.com)")
    req = request.Request(url, headers=headers)
    try:
        with request.urlopen(req, timeout=20) as resp:
            return resp.read().decode("utf-8", errors="replace")
    except error.HTTPError as e:
        print(f"  HTTP {e.code} — {url}", file=sys.stderr)
        return ""
    except Exception as e:
        print(f"  Error fetching {url}: {e}", file=sys.stderr)
        return ""


def github_get(path: str):
    """Call GitHub REST API. Returns parsed JSON or None."""
    headers = {"Accept": "application/vnd.github+json"}
    if GITHUB_TOKEN:
        headers["Authorization"] = f"Bearer {GITHUB_TOKEN}"
    raw = fetch_url(f"https://api.github.com/{path.lstrip('/')}", headers)
    if not raw:
        return None
    try:
        return json.loads(raw)
    except Exception:
        return None


# ── Telegram ──────────────────────────────────────────────────────────────────
def send_telegram(text: str) -> bool:
    if DRY_RUN:
        print("\n── Telegram message (dry run) ──────────────────────────")
        print(text)
        print("────────────────────────────────────────────────────────\n")
        return True

    if not BOT_TOKEN or not CHANNEL_ID:
        print("TELEGRAM_BOT_TOKEN or TELEGRAM_CHANNEL_ID not set — skipping send.", file=sys.stderr)
        return False

    payload = json.dumps({
        "chat_id": CHANNEL_ID,
        "text": text,
        "parse_mode": "Markdown",
        "disable_web_page_preview": True,
    }).encode()

    req = request.Request(
        f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    try:
        with request.urlopen(req, timeout=15) as resp:
            result = json.loads(resp.read())
            if not result.get("ok"):
                print(f"Telegram API error: {result}", file=sys.stderr)
                return False
            return True
    except Exception as e:
        print(f"Telegram send failed: {e}", file=sys.stderr)
        return False


# ── Content extraction ────────────────────────────────────────────────────────
def strip_tags(html: str) -> str:
    return re.sub(r"<[^>]+>", "", html)


def extract_content_titles(html: str, min_len: int = 25) -> set:
    """
    Extract plausible content titles from a page by pulling non-trivial anchor text.
    Strips nav/footer/script blocks first to reduce noise.
    """
    # Remove noisy structural blocks
    for tag in ("script", "style", "nav", "footer", "header"):
        html = re.sub(
            rf"<{tag}[^>]*>.*?</{tag}>", "", html, flags=re.DOTALL | re.IGNORECASE
        )

    links = re.findall(r"<a[^>]*>(.*?)</a>", html, re.DOTALL | re.IGNORECASE)
    titles = set()
    skip_prefixes = ("©", "Privacy", "Terms", "Cookie", "Sign in", "Log in", "Home", "Back")

    for raw in links:
        text = strip_tags(raw).strip()
        text = re.sub(r"\s+", " ", text)
        if (
            len(text) >= min_len
            and not any(text.startswith(p) for p in skip_prefixes)
        ):
            titles.add(text)

    return titles


# ── Source checkers ───────────────────────────────────────────────────────────
def check_cookbooks(state: dict) -> list:
    """
    Detect new cookbooks by watching commits to anthropics/claude-cookbooks.
    On first run, records the current HEAD SHA as baseline.
    On subsequent runs, compares to find newly added files.
    """
    key = "cookbooks_sha"
    prev_sha = state.get(key, "")

    commits = github_get("repos/anthropics/claude-cookbooks/commits?per_page=1")
    if not commits or not isinstance(commits, list):
        print("  Could not reach GitHub for cookbooks — skipping.", file=sys.stderr)
        return []

    latest_sha = commits[0].get("sha", "")
    if not latest_sha:
        return []

    if latest_sha == prev_sha:
        state[key] = latest_sha
        return []

    new_items = []
    if prev_sha:
        compare = github_get(
            f"repos/anthropics/claude-cookbooks/compare/{prev_sha}...{latest_sha}"
        )
        if compare and isinstance(compare, dict):
            for f in compare.get("files", []):
                if f.get("status") == "added":
                    filename = f.get("filename", "")
                    # Skip non-content files
                    if not any(filename.endswith(ext) for ext in (".md", ".ipynb", ".py")):
                        continue
                    # Turn filename into a readable title
                    stem = Path(filename).stem.replace("-", " ").replace("_", " ")
                    title = stem.title()
                    blob_url = f.get("blob_url", "https://platform.claude.com/cookbooks")
                    new_items.append(f"• [{title}]({blob_url})")
    else:
        # First run — just record baseline, don't report
        print("  Cookbooks: baseline recorded.")

    state[key] = latest_sha
    return new_items


def check_scrape(url: str, state_key: str, state: dict) -> list:
    """
    Fetch a page, extract content titles, diff against the previous set.
    Returns list of new title strings. On first run, baselines silently.
    """
    html = fetch_url(url)
    if not html:
        return []

    current = extract_content_titles(html)
    prev = set(state.get(state_key, []))

    state[state_key] = sorted(current)  # always update to latest full set

    if not prev:
        print(f"  {url}: baseline recorded ({len(current)} titles).")
        return []

    new_titles = sorted(current - prev)
    return [f"• {t}" for t in new_titles]


# ── Main ──────────────────────────────────────────────────────────────────────
def main() -> None:
    if RESET:
        if STATE_FILE.exists():
            STATE_FILE.unlink()
            print("State cleared. Run again to re-baseline all sources.")
        else:
            print("No state file found.")
        return

    state = load_state()
    sections = []

    print("Checking: Anthropic Cookbooks (GitHub)")
    cookbook_items = check_cookbooks(state)
    if cookbook_items:
        sections.append(("📖 New Cookbooks", cookbook_items))

    print("Checking: Anthropic Academy (Skilljar)")
    skilljar_items = check_scrape("https://anthropic.skilljar.com", "skilljar", state)
    if skilljar_items:
        sections.append(("🎓 New Courses — Anthropic Academy", skilljar_items))

    print("Checking: Anthropic Learn")
    learn_items = check_scrape("https://www.anthropic.com/learn", "anthropic_learn", state)
    if learn_items:
        sections.append(("📚 New — Anthropic Learn", learn_items))

    print("Checking: Claude Tutorials")
    tutorial_items = check_scrape(
        "https://claude.com/resources/tutorials", "claude_tutorials", state
    )
    if tutorial_items:
        sections.append(("🎬 New Tutorials", tutorial_items))

    if not DRY_RUN:
        save_state(state)

    if not sections:
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        print(f"[{ts}] No new content found.")
        return

    # Build Telegram message
    date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    lines = [f"📡 *Anthropic Content Update — {date_str}*\n"]

    for heading, items in sections:
        lines.append(f"*{heading}*")
        # Telegram messages cap at 4096 chars; limit items per section
        for item in items[:15]:
            lines.append(item)
        if len(items) > 15:
            lines.append(f"  _...and {len(items) - 15} more_")
        lines.append("")

    message = "\n".join(lines).strip()
    send_telegram(message)


if __name__ == "__main__":
    main()
