#!/usr/bin/env python3
"""
pideo-daemon.py — Watches for video analysis requests, extracts metadata +
transcript via yt-dlp, then sends the context to an LLM API and returns results.

Protocol (dirs default to ~/.pideo, override with PIDEO_DIR):
  Signal:  <PIDEO_DIR>/current               — contains UUID of the current request
  Request: <PIDEO_DIR>/in/<uuid>/request.json — { id, url, instruction }
  Result:  <PIDEO_DIR>/out/<uuid>/result.json  — { success, content, error, cost }

Usage:
  python3 pideo-daemon.py [--once]
  Requires: python3, yt-dlp (https://github.com/yt-dlp/yt-dlp)

Env vars:
  PIDEO_DIR          state dir (default ~/.pideo)
  PIDEO_MODEL        model (default google/gemini-2.5-flash)
  PIDEO_API_BASE     API base URL (default OpenRouter chat completions)
  OPENROUTER_API_KEY API key (required)
"""

import argparse
import json
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError

PIDIR = Path(os.environ.get("PIDEO_DIR", str(Path.home() / ".pideo")))
INBOX = PIDIR / "in"
OUTBOX = PIDIR / "out"
SIGNAL = PIDIR / "current"
POLL_INTERVAL = 0.3
API_TIMEOUT = 120

# Config — all paths env-overridable (no hardcoded user paths)
MODEL = os.environ.get("PIDEO_MODEL", "google/gemini-2.5-flash")
API_BASE = os.environ.get("PIDEO_API_BASE", "https://openrouter.ai/api/v1/chat/completions")


def log(msg: str):
    ts = time.strftime("%H:%M:%S")
    print(f"[pideo] {ts} {msg}", flush=True)


def extract_video_context(url: str) -> dict:
    """Use yt-dlp to extract metadata and subtitles from a video URL."""
    info = {}
    subs_path = None

    # Step 1: Get metadata
    log(f"  Fetching video metadata...")
    result = subprocess.run(
        ["yt-dlp", "--dump-json", "--no-download", url],
        capture_output=True, text=True, timeout=60,
    )
    if result.returncode != 0:
        log(f"  WARN: yt-dlp metadata failed: {result.stderr[:200]}")
        return {"error": result.stderr[:300]}

    data = json.loads(result.stdout)
    info["title"] = data.get("title", "")
    info["description"] = data.get("description", "") or ""
    info["duration"] = data.get("duration", 0)
    info["channel"] = data.get("channel", data.get("uploader", ""))
    info["channel_url"] = data.get("channel_url", "")
    info["upload_date"] = data.get("upload_date", "")
    info["view_count"] = data.get("view_count", 0)
    info["tags"] = data.get("tags", []) or []
    info["categories"] = data.get("categories", []) or []
    info["webpage_url"] = data.get("webpage_url", url)

    log(f"  Title: {info['title']}")
    log(f"  Duration: {info['duration']}s")
    log(f"  Channel: {info['channel']}")

    # Step 2: Try getting subtitles
    log(f"  Fetching subtitles...")
    try:
        # yt-dlp can output subtitles to stdout
        sub_result = subprocess.run(
            [
                "yt-dlp", "--skip-download", "--write-auto-subs",
                "--sub-langs", "en", "--convert-subs", "srt",
                "--output", str(PIDIR / "subs" / "%(id)s.%(ext)s"),
                "--print", "after_video:filepath",
                url,
            ],
            capture_output=True, text=True, timeout=60,
        )
        if sub_result.returncode == 0:
            for line in sub_result.stdout.strip().split("\n"):
                line = line.strip()
                if line and line.endswith(".srt"):
                    subs_path = line
                    break
            if subs_path:
                with open(subs_path, encoding="utf-8", errors="replace") as f:
                    info["subtitles"] = f.read()
                log(f"  Subtitles: {len(info['subtitles'])} chars")
                # Clean up sub file
                try:
                    os.unlink(subs_path)
                except Exception:
                    pass
            else:
                info["subtitles"] = ""
                log(f"  No subtitles found")
        else:
            info["subtitles"] = ""
            log(f"  No subtitles available")
    except Exception as e:
        info["subtitles"] = ""
        log(f"  Subtitle fetch error: {e}")

    # Step 3: Truncate description if too long
    if len(info["description"]) > 2000:
        info["description"] = info["description"][:2000] + "\n...[truncated]"

    # Step 4: Truncate subtitles if too long
    if len(info.get("subtitles", "")) > 50000:
        info["subtitles"] = info["subtitles"][:50000] + "\n...[truncated]"

    return info


