#!/usr/bin/env python3
"""confluence-cli — Confluence wiki from the terminal.

Interact with Atlassian Confluence Cloud via the REST API.
Requires CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN env vars
(token from https://id.atlassian.com/manage/api-tokens) and
CONFLUENCE_SERVER (defaults to https://your-domain.atlassian.net).
"""

import argparse
import json
import os
import sys
import warnings
from typing import Any, Dict, List, Optional, Tuple

warnings.simplefilter("ignore")

import requests

# === Config ===
DEFAULT_SERVER = "https://your-domain.atlassian.net"
ENV_EMAIL = os.getenv("CONFLUENCE_EMAIL", "")
ENV_TOKEN = os.getenv("CONFLUENCE_API_TOKEN", "")
ENV_SERVER = os.getenv("CONFLUENCE_SERVER", DEFAULT_SERVER)

# === Logging ===
QUIET = False


def log(msg: str) -> None:
    if not QUIET and not GLOBAL_FLAGS.get("json", False):
        print(msg)


def warn(msg: str) -> None:
    print(f"Warning: {msg}", file=sys.stderr)


def die(msg: str, exit_code: int = 1) -> None:
    print(f"Error: {msg}", file=sys.stderr)
    sys.exit(exit_code)


def emit(human: str, data: Any) -> None:
    if GLOBAL_FLAGS.get("json", False):
        print(json.dumps(data, default=str))
    else:
        print(human)


# === Global flags ===
GLOBAL_FLAGS: Dict[str, Any] = {
    "json": False, "dry_run": False, "force": False,
    "quiet": False, "verbose": False
}


def _preparse_global_flags(argv: List[str]) -> Tuple[Dict[str, Any], List[str]]:
    GLOBAL_BOOLS = {"--json", "--dry-run", "--force", "--quiet", "--verbose"}
    flags: Dict[str, Any] = {}
    filtered: List[str] = [argv[0]]
    i = 1
    while i < len(argv):
        arg = argv[i]
        if arg in GLOBAL_BOOLS:
            flags[arg.lstrip("-").replace("-", "_")] = True
            i += 1
        elif arg in ("--help", "-h"):
            return flags, argv
        elif arg == "--":
            filtered.extend(argv[i:])
            break
        else:
            filtered.append(arg)
            i += 1
    return flags, filtered


