"""
Plaud MCP Server — Model Context Protocol server for Plaud Note recordings.

Exposes tools to list recordings, fetch transcripts, pull AI summaries,
search, and export from your Plaud Note device via the unofficial web API.

Usage:
    mcp-server-plaud                  # Run as MCP server (stdio transport)

Environment variables (via .env file or Claude Desktop config):
    PLAUD_TOKEN       — Full bearer token including "bearer " prefix
    PLAUD_API_DOMAIN  — API base URL (default: https://api.plaud.ai)
"""

from __future__ import annotations

import gzip
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

import httpx
from mcp.server.fastmcp import FastMCP
from pydantic import Field

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

DEFAULT_API_DOMAIN = "https://api.plaud.ai"
MAX_RETRIES = 3
INITIAL_RETRY_DELAY = 1.0
REQUEST_TIMEOUT = 30.0

# Browser-like headers required by the Plaud API (403 without them)
BROWSER_HEADERS = {
    "Origin": "https://web.plaud.ai",
    "Referer": "https://web.plaud.ai/",
    "Accept": "application/json, text/plain, */*",
    "User-Agent": (
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/131.0.0.0 Safari/537.36"
    ),
}


# ---------------------------------------------------------------------------
# .env loader
# ---------------------------------------------------------------------------

def load_env() -> dict[str, str]:
    """Load .env from common locations."""
    search_paths = [
        Path.cwd() / ".env",
        Path.home() / ".config" / "plaud" / ".env",
        Path(__file__).resolve().parent.parent / ".env",
        Path.home() / ".claude" / "skills" / "plaud-transcripts" / ".env",  # legacy
    ]

    for p in search_paths:
        if p.exists():
            env = {}
            with open(p) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        key, _, value = line.partition("=")
                        env[key.strip()] = value.strip()
            return env
    return {}


# ---------------------------------------------------------------------------
# Load credentials at startup
# ---------------------------------------------------------------------------

_env = load_env()
_raw_token = _env.get("PLAUD_TOKEN") or os.environ.get("PLAUD_TOKEN", "")
API_DOMAIN = (
    _env.get("PLAUD_API_DOMAIN")
    or os.environ.get("PLAUD_API_DOMAIN")
    or DEFAULT_API_DOMAIN
).rstrip("/")

# Strip "bearer " prefix — we add "Bearer " in the auth header ourselves
TOKEN = _raw_token[7:] if _raw_token.lower().startswith("bearer ") else _raw_token


# ---------------------------------------------------------------------------
# Async API helpers
# ---------------------------------------------------------------------------

async def _plaud_request(
    endpoint: str,
    method: str = "GET",
    body: dict | None = None,
    retries: int = 0,
) -> dict:
    """Make an authenticated request to the Plaud API with retry logic."""
    if not TOKEN:
        return {"error": "No PLAUD_TOKEN configured. Set it in your Claude Desktop config env block or in ~/.config/plaud/.env"}

    url = f"{API_DOMAIN}{endpoint}"
    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json",
        **BROWSER_HEADERS,
    }

    try:
        async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
            response = await client.request(
                method, url, headers=headers,
                content=json.dumps(body) if body else None,
            )
            response.raise_for_status()
            return response.json()

    except httpx.HTTPStatusError as e:
        status = e.response.status_code

        # Retry on 429 / 5xx
        if (status == 429 or 500 <= status < 600) and retries < MAX_RETRIES:
            retry_after = e.response.headers.get("Retry-After")
            delay = int(retry_after) if retry_after else INITIAL_RETRY_DELAY * (2 ** retries)
            import asyncio
            await asyncio.sleep(delay)
            return await _plaud_request(endpoint, method, body, retries + 1)

        # Try to parse error body
        try:
            err = e.response.json()
            msg = err.get("msg", e.response.reason_phrase)
        except Exception:
            msg = e.response.reason_phrase or str(status)

        if status == 401:
            return {"error": f"Authentication failed ({status}): {msg}. Your token may be expired — re-extract from web.plaud.ai."}
        if status == 403:
            return {"error": f"Forbidden ({status}): {msg}. Make sure browser-like headers are present."}
        return {"error": f"Plaud API error ({status}): {msg}"}

    except httpx.TimeoutException:
        if retries < MAX_RETRIES:
            import asyncio
            await asyncio.sleep(INITIAL_RETRY_DELAY * (2 ** retries))
            return await _plaud_request(endpoint, method, body, retries + 1)
        return {"error": "Request timed out after retries. Plaud servers may be slow."}

    except Exception as e:
        return {"error": f"Network error: {type(e).__name__}: {e}"}


# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------

def _ms_to_datetime(ms: int) -> str:
    if not ms:
        return "N/A"
    return datetime.fromtimestamp(ms / 1000).strftime("%Y-%m-%d %I:%M %p %Z")


def _ms_to_duration(ms: int) -> str:
    if not ms:
        return "N/A"
    total_secs = ms // 1000
    hours, remainder = divmod(total_secs, 3600)
    minutes, seconds = divmod(remainder, 60)
    if hours:
        return f"{hours}h {minutes}m {seconds}s"
    if minutes:
        return f"{minutes}m {seconds}s"
    return f"{seconds}s"


def _format_recording_row(r: dict) -> str:
    """Format a single recording as a markdown row."""
    name = r.get("filename") or "Untitled"
    date = _ms_to_datetime(r.get("start_time", 0))
    dur = _ms_to_duration(r.get("duration", 0))
    has_t = "Yes" if r.get("is_trans") else "No"
    has_s = "Yes" if r.get("is_summary") else "No"
    rid = r.get("id", "?")
    return f"| {name} | {date} | {dur} | {has_t} | {has_s} | `{rid}` |"


# ---------------------------------------------------------------------------
# Content fetching helpers (S3 download + decompress)
# ---------------------------------------------------------------------------

async def _fetch_content_link(url: str) -> dict | list | None:
    """Download a pre-signed S3 URL, decompress gzip, and parse JSON."""
    try:
        async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
            resp = await client.get(url)
            resp.raise_for_status()
            raw = resp.content
            # httpx may auto-decompress gzip; try manual decompress first, fall back to raw
            try:
                data = gzip.decompress(raw)
            except Exception:
                data = raw
            return json.loads(data)
    except Exception as e:
        print(f"[plaud] Failed to fetch content link: {e}", file=sys.stderr)
        return None


def _format_ms_timestamp(ms: int) -> str:
    """Format milliseconds as MM:SS."""
    total_secs = ms // 1000
    minutes, seconds = divmod(total_secs, 60)
    return f"{minutes:02d}:{seconds:02d}"


async def _extract_transcript(content_list: list) -> str | None:
    """Find and download the transcript from content_list."""
    for item in content_list:
        if item.get("data_type") == "transaction" and item.get("task_status") == 1:
            url = item.get("data_link")
            if not url:
                continue
            segments = await _fetch_content_link(url)
            if not segments or not isinstance(segments, list):
                return None
            lines = []
            for seg in segments:
                speaker = seg.get("speaker") or seg.get("original_speaker") or "Unknown"
                content = seg.get("content", "")
                ts = _format_ms_timestamp(seg.get("start_time", 0))
                lines.append(f"**{speaker}** ({ts}): {content}")
            return "\n\n".join(lines) if lines else None
    return None


async def _extract_summary(content_list: list) -> str | None:
    """Find and download the AI summary from content_list."""
    for item in content_list:
        if item.get("data_type") == "auto_sum_note" and item.get("task_status") == 1:
            url = item.get("data_link")
            if not url:
                continue
            data = await _fetch_content_link(url)
            if not data or not isinstance(data, dict):
                return None
            return data.get("ai_content")
    return None


# ---------------------------------------------------------------------------
# Initialize MCP server
# ---------------------------------------------------------------------------

mcp = FastMCP(
    "plaud_mcp",
    instructions=(
        "Use these tools to access Plaud Note recordings, transcripts, and AI summaries. "
        "These tools call the Plaud API directly — do NOT open a browser or navigate to web.plaud.ai. "
        "When a user asks about Plaud recordings, meeting notes from Plaud, or transcripts, "
        "call plaud_list_recordings first, then use the recording IDs to fetch transcripts or summaries."
    ),
)


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------

