#!/usr/bin/env python3
"""Read-only Google Drive helper for gotta."""

from __future__ import annotations

import argparse
import json
import re
import shlex
import shutil
import signal
import subprocess
import sys
import urllib.parse

from gotta.helptext import is_long_help_request, print_long_help
from gotta.routing import query_route, strip_http_url_fragment
from gotta.source import derive_source_metadata_from_payload, render_source_metadata_lines
from gotta.providers.google import (
    DEFAULT_DRIVE_FIELDS,
    GOOGLE_DOC_MIME,
    OAUTH_DIR,
    TOKEN_FILE,
    GoogleError,
    drive_download_bytes,
    drive_export,
    drive_file_meta,
    drive_search_files,
    ensure_google_session,
    escape_drive_query,
    google_status_payload,
    parse_drive_ref,
    run_oauth_bootstrap,
)


DEFAULT_SEARCH_LIMIT = 10
TEXTLIKE_MIME_PREFIXES = (
    "text/",
    "application/json",
    "application/xml",
    "application/javascript",
    "application/x-javascript",
    "application/x-python-code",
    "application/x-sh",
    "application/x-shellscript",
)


def die(message: str, code: int = 2) -> int:
    print(message, file=sys.stderr)
    return code


def is_interactive() -> bool:
    return sys.stdin.isatty() and sys.stderr.isatty()


def _slug(value: str, *, fallback: str) -> str:
    value = value.strip().lower()
    value = re.sub(r"[^a-z0-9._-]+", "-", value)
    return value.strip("-") or fallback


def _output_extension(output: str) -> str:
    return {
        "markdown": "md",
        "summary": "summary",
        "text": "txt",
        "html": "html",
        "meta": "json",
        "json": "json",
        "raw": "bin",
    }.get(output, "md")


def _parse_cli(argv: list[str]) -> argparse.Namespace:
    return build_parser().parse_args(argv)


def canonical_locator(argv: list[str]) -> str:
    args = _parse_cli(argv)
    if args.command == "get":
        file_id, _ = parse_drive_ref(args.ref)
        return f"gdrive:{file_id}"
    if args.command == "status":
        return "gdrive:status"
    return f"gdrive:{shlex.join(argv)}"


def preferred_name(argv: list[str], options: object) -> str:
    if getattr(options, "save_as", ""):
        return str(getattr(options, "save_as"))
    args = _parse_cli(argv)
    if args.command == "get":
        file_id, _ = parse_drive_ref(args.ref)
        return f"{file_id}.{_output_extension(args.output)}"
    if args.command == "search":
        return f"gdrive-search-{_slug(args.query, fallback='gdrive')}.{_output_extension(args.output)}"
    if args.command == "status":
        return f"gdrive.{_output_extension(args.output)}"
    return "gdrive.txt"


def route_target(target: str) -> list[str] | None:
    if target.startswith("gdrive:search "):
        return query_route(
            "search",
            target.removeprefix("gdrive:search "),
            valued_flags=("--limit", "--mode", "--mime-type", "--output"),
        )
    if target.startswith("gdrive:"):
        return ["get", target.removeprefix("gdrive:")]
    if any(char.isspace() for char in target):
        return None
    parsed = urllib.parse.urlparse(target)
    if parsed.scheme not in {"http", "https"}:
        return None
    if parsed.netloc == "drive.google.com":
        return ["get", strip_http_url_fragment(target)]
    if parsed.netloc == "docs.google.com":
        parts = [part for part in parsed.path.split("/") if part]
        if "d" in parts and not (parts and parts[0] in {"document", "spreadsheets"}):
            return ["get", strip_http_url_fragment(target)]
    return None


def html_to_markdown(data: bytes) -> bytes:
    pandoc = shutil.which("pandoc")
    if not pandoc:
        raise GoogleError("pandoc is required for markdown conversion but is not installed")
    proc = subprocess.run(
        [pandoc, "-f", "html", "-t", "gfm", "--wrap=none"],
        input=data,
        capture_output=True,
        check=False,
    )
    if proc.returncode != 0:
        detail = proc.stderr.decode("utf-8", errors="replace").strip()
        raise GoogleError(detail or "pandoc failed to convert Google Drive HTML")
    return proc.stdout