# === Confluence API Client ===
class ConfluenceClient:
    """REST API client for Atlassian Confluence Cloud."""

    def __init__(self, email: str = "", token: str = "",
                 server: str = "", dry_run: bool = False):
        self.email = email or ENV_EMAIL
        self.token = token or ENV_TOKEN
        self.server = (server or ENV_SERVER).rstrip("/")
        self.dry_run = dry_run

    def _auth(self) -> Optional[Tuple[str, str]]:
        if self.email and self.token:
            return (self.email, self.token)
        return None

    def _request(self, method: str, path: str,
                 params: Optional[Dict] = None,
                 json_data: Any = None) -> Any:
        url = f"{self.server}{path}"

        if self.dry_run:
            return {"dry_run": True, "method": method.upper(),
                    "url": url, "params": params, "json": json_data}

        auth = self._auth()
        if not auth:
            die("CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN not set. "
                "Get a token at https://id.atlassian.com/manage/api-tokens")

        try:
            resp = requests.request(
                method=method, url=url, auth=auth,
                params=params, json=json_data,
                headers={"Accept": "application/json",
                         "Content-Type": "application/json"},
                timeout=30
            )
        except requests.ConnectionError as e:
            die(f"Cannot connect to {self.server}: {e}\n"
                f"  Check CONFLUENCE_SERVER or use --server")

        if resp.status_code == 401:
            die("Auth failed (401). Check CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN.")
        if resp.status_code == 403:
            die("Forbidden (403). Your account may not have access.")
        if resp.status_code == 404:
            return None
        if resp.status_code >= 400:
            try:
                detail = resp.json()
            except Exception:
                detail = resp.text[:200]
            die(f"API error ({resp.status_code}): {detail}")

        try:
            return resp.json()
        except ValueError:
            return {"raw": resp.text[:500]}

    def _get(self, path: str, params: Optional[Dict] = None) -> Any:
        return self._request("GET", path, params=params)

    def _post(self, path: str, json_data: Any = None) -> Any:
        return self._request("POST", path, json_data=json_data)

    # === Endpoints (v2 API) ===

    def get_current_user(self) -> Any:
        """Get current user via the /rest/api endpoint."""
        return self._get("/wiki/rest/api/user/current")

    def list_spaces(self, limit: int = 25) -> Any:
        """List spaces (v2 API)."""
        return self._get("/wiki/api/v2/spaces", params={"limit": limit})

    def get_space(self, space_key: str) -> Any:
        """Get a space by key (v2 API)."""
        return self._get(f"/wiki/api/v2/spaces", params={"keys": space_key})

    def list_pages(self, space_id: Optional[str] = None,
                   limit: int = 25, sort: str = "-modified-date") -> Any:
        """List pages (v2 API), optionally filtered by space."""
        params: Dict[str, Any] = {"limit": limit, "sort": sort}
        if space_id:
            params["spaceId"] = space_id
        return self._get("/wiki/api/v2/pages", params=params)

    def get_page(self, page_id: str) -> Any:
        """Get a page by ID (v2 API) with body."""
        return self._get(f"/wiki/api/v2/pages/{page_id}",
                         params={"body-format": "storage"})

    def search(self, cql: str, limit: int = 20) -> Any:
        """Search content with CQL (rest API)."""
        return self._get("/wiki/rest/api/search",
                         params={"cql": cql, "limit": limit, "expand": "space"})

    def create_page(self, space_id: str, title: str,
                    body_html: str = "", parent_id: Optional[str] = None) -> Any:
        """Create a page (v2 API)."""
        payload: Dict[str, Any] = {
            "spaceId": space_id,
            "title": title,
        }
        if body_html:
            payload["body"] = {
                "representation": "storage",
                "value": body_html
            }
        if parent_id:
            payload["parentId"] = parent_id
        return self._post("/wiki/api/v2/pages", json_data=payload)


# === Command Handlers ===

def cmd_me(client: ConfluenceClient, args: List[str]) -> None:
    if client.dry_run:
        emit("[dry-run] Would fetch current user profile",
             {"dry_run": True, "command": "me"})
        return
    data = client.get_current_user()
    if not data:
        emit("Could not fetch user info.", {"error": "not found"})
        return
    emit(
        f"👤 {data.get('displayName', '?')}\n"
        f"   Username: {data.get('username', data.get('userKey', '?'))}\n"
        f"   Email: {data.get('email', '?')}\n"
        f"   Account: {data.get('accountId', '?')}",
        {"displayName": data.get("displayName"),
         "username": data.get("username"),
         "email": data.get("email"),
         "accountId": data.get("accountId"),
         "timezone": data.get("timeZone")}
    )


def cmd_spaces(client: ConfluenceClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="confluence-cli spaces")
    parser.add_argument("--limit", type=int, default=25, help="Max results")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit("[dry-run] Would list spaces",
             {"dry_run": True, "command": "spaces"})
        return

    data = client.list_spaces(limit=parsed.limit)
    results = data.get("results", []) if data else []

    if not results:
        emit("No spaces found.", {"spaces": []})
        return

    lines = []
    out = []
    for s in results:
        sid = s.get("id", "?")
        key = s.get("key", "?")
        name = s.get("name", "?")
        stype = s.get("type", "?")
        status = s.get("status", "?")
        homepage = s.get("homepageId", "")
        lines.append(f"  {key:12} {name:45} [{stype}]  id={sid}{' (has homepage)' if homepage else ''}")
        out.append({"id": sid, "key": key, "name": name,
                     "type": stype, "status": status,
                     "homepage_id": homepage})

    total = data.get("size", len(results))
    emit(f"{total} space(s):\n" + "\n".join(lines),
         {"total": total, "spaces": out})


