# VidContext MCP Server — wraps the VidContext API for use in Claude Desktop, Cursor, and Claude Code.

import asyncio
import os
import tempfile
from pathlib import Path

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    "vidcontext",
    instructions=(
        "Video Intelligence API — analyze any video and get detailed text descriptions "
        "or expert framework analysis. This server runs LOCALLY on the user's machine "
        "and can access any local file path. When the user wants to analyze a video, "
        "ask them for the full file path on their computer (e.g. ~/Downloads/video.mp4). "
        "Do NOT ask them to upload the file into the chat — ask for the path instead. "
        "The tool also accepts URLs to videos hosted online. "
        "IMPORTANT: When analyze_video returns results, you MUST present the COMPLETE "
        "output to the user exactly as returned. Do not summarize, truncate, or condense "
        "the output. The full detailed analysis is what the user is paying for. "
        "Use analyze_video to process videos, check_credits to verify balance."
    ),
)

SUPPORTED_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".flv", ".wmv"}
MAX_POLL_SECONDS = 600
MAX_FILE_BYTES = 500 * 1024 * 1024  # 500MB


def _get_api_url() -> str:
    """Get API base URL from environment."""
    return os.getenv("VIDCONTEXT_API_URL", "https://api.vidcontext.com")


def _headers() -> dict[str, str]:
    """Build request headers with API key authentication."""
    key = os.getenv("VIDCONTEXT_API_KEY", "")
    if not key:
        raise ValueError(
            "VIDCONTEXT_API_KEY not set. "
            "Get your API key at https://www.vidcontext.com/app/developer"
        )
    return {"X-API-Key": key}


async def _api_get(endpoint: str) -> dict:
    """Authenticated GET request to VidContext API."""
    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.get(f"{_get_api_url()}{endpoint}", headers=_headers())
        resp.raise_for_status()
        return resp.json()


async def _upload_video(file_path: str, output_format: str) -> dict:
    """Upload a video file to the VidContext API for processing."""
    path = Path(file_path)
    filename = path.name

    # Guard against uploading files that exceed the API limit
    file_size = path.stat().st_size
    if file_size > MAX_FILE_BYTES:
        return {"error": f"File too large ({file_size // (1024 * 1024)}MB). Max 500MB."}

    # Scale timeout with file size: 30s base + 1s per MB
    write_timeout = max(60.0, 30.0 + file_size / (1024 * 1024))
    upload_timeout = httpx.Timeout(connect=10.0, read=300.0, write=write_timeout, pool=30.0)

    async with httpx.AsyncClient(timeout=upload_timeout) as client:
        with open(file_path, "rb") as f:
            resp = await client.post(
                f"{_get_api_url()}/v1/analyze",
                headers=_headers(),
                files={"file": (filename, f)},
                data={"output_format": output_format},
            )

        if resp.status_code == 400:
            detail = resp.json().get("detail", "Bad request")
            return {"error": detail}
        if resp.status_code == 402:
            return {"error": "Insufficient credits. Buy more at https://www.vidcontext.com"}
        if resp.status_code == 413:
            return {"error": "Video file too large (max 500MB for paid users)."}
        if resp.status_code == 429:
            return {"error": "Rate limited. Wait a moment and try again."}
        if resp.status_code == 503:
            return {"error": "System is busy. Try again in a few minutes."}

        resp.raise_for_status()
        return resp.json()


async def _poll_job(job_id: str) -> dict:
    """Poll job status until complete, failed, or timeout."""
    elapsed = 0
    interval = 3

    while elapsed < MAX_POLL_SECONDS:
        await asyncio.sleep(interval)
        elapsed += interval

        try:
            result = await _api_get(f"/v1/status/{job_id}")
        except httpx.HTTPStatusError as exc:
            # Fail fast on auth errors — don't poll for 10 minutes with a bad key
            if exc.response.status_code in (401, 403):
                return {"status": "failed", "error": "Authentication failed. Check your API key."}
            # Retry on server errors (5xx)
            if exc.response.status_code >= 500:
                continue
            return {"status": "failed", "error": f"HTTP {exc.response.status_code}"}
        except httpx.HTTPError:
            # Retry on network/timeout errors
            continue

        if result.get("status") in ("complete", "failed"):
            return result

        # Gradually back off: 3s → 5s → 7s → 10s max
        interval = min(interval + 2, 10)

    return {"status": "timeout", "job_id": job_id}


