#!/usr/bin/env python3
"""
MRAV Call Watcher — Phase 2C
Watches calls.jsonl for completed calls, extracts structured items via Claude,
routes each item to the correct tool, sends Telegram confirmation.
Runs via system cron every 2 minutes.
"""

import json
import os
import re
import subprocess
import tempfile
import shutil
import urllib.request
import urllib.parse
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo

CALLS_JSONL = "/home/isthekid/.openclaw/voice-calls/calls.jsonl"
STATE_FILE = "/home/isthekid/.openclaw/workspace/scripts/call-watcher-state.json"
OPENCLAW_CONFIG = "/home/isthekid/.openclaw/openclaw.json"
EXTRACTIONS_DIR = "/home/isthekid/.openclaw/voice-calls/extractions"
ANTHROPIC_SECRETS = "/home/isthekid/.openclaw/secrets/anthropic.env"
ACTION_DATA = "/home/isthekid/.openclaw/workspace/action/data.json"
MEMORY_DIR = "/home/isthekid/.openclaw/workspace/memory"
GOG_CALENDAR_ACCOUNT = "srvdeskops@gmail.com"
GOG_CALENDAR_ID = "primary"
GOG_SECRETS = "/home/isthekid/.openclaw/secrets/gog.env"
TIMEZONE = ZoneInfo("America/New_York")
ANTHROPIC_MODEL = "claude-sonnet-4-6"

EXTRACTION_PROMPT = """You are a structured data extractor. Below is a transcript of a phone call between Adner and his voice assistant MRAV.

Extract all actionable items into JSON.

Rules:
- Every item must have a unique id (format: "item-001", "item-002", etc.)
- Classify each item as exactly one type: task, calendar_event, note, decision, follow_up
- For calendar_event: extract date and time if mentioned. If only a day name is given (e.g. "Friday"), resolve it to the next occurrence from today's date: {TODAY}
- For follow_up: extract a follow-up date if mentioned
- If anything is ambiguous or you can't confidently classify it, put it in the "ambiguous" array
- Do not invent items that weren't discussed
- Ignore greetings, sign-offs, and small talk — only extract actionable content
- Respond with ONLY valid JSON, no preamble, no markdown fences

JSON format:
{{
  "call_id": "{CALL_ID}",
  "timestamp": "{CALL_TIMESTAMP}",
  "duration_seconds": {DURATION},
  "items": [
    {{
      "id": "item-001",
      "type": "task",
      "content": "Call Jerry about the permit renewal",
      "priority": "normal",
      "due": null
    }},
    {{
      "id": "item-002",
      "type": "calendar_event",
      "content": "Meeting with Erika",
      "date": "2026-03-07",
      "time": "15:00",
      "duration_minutes": 30
    }}
  ],
  "ambiguous": []
}}

Transcript:
{TRANSCRIPT}"""


def get_telegram_credentials():
    with open(OPENCLAW_CONFIG) as f:
        raw = f.read()
    token_match = re.search(r'([0-9]{8,10}:AA[A-Za-z0-9_-]{33,})', raw)
    chat_match = re.search(r'"id"\s*:\s*"?(8340647700)"?', raw)
    if not token_match:
        raise ValueError("Telegram bot token not found in openclaw.json")
    return token_match.group(1), (chat_match.group(1) if chat_match else "8340647700")


def get_anthropic_key():
    """Get Anthropic API key from env or secrets file."""
    key = os.environ.get("ANTHROPIC_API_KEY", "")
    if key:
        return key
    if os.path.exists(ANTHROPIC_SECRETS):
        with open(ANTHROPIC_SECRETS) as f:
            for line in f:
                line = line.strip()
                if line.startswith("ANTHROPIC_API_KEY="):
                    return line.split("=", 1)[1].strip().strip('"').strip("'")
    raise ValueError("ANTHROPIC_API_KEY not found in environment or secrets file")


def load_state():
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            return json.load(f)
    return {"processed_call_ids": []}


def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)


def format_duration(start_ms, end_ms):
    total_sec = max(0, int((end_ms - start_ms) / 1000))
    minutes = total_sec // 60
    seconds = total_sec % 60
    if minutes > 0:
        return f"{minutes} min {seconds} sec"
    return f"{seconds} sec"