def cmd_pages(client: ConfluenceClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="confluence-cli pages")
    parser.add_argument("--space", help="Space key (e.g. DEV)")
    parser.add_argument("--limit", type=int, default=25, help="Max results")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit("[dry-run] Would list pages",
             {"dry_run": True, "command": "pages",
              "space": parsed.space, "limit": parsed.limit})
        return

    # Resolve space key to ID if needed
    space_id = None
    if parsed.space:
        space_data = client.get_space(parsed.space)
        if space_data:
            results = space_data.get("results", [])
            if results:
                space_id = results[0].get("id")

    data = client.list_pages(space_id=space_id, limit=parsed.limit)
    results = data.get("results", []) if data else []

    if not results:
        emit("No pages found.", {"pages": []})
        return

    lines = []
    out = []
    for p in results:
        pid = p.get("id", "?")
        title = p.get("title", "?")
        status = p.get("status", "?")
        version = p.get("version", {}).get("number", "?")
        space = p.get("_links", {}).get("space", parsed.space or "?")
        lines.append(f"  {pid:8} {title:55} v{version} [{status}]  space={space}")
        out.append({
            "id": pid, "title": title, "status": status,
            "version": version, "space": space
        })

    total = data.get("size", len(results))
    emit(f"{total} page(s):\n" + "\n".join(lines),
         {"total": total, "pages": out})


def cmd_view(client: ConfluenceClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="confluence-cli view")
    parser.add_argument("page_id", help="Page ID (numeric)")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would fetch page {parsed.page_id}",
             {"dry_run": True, "command": "view",
              "page_id": parsed.page_id})
        return

    data = client.get_page(parsed.page_id)
    if not data:
        emit(f"Page {parsed.page_id} not found.",
             {"error": "not found", "page_id": parsed.page_id})
        return

    title = data.get("title", "?")
    status = data.get("status", "?")
    version = data.get("version", {}).get("number", "?")
    space_id = data.get("spaceId", "?")
    created = data.get("createdAt", "?")
    updated = (data.get("version") or {}).get("createdAt", "?")
    author = (data.get("version") or {}).get("author", {}).get("displayName", "?")
    body_data = data.get("body", {})
    body_html = ""

    # Extract body — try storage format first
    storage = body_data.get("storage", {})
    if storage:
        body_html = storage.get("value", "")
    else:
        view_data = body_data.get("view", {})
        if view_data:
            body_html = view_data.get("value", "")

    # Strip HTML tags for a plain-text preview
    import re
    body_text = re.sub(r"<[^>]+>", "", body_html) if body_html else "(no body)"
    # Truncate for display
    if len(body_text) > 1000:
        body_text = body_text[:1000] + "\n[... truncated]"

    human = (
        f"📄 {title}\n"
        f"   Status: {status}  v{version}  Space: {space_id}\n"
        f"   Author: {author}  Updated: {updated}\n"
        f"   Created: {created}\n"
        f"─── Body ({len(body_html)} chars) ───\n{body_text}"
    )
    emit(human, {
        "id": parsed.page_id, "title": title, "status": status,
        "version": version, "space_id": space_id,
        "author": author, "created": created, "updated": updated,
        "body_length": len(body_html),
        "body_html": body_html[:5000] if body_html else ""
    })


def cmd_search(client: ConfluenceClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="confluence-cli search")
    parser.add_argument("--cql", required=True, help="CQL query (e.g. 'text~\"deploy\"')")
    parser.add_argument("--limit", type=int, default=20, help="Max results")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would search: {parsed.cql}",
             {"dry_run": True, "command": "search",
              "cql": parsed.cql, "limit": parsed.limit})
        return

    data = client.search(parsed.cql, limit=parsed.limit)
    results = data.get("results", []) if data else []

    if not results:
        emit("No results found.", {"results": []})
        return

    lines = []
    out = []
    for r in results:
        title = r.get("title", "?")
        url = r.get("url", r.get("_links", {}).get("webui", ""))
        excerpt = r.get("excerpt", "")
        space = r.get("space", {})
        space_name = space.get("name", "?") if space else "?"
        lines.append(f"  {title:50}  [{space_name}]")
        if excerpt:
            # Strip highlights
            excerpt_clean = excerpt.replace("@@@hl@@@", "").replace("@@@endhl@@@", "")
            lines.append(f"  {'':4}{excerpt_clean[:120]}")
        out.append({
            "title": title, "url": url,
            "excerpt": excerpt, "space": space_name
        })

    total = data.get("totalSize", len(results))
    emit(f"{total} result(s):\n" + "\n".join(lines),
         {"total": total, "results": out})