async def _download_url(url: str) -> str:
    """Download a video from a URL to a temporary file using streaming."""
    url_path = Path(url.split("?")[0])
    ext = url_path.suffix.lower() if url_path.suffix else ".mp4"
    if ext not in SUPPORTED_EXTENSIONS:
        ext = ".mp4"

    tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    try:
        async with httpx.AsyncClient(
            timeout=httpx.Timeout(10.0, read=120.0),
            follow_redirects=True,
        ) as client:
            async with client.stream("GET", url) as resp:
                resp.raise_for_status()

                # Check Content-Length if available — reject oversized files early
                content_length = resp.headers.get("content-length")
                if content_length and int(content_length) > MAX_FILE_BYTES:
                    raise ValueError(f"Remote file too large ({int(content_length) // (1024 * 1024)}MB). Max 500MB.")

                downloaded = 0
                async for chunk in resp.aiter_bytes(chunk_size=65536):
                    downloaded += len(chunk)
                    if downloaded > MAX_FILE_BYTES:
                        raise ValueError("Remote file exceeds 500MB limit.")
                    tmp.write(chunk)

        tmp.close()
        return tmp.name
    except Exception:
        tmp.close()
        Path(tmp.name).unlink(missing_ok=True)
        raise


@mcp.tool()
async def analyze_video(
    file_path: str,
    output_format: str = "context",
) -> str:
    """Analyze a video and get a detailed text description or expert analysis.

    This tool runs LOCALLY and can read any file on the user's computer.
    Ask the user for the full file path (e.g. ~/Downloads/video.mp4) or a URL.
    Do NOT ask the user to upload files into chat — ask for the file path instead.
    Do NOT tell the user the server can't access local files — it can.

    Returns a detailed analysis based on the chosen mode. 7 modes available:
    - "context" — Detailed scene-by-scene description (default, works for any video)
    - "analysis" — Creator/social media analysis with 7 frameworks (hook, retention, CTA, etc.)
    - "ad" — Ad effectiveness scoring (message clarity, persuasion, legal compliance)
    - "ecommerce" — Product video analysis (visibility, features, purchase psychology)
    - "training" — Pedagogical effectiveness (cognitive load, scaffolding, engagement)
    - "ugc" — Creator vetting for brand partnerships (authenticity, brand safety, audience fit)
    - "competitor" — Competitive intelligence (messaging strategy, differentiation, threat score)

    IMPORTANT: Always present the COMPLETE returned output to the user without
    summarizing or truncating. The full analysis is what they are paying for.

    Processing typically takes 30-180 seconds depending on video length.
    Cost: 1 credit per minute of video (rounded up).

    Args:
        file_path: Absolute path to a local video file (mp4, mov, avi, mkv, webm) or a URL to a video
        output_format: Analysis mode — one of: context, analysis, ad, ecommerce, training, ugc, competitor
    """
    valid_modes = ("context", "analysis", "ad", "ecommerce", "training", "ugc", "competitor")
    if output_format not in valid_modes:
        return f"Error: output_format must be one of: {', '.join(valid_modes)}"

    temp_file = None

    try:
        # Handle URL input — stream download to temp file first
        if file_path.startswith(("http://", "https://")):
            try:
                temp_file = await _download_url(file_path)
            except httpx.HTTPError as exc:
                return f"Error downloading video: {exc}"
            except ValueError as exc:
                return f"Error: {exc}"
            upload_path = temp_file
        else:
            # Resolve symlinks and validate path
            path = Path(file_path).resolve()
            if not path.exists():
                return f"Error: File not found: {file_path}"
            if not path.is_file():
                return f"Error: Not a regular file: {file_path}"
            if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
                return (
                    f"Error: Unsupported format '{path.suffix}'. "
                    f"Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
                )
            upload_path = str(path)

        # Upload to VidContext API
        try:
            result = await _upload_video(upload_path, output_format)
        except httpx.HTTPStatusError as exc:
            return f"Error: Upload failed (HTTP {exc.response.status_code})"
        except httpx.HTTPError as exc:
            return f"Error: Upload failed — network error: {exc}"

        if "error" in result:
            return f"Error: {result['error']}"

        job_id = result["job_id"]

        # Poll until complete
        final = await _poll_job(job_id)

        if final["status"] == "complete":
            text = final.get("result", "")
            time_s = final.get("processing_time_seconds")
            header = "[FULL OUTPUT — present this complete text to the user, do not summarize]\n\n"
            footer = "\n\n---\n"
            if time_s:
                footer += f"Processed in {time_s:.1f} seconds. "
            footer += "1 credit per minute of video (rounded up)."
            return f"{header}{text}{footer}"

        if final["status"] == "failed":
            return f"Analysis failed: {final.get('error', 'Unknown error')}"

        return (
            f"Still processing after {MAX_POLL_SECONDS}s. "
            f"Check later with: check_job_status('{job_id}')"
        )

    finally:
        if temp_file:
            Path(temp_file).unlink(missing_ok=True)