def format_timestamp(ts_ms):
    dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).astimezone(TIMEZONE)
    return dt.strftime("%B %-d, %Y %-I:%M %p EST")


def format_timestamp_short(ts_ms):
    dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).astimezone(TIMEZONE)
    return dt.strftime("%b %-d, %-I:%M %p")


def is_recognizable_english(text):
    """Filter out noise artifacts — require at least 50% ASCII printable chars."""
    if not text or not text.strip():
        return False
    text = text.strip()
    ascii_chars = sum(1 for c in text if ord(c) < 128 and c.isprintable())
    return ascii_chars / len(text) >= 0.5


def format_transcript(transcript):
    """Format transcript as readable text, filtering noise."""
    lines = []
    for entry in transcript:
        if not entry.get("isFinal"):
            continue
        text = entry.get("text", "").strip()
        if not is_recognizable_english(text):
            continue
        speaker = "Adner" if entry.get("speaker") == "user" else "MRAV"
        lines.append(f"{speaker}: {text}")
    return "\n".join(lines)


def call_anthropic(api_key, prompt):
    """Call Anthropic API and return response text."""
    payload = json.dumps({
        "model": ANTHROPIC_MODEL,
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": prompt}]
    }).encode("utf-8")

    req = urllib.request.Request(
        "https://api.anthropic.com/v1/messages",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "x-api-key": api_key,
            "anthropic-version": "2023-06-01"
        },
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read())
    text = result["content"][0]["text"].strip()
    # Strip markdown fences if present
    text = re.sub(r'^```(?:json)?\s*', '', text)
    text = re.sub(r'\s*```$', '', text)
    return text.strip()


def send_telegram(bot_token, chat_id, text):
    payload = json.dumps({
        "chat_id": chat_id,
        "text": text,
        "parse_mode": "HTML"
    }).encode("utf-8")
    req = urllib.request.Request(
        f"https://api.telegram.org/bot{bot_token}/sendMessage",
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read())


def format_extraction_message(extraction, call, duration_str, time_short):
    """Format extracted items as a readable Telegram message."""
    items = extraction.get("items", [])
    ambiguous = extraction.get("ambiguous", [])
    call_id = call["callId"]

    header = f"📞 <b>Call extracted</b> ({time_short} — {duration_str})\n"

    if not items and not ambiguous:
        body = "No actionable items found."
    else:
        lines = []
        for i, item in enumerate(items, 1):
            t = item.get("type", "?")
            content = item.get("content", "")
            extra = ""
            if t == "calendar_event":
                date = item.get("date", "")
                time = item.get("time", "")
                if date or time:
                    extra = f" — {date} @ {time}" if time else f" — {date}"
            elif t == "follow_up":
                due = item.get("due") or item.get("date", "")
                if due:
                    extra = f" — due {due}"
            elif t == "task":
                due = item.get("due")
                if due:
                    extra = f" — due {due}"
            lines.append(f"{i}. [{t}] {content}{extra}")
        body = "\n".join(lines)

    amb_section = ""
    if ambiguous:
        amb_lines = [f"  • {a.get('content', str(a))}" for a in ambiguous]
        amb_section = "\n\n⚠️ <b>Ambiguous:</b>\n" + "\n".join(amb_lines)
    else:
        amb_section = "\n\nAmbiguous: none"

    save_path = f"{EXTRACTIONS_DIR}/{call_id}.json"
    footer = f"\n\nRaw JSON → <code>{save_path}</code>"

    return header + body + amb_section + footer


def atomic_write_json(path, obj):
    """Write JSON atomically using temp file + rename to avoid corruption."""
    dir_ = os.path.dirname(path)
    with tempfile.NamedTemporaryFile("w", dir=dir_, suffix=".tmp", delete=False) as f:
        json.dump(obj, f, indent=2)
        tmp_path = f.name
    shutil.move(tmp_path, path)