@mcp.tool(
    name="plaud_list_recordings",
    annotations={
        "title": "List Plaud Recordings",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_list_recordings(
    limit: int = Field(default=50, description="Maximum number of recordings to return (default 50)", ge=1, le=10000),
    offset: int = Field(default=0, description="Number of recordings to skip for pagination", ge=0),
) -> str:
    """List all recordings from your Plaud Note device via API (no browser needed).

    ALWAYS use this tool first when the user asks about Plaud recordings, transcripts,
    or meeting notes. Returns a table with recording IDs that you then pass to
    plaud_get_transcript or plaud_get_summary to fetch content.

    Args:
        limit: Max recordings to return (default 50).
        offset: Skip N recordings for pagination.

    Returns:
        str: Markdown table of recordings with IDs, or error message.
    """
    resp = await _plaud_request(
        f"/file/simple/web?skip={offset}&limit={limit}"
        f"&is_trash=0&sort_by=edit_time&is_desc=true"
    )

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

    recordings = resp.get("data_file_list", [])
    total = resp.get("data_file_total", len(recordings))

    if not recordings:
        return "No recordings found."

    lines = [
        f"**{total} total recording(s)** — showing {len(recordings)}",
        "",
        "| Filename | Date | Duration | Transcript | Summary | ID |",
        "|----------|------|----------|------------|---------|-----|",
    ]
    for r in recordings:
        lines.append(_format_recording_row(r))

    if total > offset + len(recordings):
        next_offset = offset + len(recordings)
        lines.append(f"\n*More available — use offset={next_offset} to see next page.*")

    return "\n".join(lines)


@mcp.tool(
    name="plaud_get_transcript",
    annotations={
        "title": "Get Plaud Transcript",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_get_transcript(
    file_id: str = Field(description="The recording's unique ID (UUID format). Get IDs from plaud_list_recordings.", min_length=1),
) -> str:
    """Fetch the full transcript text for a Plaud recording via API (no browser needed).

    Returns the complete transcript with speaker diarization (if available).
    Call plaud_list_recordings first to get recording IDs, then pass the ID here.

    Args:
        file_id: The recording's unique ID (UUID).

    Returns:
        str: Full transcript text, or error/not-available message.
    """
    resp = await _plaud_request(f"/file/detail/{file_id}")

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

    data = resp.get("data", resp)
    name = data.get("file_name") or data.get("filename") or "Untitled"
    date = _ms_to_datetime(data.get("start_time", 0))
    dur = _ms_to_duration(data.get("duration", 0))
    transcript = await _extract_transcript(data.get("content_list", []))

    if not transcript:
        return (
            f"**{name}** ({date}, {dur})\n\n"
            "No transcript available for this recording. "
            "You may need to trigger transcription in the Plaud app first."
        )

    return f"**{name}** — {date} — {dur}\n\n---\n\n{transcript}"


@mcp.tool(
    name="plaud_get_summary",
    annotations={
        "title": "Get Plaud AI Summary",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_get_summary(
    file_id: str = Field(description="The recording's unique ID (UUID format). Get IDs from plaud_list_recordings.", min_length=1),
) -> str:
    """Fetch the AI-generated summary for a Plaud recording via API (no browser needed).

    Returns the summary produced by Plaud's AI analysis.
    Call plaud_list_recordings first to get recording IDs, then pass the ID here.

    Args:
        file_id: The recording's unique ID (UUID).

    Returns:
        str: AI summary text, or error/not-available message.
    """
    resp = await _plaud_request(f"/file/detail/{file_id}")

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

    data = resp.get("data", resp)
    name = data.get("file_name") or data.get("filename") or "Untitled"
    date = _ms_to_datetime(data.get("start_time", 0))
    dur = _ms_to_duration(data.get("duration", 0))
    summary = await _extract_summary(data.get("content_list", []))

    if not summary:
        return (
            f"**{name}** ({date}, {dur})\n\n"
            "No AI summary available. The recording may not have been processed yet."
        )

    return f"**{name}** — {date} — {dur}\n\n---\n\n{summary}"


@mcp.tool(
    name="plaud_get_recording_details",
    annotations={
        "title": "Get Full Plaud Recording Details",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_get_recording_details(
    file_id: str = Field(description="The recording's unique ID (UUID format). Get IDs from plaud_list_recordings.", min_length=1),
) -> str:
    """Get complete details for a Plaud recording via API (no browser needed).

    Returns everything in one call: filename, date, duration, device info, full
    transcript, and AI summary. Best when you need both transcript and summary.
    Call plaud_list_recordings first to get recording IDs, then pass the ID here.

    Args:
        file_id: The recording's unique ID (UUID).

    Returns:
        str: Markdown-formatted recording details, or error message.
    """
    resp = await _plaud_request(f"/file/detail/{file_id}")

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

    data = resp.get("data", resp)
    name = data.get("file_name") or data.get("filename") or "Untitled"
    date = _ms_to_datetime(data.get("start_time", 0))
    dur = _ms_to_duration(data.get("duration", 0))
    device = data.get("serial_number", "Unknown")
    rid = data.get("file_id") or data.get("id") or "?"

    lines = [
        f"# {name}",
        "",
        f"**Date:** {date}",
        f"**Duration:** {dur}",
        f"**Device:** {device}",
        f"**ID:** `{rid}`",
    ]

    content_list = data.get("content_list", [])
    transcript = await _extract_transcript(content_list)
    if transcript:
        lines.extend(["", "## Transcript", "", transcript])
    else:
        lines.extend(["", "*No transcript available.*"])

    summary = await _extract_summary(content_list)
    if summary:
        lines.extend(["", "## AI Summary", "", summary])
    else:
        lines.extend(["", "*No AI summary available.*"])

    return "\n".join(lines)


@mcp.tool(
    name="plaud_search_recordings",
    annotations={
        "title": "Search Plaud Recordings",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_search_recordings(
    query: str = Field(description="Search term to match against recording filenames and keywords", min_length=1, max_length=200),
    limit: int = Field(default=50, description="Maximum results to return", ge=1, le=10000),
) -> str:
    """Search Plaud recordings by filename or keywords via API (no browser needed).

    Searches recording filenames and auto-extracted keywords for matches.
    Returns matching recordings in a table format with IDs for use with other tools.

    Args:
        query: Search term to match.
        limit: Max results (default 50).

    Returns:
        str: Markdown table of matching recordings, or no-results message.
    """
    query = query.strip()
    if not query:
        return "Error: Query cannot be empty or whitespace."

    resp = await _plaud_request(
        f"/file/simple/web?skip=0&limit={limit}"
        f"&is_trash=0&sort_by=edit_time&is_desc=true"
    )

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

    recordings = resp.get("data_file_list", [])
    query_lower = query.lower()

    matches = [
        r for r in recordings
        if query_lower in (r.get("filename") or "").lower()
        or query_lower in " ".join(r.get("keywords") or []).lower()
    ]

    if not matches:
        return f"No recordings found matching '{query}'."

    lines = [
        f"**{len(matches)} recording(s) matching '{query}':**",
        "",
        "| Filename | Date | Duration | Transcript | Summary | ID |",
        "|----------|------|----------|------------|---------|-----|",
    ]
    for r in matches:
        lines.append(_format_recording_row(r))

    return "\n".join(lines)


@mcp.tool(
    name="plaud_list_devices",
    annotations={
        "title": "List Plaud Devices",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def plaud_list_devices() -> str:
    """List connected Plaud Note devices via API (no browser needed).

    Returns device names, serial numbers, and model info.

    Returns:
        str: Markdown list of devices, or error message.
    """
    resp = await _plaud_request("/device/list")

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

    devices = resp.get("data_devices", [])
    if not devices:
        return "No Plaud devices found."

    lines = [f"**{len(devices)} device(s):**", ""]
    for d in devices:
        name = d.get("name", "Unknown")
        sn = d.get("sn", "?")
        model = d.get("model", "?")
        lines.append(f"- **{name}** — SN: `{sn}`, Model: {model}")

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    """Entry point for the mcp-server-plaud command."""
    mcp.run()


if __name__ == "__main__":
    main()