@mcp.tool()
async def check_job_status(job_id: str) -> str:
    """Check the status of a video analysis job.

    Use this to check on a job that's still processing, or to retrieve
    results from a previous analysis.

    Args:
        job_id: The job ID returned from analyze_video
    """
    try:
        result = await _api_get(f"/v1/status/{job_id}")
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 404:
            return f"Job '{job_id}' not found. Jobs expire after 24 hours."
        return f"Error: HTTP {exc.response.status_code}"
    except httpx.HTTPError as exc:
        return f"Error: Network error — {exc}"
    except ValueError as exc:
        return str(exc)

    status = result.get("status", "unknown")

    if status == "complete":
        text = result.get("result", "No output.")
        time_s = result.get("processing_time_seconds")
        fmt = result.get("output_format", "unknown")
        header = f"Status: Complete ({fmt} mode)"
        if time_s:
            header += f" | {time_s:.1f}s"
        return f"{header}\n\n{text}"

    if status == "failed":
        return f"Status: Failed\nError: {result.get('error', 'Unknown error')}"

    if status == "processing":
        return "Status: Processing. Check again in 15-30 seconds."

    if status == "queued":
        return "Status: Queued. Waiting to start processing."

    return f"Status: {status}"


@mcp.tool()
async def check_credits() -> str:
    """Check your VidContext credit balance and usage limits.

    Shows current credits, tier, file size limits, and duration limits.
    """
    try:
        usage = await _api_get("/v1/usage")
    except httpx.HTTPStatusError as exc:
        return f"Error: HTTP {exc.response.status_code}"
    except httpx.HTTPError as exc:
        return f"Error: Network error — {exc}"
    except ValueError as exc:
        return str(exc)

    tier = usage.get("tier", "unknown")
    credits = usage.get("credits", 0)

    if tier == "credit":
        return (
            f"Credits: {credits}\n"
            f"Tier: Paid\n"
            f"Max file size: {usage.get('max_file_size_mb', 500)}MB\n"
            f"Max duration: {usage.get('max_duration_seconds', 900) // 60} minutes\n"
            f"Cost: 1 credit per minute of video (rounded up)"
        )

    remaining = usage.get("remaining", 0)
    return (
        f"Free uses remaining: {remaining} of {usage.get('limit', 5)}\n"
        f"Max file size: {usage.get('max_file_size_mb', 100)}MB\n"
        f"Max duration: {usage.get('max_duration_seconds', 60)}s\n"
        f"Get more credits at https://www.vidcontext.com"
    )


@mcp.tool()
async def get_account() -> str:
    """Get your VidContext account information.

    Shows email, credits, tier, and active subscription details.
    """
    try:
        me = await _api_get("/v1/me")
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 401:
            return "Invalid API key. Check your VIDCONTEXT_API_KEY."
        return f"Error: HTTP {exc.response.status_code}"
    except httpx.HTTPError as exc:
        return f"Error: Network error — {exc}"
    except ValueError as exc:
        return str(exc)

    if not me.get("authenticated"):
        return "Not authenticated. Set VIDCONTEXT_API_KEY in your MCP config."

    lines = [
        f"Email: {me.get('email', 'N/A')}",
        f"Name: {me.get('display_name', 'N/A')}",
        f"Credits: {me.get('credits', 0)}",
        f"Tier: {me.get('tier', 'N/A')}",
    ]

    sub = me.get("subscription")
    if sub:
        lines.extend([
            f"\nSubscription: {sub.get('plan', 'N/A').title()} ({sub.get('billing_interval', 'N/A')})",
            f"Credits/period: {sub.get('credits_per_period', 'N/A')}",
            f"Status: {sub.get('status', 'N/A')}",
            f"Renews: {sub.get('current_period_end', 'N/A')}",
        ])
    else:
        lines.append("\nNo active subscription")

    return "\n".join(lines)


def main():
    """Entry point for the VidContext MCP server."""
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