def drive_file_summary(meta: dict[str, object]) -> str:
    file_id = str(meta.get("id") or "")
    title = str(meta.get("name") or "(untitled)")
    url = str(meta.get("webViewLink") or "")
    mime_type = str(meta.get("mimeType") or "")
    created = str(meta.get("createdTime") or "")
    modified = str(meta.get("modifiedTime") or "")
    size = str(meta.get("size") or "")
    owners = meta.get("owners")
    owner_names: list[str] = []
    if isinstance(owners, list):
        for owner in owners:
            if isinstance(owner, dict):
                name = str(owner.get("displayName") or "").strip()
                if name:
                    owner_names.append(name)
    lines = [f"# {title}", ""]
    if url:
        lines.append(f"- **URL:** {url}")
    if file_id:
        lines.append(f"- **Drive ID:** `{file_id}`")
    if mime_type:
        lines.append(f"- **MIME type:** `{mime_type}`")
    if created:
        lines.append(f"- **Created:** {created}")
    if modified:
        lines.append(f"- **Modified:** {modified}")
    if size:
        lines.append(f"- **Size:** {size} bytes")
    if owner_names:
        lines.append(f"- **Owners:** {', '.join(f'`{owner}`' for owner in owner_names)}")
    if mime_type == GOOGLE_DOC_MIME:
        lines.extend(
            [
                "",
                "This is a native Google Doc. `--output markdown`, `text`, and `html`",
                "will export the document body through Drive.",
            ]
        )
    else:
        lines.extend(
            [
                "",
                "Readable output will render text-like files directly when possible.",
                "Use `--output raw` to download file bytes for non-text files.",
            ]
        )
    return "\n".join(lines) + "\n"


def is_textlike_mime_type(mime_type: str) -> bool:
    normalized = mime_type.strip().lower()
    return any(
        normalized == prefix or normalized.startswith(prefix)
        for prefix in TEXTLIKE_MIME_PREFIXES
    )


def normalize_search_result(item: dict[str, object], *, matched_by: set[str]) -> dict[str, object]:
    owners = item.get("owners")
    owner_names: list[str] = []
    if isinstance(owners, list):
        for owner in owners:
            if isinstance(owner, dict):
                display_name = str(owner.get("displayName") or "").strip()
                if display_name:
                    owner_names.append(display_name)
    return {
        "fileId": str(item.get("id") or ""),
        "title": str(item.get("name") or "(untitled)"),
        "url": str(item.get("webViewLink") or ""),
        "createdTime": str(item.get("createdTime") or ""),
        "modifiedTime": str(item.get("modifiedTime") or ""),
        "mimeType": str(item.get("mimeType") or ""),
        "owners": owner_names,
        "ownerDisplayName": owner_names[0] if owner_names else "",
        "matchedBy": sorted(matched_by),
    }


def merge_search_results(
    title_files: list[dict[str, object]],
    content_files: list[dict[str, object]],
    *,
    limit: int,
) -> list[dict[str, object]]:
    merged: dict[str, dict[str, object]] = {}
    for source, files in (("title", title_files), ("content", content_files)):
        for item in files:
            file_id = str(item.get("id") or "").strip()
            if not file_id:
                continue
            current = merged.get(file_id)
            if current is None:
                merged[file_id] = normalize_search_result(item, matched_by={source})
                continue
            matched_by = set(current.get("matchedBy") or [])
            matched_by.add(source)
            current["matchedBy"] = sorted(matched_by)
            if not current.get("url") and item.get("webViewLink"):
                current["url"] = str(item.get("webViewLink") or "")
            if not current.get("createdTime") and item.get("createdTime"):
                current["createdTime"] = str(item.get("createdTime") or "")
            if not current.get("modifiedTime") and item.get("modifiedTime"):
                current["modifiedTime"] = str(item.get("modifiedTime") or "")
            if not current.get("mimeType") and item.get("mimeType"):
                current["mimeType"] = str(item.get("mimeType") or "")
            if not current.get("owners"):
                normalized = normalize_search_result(item, matched_by=matched_by)
                current["owners"] = normalized["owners"]
                current["ownerDisplayName"] = normalized["ownerDisplayName"]

    def sort_key(item: dict[str, object]) -> tuple[int, str, str]:
        matched_by = set(item.get("matchedBy") or [])
        if matched_by == {"title", "content"}:
            rank = 2
        elif "title" in matched_by:
            rank = 1
        else:
            rank = 0
        return (
            rank,
            str(item.get("modifiedTime") or ""),
            str(item.get("title") or "").lower(),
        )

    return sorted(merged.values(), key=sort_key, reverse=True)[:limit]