def route_board_card(item, call_timestamp_short):
    """Add a new card to action/data.json. Returns (ok, message)."""
    try:
        with open(ACTION_DATA) as f:
            data = json.load(f)

        # Determine target column
        item_type = item.get("type")
        target_col = "waiting" if item_type == "follow_up" else "do_now"

        # Generate unique ID
        ts = datetime.now(TIMEZONE).strftime("%Y%m%d%H%M%S")
        rand = os.urandom(2).hex()
        card_id = f"mrav-{ts}-{rand}"

        # Build card
        card = {
            "id": card_id,
            "title": item.get("content", "Untitled task"),
            "area": "MRAV Capture",
            "priority": item.get("priority", "normal").capitalize(),
            "due": item.get("due") or item.get("date") or None,
            "nextActions": [],
            "tags": ["mrav"],
            "status": f"Captured via voice call ({call_timestamp_short})"
        }

        # Add to target column
        inserted = False
        for col in data.get("columns", []):
            if col.get("id") == target_col:
                col.setdefault("cards", []).insert(0, card)
                inserted = True
                break

        if not inserted:
            # Fallback: add to first column
            if data.get("columns"):
                data["columns"][0].setdefault("cards", []).insert(0, card)

        atomic_write_json(ACTION_DATA, data)
        return True, f"Board card added → {target_col}: {card['title']}"

    except Exception as e:
        return False, f"Board error: {e}"


def get_gog_env():
    """Build env dict for gog subprocess, injecting GOG_KEYRING_PASSWORD if available."""
    env = os.environ.copy()
    if os.path.exists(GOG_SECRETS):
        with open(GOG_SECRETS) as f:
            for line in f:
                line = line.strip()
                if line.startswith("GOG_KEYRING_PASSWORD="):
                    env["GOG_KEYRING_PASSWORD"] = line.split("=", 1)[1].strip().strip('"').strip("'")
    return env


def route_calendar_event(item, call_timestamp_short):
    """Create a Google Calendar event via gog CLI. Returns (ok, message)."""
    try:
        summary = item.get("content", "Event")
        date = item.get("date")
        time_ = item.get("time", "09:00")
        duration_min = item.get("duration_minutes") or 30

        if not date:
            return False, "Calendar skipped: no date provided"

        # Parse start datetime
        start_str = f"{date}T{time_}:00"
        start_dt = datetime.fromisoformat(start_str).replace(tzinfo=TIMEZONE)
        end_dt = start_dt + timedelta(minutes=int(duration_min))

        from_rfc = start_dt.isoformat()
        to_rfc = end_dt.isoformat()

        result = subprocess.run(
            [
                "gog", "calendar", "create", GOG_CALENDAR_ID,
                "--account", GOG_CALENDAR_ACCOUNT,
                "--summary", summary,
                "--from", from_rfc,
                "--to", to_rfc,
                "--description", f"Captured via MRAV voice call ({call_timestamp_short})",
                "--no-input"
            ],
            capture_output=True, text=True, timeout=20,
            env=get_gog_env()
        )

        if result.returncode != 0:
            return False, f"Calendar error: {result.stderr.strip() or result.stdout.strip()}"

        return True, f"Calendar event created: {summary} on {date} @ {time_}"

    except Exception as e:
        return False, f"Calendar error: {e}"


def route_memory(item, call_timestamp_short):
    """Append note or decision to today's memory file. Returns (ok, message)."""
    try:
        today = datetime.now(TIMEZONE).strftime("%Y-%m-%d")
        mem_file = os.path.join(MEMORY_DIR, f"{today}.md")

        item_type = item.get("type", "note").capitalize()
        content = item.get("content", "")
        time_now = datetime.now(TIMEZONE).strftime("%-I:%M %p")

        entry = f"\n## MRAV Capture — {time_now}\n\n**{item_type} (via voice call, {call_timestamp_short}):**\n{content}\n"

        os.makedirs(MEMORY_DIR, exist_ok=True)
        with open(mem_file, "a") as f:
            f.write(entry)

        return True, f"Memory logged ({item_type.lower()}): {content[:60]}"

    except Exception as e:
        return False, f"Memory error: {e}"


def route_item(item, call_timestamp_short):
    """Route a single extracted item to the correct tool."""
    t = item.get("type")
    if t in ("task", "follow_up"):
        return route_board_card(item, call_timestamp_short)
    elif t == "calendar_event":
        return route_calendar_event(item, call_timestamp_short)
    elif t in ("note", "decision"):
        return route_memory(item, call_timestamp_short)
    else:
        return False, f"Unknown type: {t}"


