"""
FrankenClaw — Google Drive (read-only)

Two tools for reading the user's Google Drive:
    gdrive_search  — find files by name/content
    gdrive_read    — read a file's contents (Docs/Sheets/Slides auto-export
                     to text/csv/plain; plain-text files download directly;
                     binary files are refused)

Uses the OAuth token at ~/.openclaw/google/.gdrive-server-credentials.json
produced by the MCP gdrive server's `auth` subcommand. Override paths with
the `FRANKENCLAW_GDRIVE_TOKEN_PATH` and `FRANKENCLAW_GDRIVE_CLIENT_SECRET_PATH`
env vars if you store credentials elsewhere. Scope is read-only.
No write code paths exist in this module — deliberate, read-only by design.

If the token is missing or expired, the user re-runs:
    npx -y @modelcontextprotocol/server-gdrive auth
and the refreshed token lands back in the same path.
"""

import asyncio
import json
import os
from pathlib import Path

from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


_DEFAULT_TOKEN = Path.home() / ".openclaw" / "google" / ".gdrive-server-credentials.json"
_DEFAULT_CLIENT_SECRET = Path.home() / ".openclaw" / "google" / "credentials.json"
TOKEN_PATH = Path(os.environ.get("FRANKENCLAW_GDRIVE_TOKEN_PATH", _DEFAULT_TOKEN))
CLIENT_SECRET_PATH = Path(os.environ.get("FRANKENCLAW_GDRIVE_CLIENT_SECRET_PATH", _DEFAULT_CLIENT_SECRET))
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]

EXPORT_MAP = {
    "application/vnd.google-apps.document": ("text/markdown", ".md"),
    "application/vnd.google-apps.spreadsheet": ("text/csv", ".csv"),
    "application/vnd.google-apps.presentation": ("text/plain", ".txt"),
    "application/vnd.google-apps.drawing": ("image/png", ".png"),
}

DIRECT_READ_MIME_PREFIXES = ("text/", "application/json", "application/xml")


def _load_credentials() -> Credentials:
    """Load + refresh OAuth credentials from the saved token file."""
    if not TOKEN_PATH.exists():
        raise FileNotFoundError(
            f"No gdrive token at {TOKEN_PATH}. Run: "
            f"npx -y @modelcontextprotocol/server-gdrive auth"
        )
    with open(TOKEN_PATH) as f:
        data = json.load(f)

    client_info = {}
    if CLIENT_SECRET_PATH.exists():
        with open(CLIENT_SECRET_PATH) as f:
            client_info = json.load(f).get("installed", {})

    creds = Credentials(
        token=data.get("access_token"),
        refresh_token=data.get("refresh_token"),
        token_uri=client_info.get("token_uri", "https://oauth2.googleapis.com/token"),
        client_id=client_info.get("client_id"),
        client_secret=client_info.get("client_secret"),
        scopes=SCOPES,
    )

    if creds.expired and creds.refresh_token:
        creds.refresh(Request())
        _persist_refreshed_token(creds, data)

    return creds


def _persist_refreshed_token(creds: Credentials, original: dict) -> None:
    """Write the refreshed access_token back so subsequent calls don't re-refresh."""
    original["access_token"] = creds.token
    if creds.expiry:
        original["expiry_date"] = int(creds.expiry.timestamp() * 1000)
    tmp = TOKEN_PATH.with_suffix(TOKEN_PATH.suffix + ".tmp")
    with open(tmp, "w") as f:
        json.dump(original, f)
    os.chmod(tmp, 0o600)
    os.replace(tmp, TOKEN_PATH)


def _service():
    return build("drive", "v3", credentials=_load_credentials(), cache_discovery=False)


def _ok(data) -> str:
    return json.dumps({"ok": True, "data": data}, indent=2, default=str)


def _err(msg: str) -> str:
    return json.dumps({"ok": False, "error": msg}, indent=2)


def _sync_search(query: str, max_results: int):
    svc = _service()
    escaped = query.replace("'", "\\'")
    q = f"fullText contains '{escaped}' or name contains '{escaped}'"
    resp = svc.files().list(
        q=q,
        pageSize=max_results,
        fields="files(id,name,mimeType,modifiedTime,size,webViewLink)",
        spaces="drive",
    ).execute()
    return resp.get("files", [])


def _sync_read(file_id: str):
    svc = _service()
    meta = svc.files().get(
        fileId=file_id,
        fields="id,name,mimeType,modifiedTime,size,webViewLink",
    ).execute()
    mime = meta.get("mimeType", "")

    if mime in EXPORT_MAP:
        export_mime, _ext = EXPORT_MAP[mime]
        content_bytes = svc.files().export(fileId=file_id, mimeType=export_mime).execute()
        content = content_bytes.decode("utf-8", errors="replace") if isinstance(content_bytes, bytes) else str(content_bytes)
        return {"metadata": meta, "export_mime": export_mime, "content": content}

    if any(mime.startswith(p) for p in DIRECT_READ_MIME_PREFIXES):
        content_bytes = svc.files().get_media(fileId=file_id).execute()
        content = content_bytes.decode("utf-8", errors="replace") if isinstance(content_bytes, bytes) else str(content_bytes)
        return {"metadata": meta, "content": content}

    return {
        "metadata": meta,
        "error": f"Refused: mimeType {mime!r} is binary or unsupported for read_only export. Open via webViewLink instead.",
    }


async def gdrive_search(query: str, max_results: int = 10) -> str:
    """
    Search the user's Google Drive for files matching a query.

    Args:
        query: Free-text query. Matches filenames and full-text content.
        max_results: Cap on results (default 10, max 100).

    Returns:
        JSON with a list of {id, name, mimeType, modifiedTime, size, webViewLink}.
        File IDs from this tool are what gdrive_read expects.
    """
    try:
        max_results = min(max(1, max_results), 100)
        files = await asyncio.to_thread(_sync_search, query, max_results)
        return _ok(files)
    except FileNotFoundError as e:
        return _err(str(e))
    except HttpError as e:
        return _err(f"Drive API error: {e}")
    except Exception as e:
        return _err(f"{type(e).__name__}: {e}")


async def gdrive_read(file_id: str) -> str:
    """
    Read the contents of a Google Drive file.

    Google Docs export to markdown. Sheets export to CSV. Slides export to
    plain text. Plain-text/JSON/XML files download directly. Binary files
    (images, PDFs, videos, unknown) are refused — the metadata and a
    webViewLink are returned instead so the user can open them manually.

    Args:
        file_id: Drive file ID (from gdrive_search results, or pasted from
                 a share link — the part after /d/ and before /edit).

    Returns:
        JSON with {metadata, content} on success, or {metadata, error}
        when the file is binary/unsupported.
    """
    try:
        result = await asyncio.to_thread(_sync_read, file_id)
        return _ok(result)
    except FileNotFoundError as e:
        return _err(str(e))
    except HttpError as e:
        return _err(f"Drive API error: {e}")
    except Exception as e:
        return _err(f"{type(e).__name__}: {e}")