def search_files(
    access_token: str,
    query: str,
    *,
    limit: int,
    mode: str,
    mime_type: str,
) -> dict[str, object]:
    escaped = escape_drive_query(query)
    mime_clause = f" and mimeType='{escape_drive_query(mime_type)}'" if mime_type else ""
    fields = "id,name,mimeType,createdTime,modifiedTime,webViewLink,owners(displayName)"
    title_files: list[dict[str, object]] = []
    content_files: list[dict[str, object]] = []
    if mode in {"title", "auto"}:
        title_files = drive_search_files(
            access_token,
            f"trashed=false{mime_clause} and name contains '{escaped}'",
            limit=limit,
            fields=fields,
        )
    if mode in {"content", "auto"}:
        content_files = drive_search_files(
            access_token,
            f"trashed=false{mime_clause} and fullText contains '{escaped}'",
            limit=limit,
            fields=fields,
        )
    results = merge_search_results(title_files, content_files, limit=limit)
    return {
        "query": query,
        "mode": mode,
        "mimeType": mime_type,
        "source": "google-drive",
        "resultCount": len(results),
        "results": results,
    }


def export_document(access_token: str, file_id: str, output: str) -> bytes:
    if output == "html":
        return drive_export(access_token, file_id, "text/html")
    if output == "text":
        return drive_export(access_token, file_id, "text/plain")
    try:
        return drive_export(access_token, file_id, "text/markdown")
    except GoogleError:
        try:
            return html_to_markdown(drive_export(access_token, file_id, "text/html"))
        except GoogleError:
            return drive_export(access_token, file_id, "text/plain")


def cmd_auth(args: argparse.Namespace) -> int:
    if args.full:
        oauth_state = run_oauth_bootstrap(interactive_ok=is_interactive())
    else:
        oauth_state = ensure_google_session(
            allow_bootstrap=True,
            interactive_ok=is_interactive(),
            auth_command="gdrive",
        )
    expires_at = oauth_state.get("expires_at")
    print(
        json.dumps(
            {
                "authenticated": True,
                "token_file": str(TOKEN_FILE),
                "oauth_dir": str(OAUTH_DIR),
                "expires_at": expires_at,
            },
            indent=2,
        )
    )
    return 0


def cmd_status(args: argparse.Namespace) -> int:
    payload = google_status_payload()
    payload["surface"] = "gdrive"
    if args.output == "json":
        print(json.dumps(payload, indent=2, sort_keys=True))
        return 0
    sys.stdout.write(
        "\n".join(
            [
                "surface\tgdrive",
                f"credentials_configured\t{str(bool(payload['credentialsConfigured'])).lower()}",
                f"session_status\t{payload['sessionStatus']}",
                f"expires_at\t{payload.get('expiresAt') or ''}",
                f"has_refresh_token\t{str(bool(payload.get('hasRefreshToken'))).lower()}",
                f"token_file\t{payload['tokenFile']}",
                f"next_step\t{payload.get('nextStep') or ''}",
            ]
        )
        + "\n"
    )
    return 0


def cmd_get(args: argparse.Namespace) -> int:
    oauth_state = ensure_google_session(
        allow_bootstrap=True,
        interactive_ok=is_interactive(),
        auth_command="gdrive",
    )
    access_token = str(oauth_state.get("access_token") or "").strip()
    file_id, canonical_url = parse_drive_ref(args.ref)
    meta = drive_file_meta(access_token, file_id, fields=DEFAULT_DRIVE_FIELDS)
    meta.setdefault("webViewLink", canonical_url)
    mime_type = str(meta.get("mimeType") or "")
    if args.output == "json":
        print(json.dumps(meta, indent=2, sort_keys=True))
        return 0
    if args.output == "meta":
        print(json.dumps(meta, indent=2, sort_keys=True))
        return 0
    if args.output == "raw":
        if mime_type == GOOGLE_DOC_MIME:
            raise GoogleError(
                "raw download is not available for native Google Docs; use "
                "--output markdown, text, html, meta, or json"
            )
        sys.stdout.buffer.write(drive_download_bytes(access_token, file_id))
        return 0
    if mime_type == GOOGLE_DOC_MIME:
        sys.stdout.buffer.write(export_document(access_token, file_id, args.output))
        return 0
    if args.output == "html":
        if mime_type == "text/html":
            sys.stdout.buffer.write(drive_download_bytes(access_token, file_id))
            return 0
        raise GoogleError(
            f"{mime_type or 'this file'} is not exportable as html; "
            "use --output raw, meta, json, or markdown"
        )
    if args.output in {"markdown", "text"} and is_textlike_mime_type(mime_type):
        data = drive_download_bytes(access_token, file_id)
        if args.output == "markdown" and mime_type == "text/html":
            try:
                data = html_to_markdown(data)
            except GoogleError:
                pass
        sys.stdout.buffer.write(data)
        return 0
    if args.output == "text":
        raise GoogleError(
            f"{mime_type or 'this file'} is not directly readable as text; "
            "use --output raw, meta, json, or markdown"
        )
    sys.stdout.write(drive_file_summary(meta))
    return 0