class AnalysisAPI:
    """Sends video context to Gemini via OpenRouter for analysis."""

    def __init__(self, api_key: str):
        self.api_key = api_key

    def analyze(self, instruction: str, context: dict) -> tuple[str | None, dict | None]:
        """Send video context + user instruction to Gemini for analysis."""

        # Build a rich context prompt
        parts = [f"## Video Metadata\n"]
        parts.append(f"Title: {context.get('title', 'N/A')}")
        parts.append(f"Channel: {context.get('channel', 'N/A')}")
        parts.append(f"Duration: {context.get('duration', 0)} seconds")
        parts.append(f"Views: {context.get('view_count', 'N/A')}")
        parts.append(f"Uploaded: {context.get('upload_date', 'N/A')}")
        if context.get("tags"):
            parts.append(f"Tags: {', '.join(context['tags'][:20])}")
        if context.get("categories"):
            parts.append(f"Categories: {', '.join(context['categories'][:10])}")

        desc = context.get("description", "")
        if desc:
            parts.append(f"\n## Description\n{desc}")

        subs = context.get("subtitles", "")
        if subs:
            parts.append(f"\n## Transcript/Captions\n{subs}")

        parts.append(f"\n## User Request\n{instruction}")
        parts.append(
            "\nBased on the video metadata, description, and transcript above, "
            "respond to the user's request."
        )

        full_prompt = "\n".join(parts)

        # OpenAI-compatible format for OpenRouter
        payload = {
            "model": MODEL,
            "messages": [
                {
                    "role": "system",
                    "content": (
                        "You are a video analysis assistant. You analyze YouTube/TikTok videos "
                        "using their metadata, description, and transcript. Provide thorough, "
                        "accurate analysis based on the available information."
                    ),
                },
                {"role": "user", "content": full_prompt},
            ],
            "temperature": 0.3,
            "max_tokens": 8192,
        }

        payload_json = json.dumps(payload).encode("utf-8")
        log(f"  Sending analysis request ({len(full_prompt)} chars)...")

        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}",
            "User-Agent": "pideo/1.0",
        }

        try:
            req = Request(API_BASE, data=payload_json, headers=headers, method="POST")
            with urlopen(req, timeout=API_TIMEOUT) as response:
                resp_data = json.loads(response.read().decode("utf-8"))

            choices = resp_data.get("choices", [])
            if not choices:
                log(f"  ERROR: no choices in response")
                return None, None

            result = choices[0].get("message", {}).get("content", "") or ""
            usage = resp_data.get("usage", {})
            cost_info = {
                "model": resp_data.get("model", MODEL),
                "provider": resp_data.get("provider", ""),
                "tokens": usage.get("total_tokens", 0),
                "prompt_tokens": usage.get("prompt_tokens", 0),
                "completion_tokens": usage.get("completion_tokens", 0),
                "cost": usage.get("cost", 0),
            }
            log(f"  Response: {len(result)} chars, cost: ${cost_info['cost']:.8f}")
            return result, cost_info

        except URLError as e:
            log(f"  ERROR: API request failed: {e}")
            if hasattr(e, "read"):
                try:
                    error_body = e.read().decode("utf-8", errors="replace")
                    log(f"  Error body: {error_body[:500]}")
                except Exception:
                    pass
            return None
        except json.JSONDecodeError as e:
            log(f"  ERROR: failed to parse response: {e}")
            return None
        except Exception as e:
            log(f"  ERROR: unexpected error: {e}")
            return None