def cmd_create(client: ConfluenceClient, args: List[str]) -> None:
    parser = argparse.ArgumentParser(prog="confluence-cli create")
    parser.add_argument("--space", required=True, help="Space key (e.g. DEV)")
    parser.add_argument("--title", required=True, help="Page title")
    parser.add_argument("--body", default="", help="HTML body content")
    parser.add_argument("--parent", help="Parent page ID")
    parsed, _ = parser.parse_known_args(args)

    if client.dry_run:
        emit(f"[dry-run] Would create page '{parsed.title}' in space {parsed.space}",
             {"dry_run": True, "command": "create",
              "space": parsed.space, "title": parsed.title,
              "parent_id": parsed.parent})
        return

    # Resolve space key to ID
    space_data = client.get_space(parsed.space)
    if not space_data:
        die(f"Space '{parsed.space}' not found.")
    results = space_data.get("results", [])
    if not results:
        die(f"Space '{parsed.space}' not found.")
    space_id = results[0].get("id")
    space_key = results[0].get("key", parsed.space)

    result = client.create_page(
        space_id=space_id,
        title=parsed.title,
        body_html=parsed.body,
        parent_id=parsed.parent or None
    )
    pid = result.get("id", "?") if result else "?"
    title = result.get("title", parsed.title) if result else parsed.title
    emit(f"✅ Created page: {title} (id={pid})",
         {"status": "created", "id": pid, "title": title,
          "space": space_key})


# === Main ===

def main() -> None:
    global GLOBAL_FLAGS, QUIET
    GLOBAL_FLAGS, filtered_argv = _preparse_global_flags(sys.argv)
    if GLOBAL_FLAGS.get("quiet", False):
        QUIET = True
    if GLOBAL_FLAGS.get("json", False):
        warnings.simplefilter("ignore")

    parser = argparse.ArgumentParser(
        prog="confluence-cli",
        description="Confluence wiki from the terminal.",
        epilog="Global flags work anywhere: confluence-cli --json spaces"
    )
    sub = parser.add_subparsers(dest="command", help="Available commands")

    sub.add_parser("me", help="Get current user profile")
    sub.add_parser("spaces", help="List spaces")

    p_pages = sub.add_parser("pages", help="List pages")
    p_pages.add_argument("--space", help="Space key filter")
    p_pages.add_argument("--limit", type=int, default=25, help="Max results")

    p_view = sub.add_parser("view", help="View a page by ID")
    p_view.add_argument("page_id", help="Page ID (numeric)")

    p_search = sub.add_parser("search", help="Search with CQL")
    p_search.add_argument("--cql", required=True, help="CQL query")
    p_search.add_argument("--limit", type=int, default=20, help="Max results")

    p_create = sub.add_parser("create", help="Create a page")
    p_create.add_argument("--space", required=True, help="Space key")
    p_create.add_argument("--title", required=True, help="Page title")
    p_create.add_argument("--body", default="", help="HTML body content")
    p_create.add_argument("--parent", help="Parent page ID")

    args = parser.parse_args(filtered_argv[1:])
    if not args.command:
        parser.print_help()
        sys.exit(1)

    needs_auth = not GLOBAL_FLAGS.get("dry_run", False)
    if needs_auth and not (ENV_EMAIL and ENV_TOKEN):
        die("Set CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN.\n"
            "  Get a token: https://id.atlassian.com/manage/api-tokens\n"
            "  Or use --dry-run to preview without credentials.")

    client = ConfluenceClient(dry_run=GLOBAL_FLAGS.get("dry_run", False))

    cmd_map = {
        "me": cmd_me,
        "spaces": cmd_spaces,
        "pages": cmd_pages,
        "view": cmd_view,
        "search": cmd_search,
        "create": cmd_create,
    }
    handler = cmd_map.get(args.command)
    if not handler:
        parser.print_help()
        sys.exit(1)

    remaining = filtered_argv[filtered_argv.index(args.command) + 1:]
    handler(client, remaining)


if __name__ == "__main__":
    main()