def read_completed_calls():
    if not os.path.exists(CALLS_JSONL):
        return []
    seen = {}
    with open(CALLS_JSONL) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                record = json.loads(line)
                if record.get("endedAt"):
                    cid = record.get("callId")
                    if cid:
                        seen[cid] = record
            except json.JSONDecodeError:
                continue
    return list(seen.values())


def main():
    state = load_state()
    processed = set(state.get("processed_call_ids", []))

    try:
        bot_token, chat_id = get_telegram_credentials()
    except Exception as e:
        print(f"[call-watcher] Telegram credentials error: {e}")
        return

    try:
        anthropic_key = get_anthropic_key()
    except Exception as e:
        print(f"[call-watcher] Anthropic key error: {e}")
        return

    os.makedirs(EXTRACTIONS_DIR, exist_ok=True)

    today = datetime.now(TIMEZONE).strftime("%Y-%m-%d")
    completed_calls = read_completed_calls()
    new_calls = [c for c in completed_calls if c["callId"] not in processed]

    for call in new_calls:
        call_id = call["callId"]
        try:
            started = call.get("startedAt", 0)
            ended = call.get("endedAt", 0)
            duration_sec = max(0, int((ended - started) / 1000))
            duration_str = format_duration(started, ended)
            time_short = format_timestamp_short(started)
            call_timestamp = format_timestamp(started)

            transcript_text = format_transcript(call.get("transcript", []))

            if not transcript_text.strip():
                print(f"[call-watcher] No usable transcript for {call_id}, skipping extraction")
                processed.add(call_id)
                continue

            # Build extraction prompt
            prompt = EXTRACTION_PROMPT.format(
                TODAY=today,
                CALL_ID=call_id,
                CALL_TIMESTAMP=call_timestamp,
                DURATION=duration_sec,
                TRANSCRIPT=transcript_text
            )

            print(f"[call-watcher] Extracting {call_id}...")
            raw_json = call_anthropic(anthropic_key, prompt)

            # Parse and save extraction
            extraction = json.loads(raw_json)
            save_path = os.path.join(EXTRACTIONS_DIR, f"{call_id}.json")
            with open(save_path, "w") as f:
                json.dump(extraction, f, indent=2)

            # Route each item
            routing_results = []
            for item in extraction.get("items", []):
                ok, msg_r = route_item(item, time_short)
                routing_results.append((item, ok, msg_r))
                print(f"[call-watcher] Route {'✓' if ok else '✗'}: {msg_r}")

            # Flag ambiguous items on Telegram
            for amb in extraction.get("ambiguous", []):
                content = amb.get("content", str(amb))
                send_telegram(bot_token, chat_id,
                    f"⚠️ <b>MRAV — ambiguous item needs clarification:</b>\n{content}\n\n"
                    f"Reply to route it manually.")

            # Send routing summary to Telegram
            routed_lines = []
            for item, ok, msg_r in routing_results:
                icon = "✅" if ok else "❌"
                routed_lines.append(f"{icon} {msg_r}")

            routing_block = "\n".join(routed_lines) if routed_lines else "No items to route."

            msg = format_extraction_message(extraction, call, duration_str, time_short)
            msg += f"\n\n<b>Routed:</b>\n{routing_block}"
            send_telegram(bot_token, chat_id, msg)
            print(f"[call-watcher] Extracted, routed, and notified: {call_id}")

        except json.JSONDecodeError as e:
            print(f"[call-watcher] JSON parse error for {call_id}: {e}")
            send_telegram(bot_token, chat_id,
                f"⚠️ Call watcher: extraction failed for {call_id}\nError: Claude returned non-JSON")
        except Exception as e:
            print(f"[call-watcher] Error processing {call_id}: {e}")
            send_telegram(bot_token, chat_id,
                f"⚠️ Call watcher: extraction failed for {call_id}\nError: {e}")

        processed.add(call_id)

    state["processed_call_ids"] = list(processed)
    save_state(state)


if __name__ == "__main__":
    main()