def process_request(api: AnalysisAPI, uuid: str):
    """Process a single request identified by uuid."""
    req_file = INBOX / uuid / "request.json"
    out_dir = OUTBOX / uuid
    result_file = out_dir / "result.json"

    if not req_file.exists():
        log(f"WARN: {uuid} has no request.json, skipping")
        _clear_signal()
        return

    out_dir.mkdir(parents=True, exist_ok=True)

    try:
        with open(req_file) as f:
            req = json.load(f)
    except Exception as e:
        log(f"ERROR: failed to parse {req_file}: {e}")
        _write_result(result_file, success=False, error=f"Invalid request: {e}")
        _clear_signal()
        return

    instruction = req.get("instruction", "Describe this video in detail")
    url = req.get("url", "")

    log(f"→ Processing {uuid}")
    log(f"   URL: {url}")
    log(f"   Instruction: {instruction[:100]}")

    # Step 1: Extract video context via yt-dlp
    context = extract_video_context(url)

    if "error" in context:
        log(f"✗ Failed to extract video info for {uuid}")
        _write_result(
            result_file,
            success=False,
            error=f"Could not extract video info: {context['error']}",
        )
        _clear_signal()
        return

    # Step 2: Analyze via Gemini
    response, cost_info = api.analyze(instruction, context)

    if response is None:
        log(f"✗ Analysis failed for {uuid}")
        _write_result(result_file, success=False, error="Analysis API returned no response")
    else:
        log(f"✓ Done: {uuid} ({len(response)} chars)")
        _write_result(result_file, success=True, content=response, cost=cost_info)

    _clear_signal()


def _write_result(path: Path, success: bool, content: str = "", error: str = "", cost: dict | None = None):
    result = {
        "success": success,
        "content": content,
        "error": error,
    }
    if cost:
        result["cost"] = cost
    path.write_text(json.dumps(result, indent=2))


def _clear_signal():
    try:
        SIGNAL.unlink(missing_ok=True)
    except Exception:
        pass


def main():
    parser = argparse.ArgumentParser(description="pideo video agent daemon")
    parser.add_argument("--once", action="store_true", help="Process one request and exit")
    args = parser.parse_args()

    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key:
        log("FATAL: OPENROUTER_API_KEY environment variable not set")
        sys.exit(1)

    # Ensure directories exist
    INBOX.mkdir(parents=True, exist_ok=True)
    OUTBOX.mkdir(parents=True, exist_ok=True)
    (PIDIR / "subs").mkdir(parents=True, exist_ok=True)

    log(f"🎬 pideo daemon starting")
    log(f"   Model: {MODEL} (via OpenRouter)")
    log(f"   Watch: {SIGNAL}")
    log(f"   Mode:  {'single-shot' if args.once else 'continuous'}")
    print("───", flush=True)

    api = AnalysisAPI(api_key=api_key)

    def shutdown(signum, frame):
        log("Shutting down...")
        sys.exit(0)

    signal.signal(signal.SIGINT, shutdown)
    signal.signal(signal.SIGTERM, shutdown)

    try:
        if args.once:
            if SIGNAL.exists():
                uuid = SIGNAL.read_text().strip()
                process_request(api, uuid)
        else:
            log(f"Watching {SIGNAL} (poll {POLL_INTERVAL}s)...")
            while True:
                if SIGNAL.exists():
                    uuid = SIGNAL.read_text().strip()
                    if uuid:
                        process_request(api, uuid)
                time.sleep(POLL_INTERVAL)

    except Exception as e:
        log(f"FATAL: {e}")
        raise


if __name__ == "__main__":
    main()