def render_search_markdown(payload: dict[str, object]) -> str:
    results = payload.get("results")
    if not isinstance(results, list) or not results:
        return (
            f"### Google Drive Search: {payload.get('query') or ''}\n\n"
            f"- _Mode_: `{payload.get('mode') or 'auto'}`\n"
            "- _Matches_: 0\n\n"
            "No Google Drive files matched.\n"
        )
    lines: list[str] = [
        f"### Google Drive Search: {payload.get('query') or ''}",
        "",
        f"- _Mode_: `{payload.get('mode') or 'auto'}`",
        f"- _Matches_: {payload.get('resultCount') or len(results)}",
    ]
    lines.extend(render_source_metadata_lines(derive_source_metadata_from_payload(payload)))
    lines.append("")
    for item in results:
        if not isinstance(item, dict):
            continue
        title = str(item.get("title") or "(untitled)")
        url = str(item.get("url") or "")
        created = str(item.get("createdTime") or "")
        modified = str(item.get("modifiedTime") or "")
        owner = str(item.get("ownerDisplayName") or "")
        mime_type = str(item.get("mimeType") or "")
        matched_by = [str(value) for value in item.get("matchedBy") or [] if str(value)]
        line = f"- [{title}]({url})"
        details: list[str] = []
        if matched_by:
            details.append(f"matched by `{'+'.join(matched_by)}`")
        if mime_type:
            details.append(f"mime `{mime_type}`")
        if created:
            details.append(f"created `{created}`")
        if modified:
            details.append(f"modified `{modified}`")
        if owner:
            details.append(f"owner `{owner}`")
        if details:
            line += " - " + ", ".join(details)
        lines.append(line)
    return "\n".join(lines) + "\n"


def cmd_search(args: argparse.Namespace) -> int:
    oauth_state = ensure_google_session(
        allow_bootstrap=True,
        interactive_ok=is_interactive(),
        auth_command="gdrive",
    )
    access_token = str(oauth_state.get("access_token") or "").strip()
    payload = search_files(
        access_token,
        args.query,
        limit=args.limit,
        mode=args.mode,
        mime_type=args.mime_type,
    )
    if args.output == "json":
        print(json.dumps(payload, indent=2, sort_keys=True))
        return 0
    sys.stdout.write(render_search_markdown(payload))
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="gotta gdrive",
        description=(
            "Read-only Google Drive helper. Uses shared Google OAuth state and "
            "the Drive API for file metadata, document export, and binary download."
        ),
    )
    sub = parser.add_subparsers(dest="command", required=True)

    p = sub.add_parser(
        "auth",
        help="ensure local Google OAuth state is usable; refresh silently when possible",
    )
    p.add_argument(
        "--full",
        action="store_true",
        help="force a full browser OAuth bootstrap instead of reusing or refreshing cached state",
    )
    p.set_defaults(func=cmd_auth)

    p = sub.add_parser("status", help="inspect local Google OAuth readiness for Drive access")
    p.add_argument("--output", choices=["json", "summary"], default="summary")
    p.set_defaults(func=cmd_status)

    p = sub.add_parser("get", help="fetch one Google Drive file by URL or file ID")
    p.add_argument("ref", help="Google Drive URL or file ID")
    p.add_argument(
        "--output",
        choices=["markdown", "text", "html", "meta", "json", "raw"],
        default="markdown",
        help="render format for get; defaults to markdown",
    )
    p.set_defaults(func=cmd_get)

    p = sub.add_parser("search", help="search Google Drive by title or indexed content")
    p.add_argument("query", help="search query")
    p.add_argument("--limit", type=int, default=DEFAULT_SEARCH_LIMIT)
    p.add_argument(
        "--mode",
        choices=["auto", "title", "content"],
        default="auto",
        help="search titles, indexed body content, or both; defaults to auto",
    )
    p.add_argument(
        "--mime-type",
        default="",
        help="optional Drive MIME filter, e.g. application/vnd.google-apps.document",
    )
    p.add_argument("--output", choices=["markdown", "json"], default="markdown")
    p.set_defaults(func=cmd_search)

    return parser


def main(argv: list[str]) -> int:
    parser = build_parser()
    if is_long_help_request(argv):
        return print_long_help(parser)
    args = parser.parse_args(argv)
    try:
        return int(args.func(args))
    except BrokenPipeError:
        return 0
    except GoogleError as exc:
        return die(str(exc), code=1)


if __name__ == "__main__":
    if hasattr(signal, "SIGPIPE"):
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    raise SystemExit(main(sys.argv[1:]))
