#!/usr/bin/env python3
"""
clickup.py — ClickUp API CLI for OpenClaw
Full read/write access to ClickUp: spaces, projects, lists, tasks

Usage:
    python3 clickup.py --help
    python3 clickup.py spaces
    python3 clickup.py tasks [list_id] --status open
    python3 clickup.py create --list [id] --title "My Task"

Config: ~/.config/clickup/config.json OR env CLICKUP_API_KEY
"""

import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime
from pathlib import Path

# ─────────────────────────────────────────────
# Config / Auth
# ─────────────────────────────────────────────

CONFIG_PATHS = [
    Path.home() / ".config" / "clickup" / "config.json",
    Path(__file__).parent / "config.json",
]

BASE_URL = "https://api.clickup.com/api/v2"


def load_config() -> dict:
    """Load config from file or environment."""
    config = {}

    # Try config files in order
    for path in CONFIG_PATHS:
        if path.exists():
            with open(path) as f:
                config = json.load(f)
            break

    # Environment overrides
    if api_key := os.environ.get("CLICKUP_API_KEY"):
        config["api_key"] = api_key
    if ws_id := os.environ.get("CLICKUP_WORKSPACE_ID"):
        config["workspace_id"] = ws_id
    if space_id := os.environ.get("CLICKUP_SPACE_ID"):
        config["default_space_id"] = space_id

    return config


def get_api_key(config: dict) -> str:
    """Get API key or exit with helpful error."""
    key = config.get("api_key", "")
    if not key or key == "YOUR_API_KEY_HERE":
        print(json.dumps({
            "error": "No API key configured",
            "fix": "Set CLICKUP_API_KEY env var or add api_key to ~/.config/clickup/config.json",
            "docs": "Get your key at: https://app.clickup.com/settings/apps"
        }), file=sys.stderr)
        sys.exit(1)
    return key


# ─────────────────────────────────────────────
# HTTP Client
# ─────────────────────────────────────────────

def api_request(method: str, endpoint: str, api_key: str,
                data: dict = None, params: dict = None,
                retries: int = 3) -> dict:
    """Make ClickUp API request with retry + rate limit handling."""
    url = f"{BASE_URL}/{endpoint.lstrip('/')}"

    if params:
        url += "?" + urllib.parse.urlencode(params)

    headers = {
        "Authorization": api_key,
        "Content-Type": "application/json",
    }

    body = json.dumps(data).encode("utf-8") if data else None

    for attempt in range(retries):
        try:
            req = urllib.request.Request(url, data=body, headers=headers, method=method)
            with urllib.request.urlopen(req, timeout=30) as resp:
                raw = resp.read()
                if not raw:
                    return {}  # 204 No Content (e.g., DELETE success)
                return json.loads(raw.decode("utf-8"))

        except urllib.error.HTTPError as e:
            # 204 No Content is a success (used by DELETE)
            if e.code == 204:
                return {}

            body_bytes = e.read()
            try:
                err_body = json.loads(body_bytes.decode("utf-8"))
            except Exception:
                err_body = {"raw": body_bytes.decode("utf-8", errors="replace")}

            if e.code == 429:
                # Rate limited — check for Retry-After header
                retry_after = e.headers.get("Retry-After", "60")
                print(json.dumps({
                    "error": "Rate limited (429)",
                    "retry_after_seconds": int(retry_after),
                    "message": f"ClickUp rate limit hit. Try again in {retry_after}s.",
                }), file=sys.stderr)
                sys.exit(1)

            elif e.code == 401:
                print(json.dumps({
                    "error": "Authentication failed (401)",
                    "message": "Invalid API key. Check CLICKUP_API_KEY or config file.",
                    "detail": err_body,
                }), file=sys.stderr)
                sys.exit(1)

            elif e.code == 404:
                print(json.dumps({
                    "error": "Not found (404)",
                    "message": f"Resource not found: {endpoint}",
                    "detail": err_body,
                }), file=sys.stderr)
                sys.exit(1)

            elif e.code >= 500 and attempt < retries - 1:
                wait = 2 ** attempt
                time.sleep(wait)
                continue

            else:
                print(json.dumps({
                    "error": f"HTTP {e.code}",
                    "endpoint": endpoint,
                    "detail": err_body,
                }), file=sys.stderr)
                sys.exit(1)

        except urllib.error.URLError as e:
            if attempt < retries - 1:
                time.sleep(2 ** attempt)
                continue
            print(json.dumps({
                "error": "Network error",
                "message": str(e.reason),
                "hint": "Check your internet connection",
            }), file=sys.stderr)
            sys.exit(1)

        except TimeoutError:
            if attempt < retries - 1:
                continue
            print(json.dumps({
                "error": "Request timeout",
                "message": "ClickUp API did not respond in 30s",
            }), file=sys.stderr)
            sys.exit(1)


def output(data, pretty: bool = True):
    """Print JSON output."""
    if pretty:
        print(json.dumps(data, indent=2))
    else:
        print(json.dumps(data))


# ─────────────────────────────────────────────
# ClickUp API Methods
# ─────────────────────────────────────────────

class ClickUpAPI:
    def __init__(self, api_key: str, config: dict):
        self.api_key = api_key
        self.config = config

    def _get(self, endpoint: str, params: dict = None) -> dict:
        return api_request("GET", endpoint, self.api_key, params=params)

    def _post(self, endpoint: str, data: dict) -> dict:
        return api_request("POST", endpoint, self.api_key, data=data)

    def _put(self, endpoint: str, data: dict) -> dict:
        return api_request("PUT", endpoint, self.api_key, data=data)

    def _delete(self, endpoint: str) -> dict:
        return api_request("DELETE", endpoint, self.api_key)

    # ── Auth / User ──────────────────────────

    def get_user(self) -> dict:
        return self._get("user")

    def get_workspaces(self) -> dict:
        return self._get("team")

    # ── Spaces ──────────────────────────────

    def get_spaces(self, workspace_id: str = None) -> dict:
        ws_id = workspace_id or self.config.get("workspace_id")
        if not ws_id:
            # Try to auto-discover from user's teams
            teams = self.get_workspaces()
            if teams.get("teams"):
                ws_id = teams["teams"][0]["id"]
                # Cache it back
                self.config["workspace_id"] = ws_id
            else:
                print(json.dumps({"error": "No workspace_id configured and none found"}),
                      file=sys.stderr)
                sys.exit(1)
        return self._get(f"team/{ws_id}/space", {"archived": "false"})

    def get_space(self, space_id: str) -> dict:
        return self._get(f"space/{space_id}")

    # ── Folders (Projects) ──────────────────

    def get_folders(self, space_id: str) -> dict:
        return self._get(f"space/{space_id}/folder", {"archived": "false"})

    def get_folder(self, folder_id: str) -> dict:
        return self._get(f"folder/{folder_id}")

    # ── Lists ────────────────────────────────

    def get_lists(self, folder_id: str) -> dict:
        return self._get(f"folder/{folder_id}/list", {"archived": "false"})

    def get_folderless_lists(self, space_id: str) -> dict:
        return self._get(f"space/{space_id}/list", {"archived": "false"})

    def get_list(self, list_id: str) -> dict:
        return self._get(f"list/{list_id}")

    # ── Tasks ────────────────────────────────

    def get_tasks(self, list_id: str, status: str = None, assignee: str = None,
                  due_date_gt: int = None, due_date_lt: int = None,
                  page: int = 0, subtasks: bool = False) -> dict:
        params = {"page": page, "subtasks": str(subtasks).lower()}
        if status:
            params["statuses[]"] = status
        if assignee:
            params["assignees[]"] = assignee
        if due_date_gt:
            params["due_date_gt"] = due_date_gt
        if due_date_lt:
            params["due_date_lt"] = due_date_lt
        return self._get(f"list/{list_id}/task", params)

    def get_task(self, task_id: str) -> dict:
        return self._get(f"task/{task_id}", {"include_subtasks": "true"})

    def create_task(self, list_id: str, title: str, description: str = None,
                    status: str = None, priority: int = None,
                    due_date: int = None, assignees: list = None,
                    tags: list = None, custom_fields: list = None) -> dict:
        data = {"name": title}
        if description:
            data["description"] = description
        if status:
            data["status"] = status
        if priority is not None:
            data["priority"] = priority
        if due_date:
            data["due_date"] = due_date
        if assignees:
            data["assignees"] = assignees
        if tags:
            data["tags"] = tags
        if custom_fields:
            data["custom_fields"] = custom_fields
        return self._post(f"list/{list_id}/task", data)

    def update_task(self, task_id: str, title: str = None, description: str = None,
                    status: str = None, priority: int = None,
                    due_date: int = None, assignees_add: list = None,
                    assignees_rem: list = None) -> dict:
        data = {}
        if title:
            data["name"] = title
        if description is not None:
            data["description"] = description
        if status:
            data["status"] = status
        if priority is not None:
            data["priority"] = priority
        if due_date is not None:
            data["due_date"] = due_date
        if assignees_add or assignees_rem:
            data["assignees"] = {}
            if assignees_add:
                data["assignees"]["add"] = assignees_add
            if assignees_rem:
                data["assignees"]["rem"] = assignees_rem
        return self._put(f"task/{task_id}", data)

    def delete_task(self, task_id: str) -> dict:
        return self._delete(f"task/{task_id}")

    # ── Search ────────────────────────────────

    def search_tasks(self, workspace_id: str, query: str, space_ids: list = None,
                     statuses: list = None, page: int = 0) -> dict:
        ws_id = workspace_id or self.config.get("workspace_id")
        params = {"query": query, "page": page}
        if space_ids:
            params["space_ids"] = ",".join(space_ids)
        if statuses:
            params["statuses"] = ",".join(statuses)
        return self._get(f"team/{ws_id}/task", params)

    # ── Custom Fields ─────────────────────────

    def get_list_custom_fields(self, list_id: str) -> dict:
        return self._get(f"list/{list_id}/field")

    def set_custom_field(self, task_id: str, field_id: str, value) -> dict:
        return self._post(f"task/{task_id}/field/{field_id}", {"value": value})

    # ── Comments ─────────────────────────────

    def get_task_comments(self, task_id: str) -> dict:
        return self._get(f"task/{task_id}/comment")

    def add_comment(self, task_id: str, comment: str) -> dict:
        return self._post(f"task/{task_id}/comment", {"comment_text": comment})


# ─────────────────────────────────────────────
# Formatters
# ─────────────────────────────────────────────

def fmt_task_summary(task: dict) -> dict:
    """Return a clean summary of a task."""
    due = None
    if task.get("due_date"):
        try:
            due = datetime.fromtimestamp(int(task["due_date"]) / 1000).strftime("%Y-%m-%d")
        except Exception:
            due = task["due_date"]

    return {
        "id": task.get("id"),
        "name": task.get("name"),
        "status": task.get("status", {}).get("status") if isinstance(task.get("status"), dict) else task.get("status"),
        "priority": task.get("priority", {}).get("priority") if isinstance(task.get("priority"), dict) else None,
        "due_date": due,
        "assignees": [a.get("username") or a.get("email", "") for a in task.get("assignees", [])],
        "url": task.get("url"),
        "list": task.get("list", {}).get("name") if isinstance(task.get("list"), dict) else None,
    }


def parse_due_date(s: str) -> int:
    """Parse a date string to Unix ms timestamp."""
    formats = ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "tomorrow", "today"]
    s = s.strip()

    if s.lower() == "today":
        return int(datetime.now().replace(hour=23, minute=59, second=59).timestamp() * 1000)
    if s.lower() == "tomorrow":
        from datetime import timedelta
        d = datetime.now() + timedelta(days=1)
        return int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000)

    for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"]:
        try:
            return int(datetime.strptime(s, fmt).timestamp() * 1000)
        except ValueError:
            continue

    print(json.dumps({"error": f"Cannot parse date: {s}", "hint": "Use YYYY-MM-DD format"}),
          file=sys.stderr)
    sys.exit(1)


PRIORITY_MAP = {"urgent": 1, "high": 2, "normal": 3, "low": 4, "1": 1, "2": 2, "3": 3, "4": 4}


def parse_priority(s: str) -> int:
    """Convert priority name or number to ClickUp int."""
    val = PRIORITY_MAP.get(s.lower())
    if val is None:
        print(json.dumps({
            "error": f"Invalid priority: {s}",
            "valid_values": list(PRIORITY_MAP.keys())
        }), file=sys.stderr)
        sys.exit(1)
    return val


# ─────────────────────────────────────────────
# CLI Commands
# ─────────────────────────────────────────────

def cmd_whoami(api: ClickUpAPI, args, _config):
    data = api.get_user()
    output(data)


def cmd_workspaces(api: ClickUpAPI, args, _config):
    data = api.get_workspaces()
    teams = data.get("teams", [])
    result = [{"id": t["id"], "name": t["name"], "members": len(t.get("members", []))} for t in teams]
    output(result)


def cmd_spaces(api: ClickUpAPI, args, config):
    ws_id = getattr(args, "workspace_id", None) or config.get("workspace_id")
    data = api.get_spaces(ws_id)
    spaces = data.get("spaces", [])
    result = [{"id": s["id"], "name": s["name"], "private": s.get("private", False)} for s in spaces]
    output(result)


def cmd_projects(api: ClickUpAPI, args, config):
    space_id = getattr(args, "space_id", None) or config.get("default_space_id")
    if not space_id:
        print(json.dumps({"error": "space_id required", "hint": "clickup projects [space_id]"}),
              file=sys.stderr)
        sys.exit(1)
    data = api.get_folders(space_id)
    folders = data.get("folders", [])
    result = [{"id": f["id"], "name": f["name"], "task_count": f.get("task_count", 0),
               "list_count": len(f.get("lists", []))} for f in folders]
    output(result)


def cmd_lists(api: ClickUpAPI, args, config):
    folder_id = getattr(args, "folder_id", None)
    space_id = getattr(args, "space_id", None) or config.get("default_space_id")

    if folder_id:
        data = api.get_lists(folder_id)
        lists = data.get("lists", [])
    elif space_id:
        data = api.get_folderless_lists(space_id)
        lists = data.get("lists", [])
    else:
        print(json.dumps({"error": "folder_id or space_id required"}), file=sys.stderr)
        sys.exit(1)

    result = [{"id": l["id"], "name": l["name"], "task_count": l.get("task_count", 0),
               "status": l.get("status")} for l in lists]
    output(result)


def cmd_tasks(api: ClickUpAPI, args, config):
    list_id = getattr(args, "list_id", None)
    if not list_id:
        # Try config default
        lists_map = config.get("list_mappings", {})
        if lists_map:
            first = next(iter(lists_map.values()))
            list_id = first
        else:
            print(json.dumps({"error": "list_id required", "hint": "clickup tasks [list_id] --status open"}),
                  file=sys.stderr)
            sys.exit(1)

    # Resolve friendly name
    lists_map = config.get("list_mappings", {})
    if list_id in lists_map:
        list_id = lists_map[list_id]

    data = api.get_tasks(
        list_id,
        status=getattr(args, "status", None),
        assignee=getattr(args, "assignee", None),
        subtasks=getattr(args, "subtasks", False),
    )
    tasks = data.get("tasks", [])

    if getattr(args, "summary", False):
        result = [fmt_task_summary(t) for t in tasks]
    else:
        result = tasks

    output(result)


def cmd_task(api: ClickUpAPI, args, config):
    task_id = args.task_id
    data = api.get_task(task_id)
    if getattr(args, "summary", False):
        output(fmt_task_summary(data))
    else:
        output(data)


def cmd_create(api: ClickUpAPI, args, config):
    list_id = args.list
    if not list_id:
        print(json.dumps({"error": "--list required", "hint": "clickup create --list LIST_ID --title 'My Task'"}),
              file=sys.stderr)
        sys.exit(1)

    # Resolve friendly name
    lists_map = config.get("list_mappings", {})
    if list_id in lists_map:
        list_id = lists_map[list_id]

    if not args.title:
        print(json.dumps({"error": "--title required"}), file=sys.stderr)
        sys.exit(1)

    due_date_ms = None
    if getattr(args, "due", None):
        due_date_ms = parse_due_date(args.due)

    priority_int = None
    if getattr(args, "priority", None):
        priority_int = parse_priority(args.priority)

    assignees = []
    if getattr(args, "assignee", None):
        assignees = [int(a) for a in args.assignee.split(",") if a.strip()]

    tags = []
    if getattr(args, "tags", None):
        tags = [t.strip() for t in args.tags.split(",") if t.strip()]

    data = api.create_task(
        list_id,
        title=args.title,
        description=getattr(args, "description", None),
        status=getattr(args, "status", None),
        priority=priority_int,
        due_date=due_date_ms,
        assignees=assignees,
        tags=tags,
    )
    output({"created": True, "id": data.get("id"), "name": data.get("name"), "url": data.get("url")})


def cmd_update(api: ClickUpAPI, args, config):
    task_id = args.task_id
    if not task_id:
        print(json.dumps({"error": "task_id required", "hint": "clickup update TASK_ID --status done"}),
              file=sys.stderr)
        sys.exit(1)

    due_date_ms = None
    if getattr(args, "due", None):
        due_date_ms = parse_due_date(args.due)

    priority_int = None
    if getattr(args, "priority", None):
        priority_int = parse_priority(args.priority)

    assignees_add = []
    if getattr(args, "add_assignee", None):
        assignees_add = [int(a) for a in args.add_assignee.split(",") if a.strip()]

    assignees_rem = []
    if getattr(args, "remove_assignee", None):
        assignees_rem = [int(a) for a in args.remove_assignee.split(",") if a.strip()]

    data = api.update_task(
        task_id,
        title=getattr(args, "title", None),
        description=getattr(args, "description", None),
        status=getattr(args, "status", None),
        priority=priority_int,
        due_date=due_date_ms,
        assignees_add=assignees_add,
        assignees_rem=assignees_rem,
    )
    output({"updated": True, "id": data.get("id"), "name": data.get("name"), "url": data.get("url")})


def cmd_delete(api: ClickUpAPI, args, config):
    task_id = args.task_id
    if getattr(args, "confirm", False):
        api.delete_task(task_id)  # returns {} on 204 or None on error
        output({"deleted": True, "id": task_id})
    else:
        print(json.dumps({"error": "Add --confirm to delete", "task_id": task_id}), file=sys.stderr)
        sys.exit(1)


def cmd_search(api: ClickUpAPI, args, config):
    ws_id = getattr(args, "workspace_id", None) or config.get("workspace_id")
    if not ws_id:
        # auto-discover
        teams = api.get_workspaces()
        if teams.get("teams"):
            ws_id = teams["teams"][0]["id"]
        else:
            print(json.dumps({"error": "workspace_id required"}), file=sys.stderr)
            sys.exit(1)

    space_ids = None
    if getattr(args, "space", None):
        space_ids = [args.space]

    statuses = None
    if getattr(args, "status", None):
        statuses = [args.status]

    data = api.search_tasks(ws_id, args.query, space_ids=space_ids, statuses=statuses)
    tasks = data.get("tasks", [])

    if getattr(args, "summary", False):
        output([fmt_task_summary(t) for t in tasks])
    else:
        output(tasks)


def cmd_comment(api: ClickUpAPI, args, config):
    task_id = args.task_id
    if getattr(args, "add", None):
        data = api.add_comment(task_id, args.add)
        output({"commented": True, "comment_id": data.get("id")})
    else:
        data = api.get_task_comments(task_id)
        output(data)


def cmd_fields(api: ClickUpAPI, args, config):
    list_id = args.list_id
    data = api.get_list_custom_fields(list_id)
    fields = data.get("fields", [])
    result = [{"id": f["id"], "name": f["name"], "type": f.get("type")} for f in fields]
    output(result)


def cmd_export(api: ClickUpAPI, args, config):
    """Export tasks from a list to markdown or CSV."""
    list_id = args.list_id
    fmt = getattr(args, "format", "markdown")
    data = api.get_tasks(list_id, status=getattr(args, "status", None))
    tasks = data.get("tasks", [])

    if fmt == "csv":
        import csv
        import io
        buf = io.StringIO()
        writer = csv.DictWriter(buf, fieldnames=["id", "name", "status", "priority", "due_date", "assignees", "url"])
        writer.writeheader()
        for t in tasks:
            s = fmt_task_summary(t)
            s["assignees"] = "; ".join(s["assignees"])
            writer.writerow(s)
        print(buf.getvalue())
    else:
        # Markdown
        lines = [f"# ClickUp Tasks — {datetime.now().strftime('%Y-%m-%d')}", ""]
        for t in tasks:
            s = fmt_task_summary(t)
            lines.append(f"## {s['name']}")
            lines.append(f"- **ID:** {s['id']}")
            lines.append(f"- **Status:** {s['status']}")
            lines.append(f"- **Priority:** {s['priority']}")
            lines.append(f"- **Due:** {s['due_date']}")
            lines.append(f"- **Assignees:** {', '.join(s['assignees'])}")
            lines.append(f"- **URL:** {s['url']}")
            lines.append("")
        print("\n".join(lines))


def cmd_config_show(api, args, config):
    """Show current config (redacting API key)."""
    safe = dict(config)
    if "api_key" in safe:
        safe["api_key"] = safe["api_key"][:8] + "..." + safe["api_key"][-4:]
    output(safe)


def cmd_config_init(api, args, config):
    """Create config file at ~/.config/clickup/config.json."""
    path = Path.home() / ".config" / "clickup" / "config.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    template = {
        "api_key": "YOUR_API_KEY_HERE",
        "workspace_id": "",
        "default_space_id": "",
        "list_mappings": {
            "todo": "LIST_ID_HERE",
            "backlog": "LIST_ID_HERE"
        }
    }
    if path.exists() and not getattr(args, "force", False):
        print(json.dumps({"error": "Config already exists", "path": str(path),
                          "hint": "Use --force to overwrite"}), file=sys.stderr)
        sys.exit(1)
    with open(path, "w") as f:
        json.dump(template, f, indent=2)
    path.chmod(0o600)
    output({"created": True, "path": str(path), "next": "Edit the file and add your API key"})


# ─────────────────────────────────────────────
# Argument Parser
# ─────────────────────────────────────────────

def build_parser():
    parser = argparse.ArgumentParser(
        prog="clickup",
        description="ClickUp CLI for OpenClaw — full read/write API access",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  clickup spaces
  clickup projects 90131082312
  clickup lists 90131082312 --space
  clickup tasks 901303874101 --status open --summary
  clickup task abc123xyz
  clickup create --list 901303874101 --title "Review PR" --due 2026-03-01 --priority high
  clickup update abc123xyz --status done
  clickup search --query "bug fix" --status open
  clickup comment abc123xyz --add "Looks good!"
  clickup export 901303874101 --format csv

Config:
  clickup config init       # create ~/.config/clickup/config.json
  clickup config show       # show current config

Environment:
  CLICKUP_API_KEY           # API key
  CLICKUP_WORKSPACE_ID      # default workspace
  CLICKUP_SPACE_ID          # default space
"""
    )

    sub = parser.add_subparsers(dest="command", help="Command")

    # whoami
    p_who = sub.add_parser("whoami", help="Show authenticated user")

    # workspaces
    p_ws = sub.add_parser("workspaces", help="List workspaces/teams")

    # spaces
    p_spaces = sub.add_parser("spaces", help="List spaces in workspace")
    p_spaces.add_argument("workspace_id", nargs="?", help="Workspace ID (uses config if omitted)")

    # projects (folders)
    p_proj = sub.add_parser("projects", help="List projects (folders) in a space")
    p_proj.add_argument("space_id", nargs="?", help="Space ID (uses config default if omitted)")

    # lists
    p_lists = sub.add_parser("lists", help="List lists in a folder or space")
    p_lists.add_argument("folder_id", nargs="?", help="Folder/project ID")
    p_lists.add_argument("--space", dest="space_id", help="Space ID (for folderless lists)")

    # tasks
    p_tasks = sub.add_parser("tasks", help="List tasks in a list")
    p_tasks.add_argument("list_id", nargs="?", help="List ID")
    p_tasks.add_argument("--status", help="Filter by status (e.g. open, done, in progress)")
    p_tasks.add_argument("--assignee", help="Filter by assignee user ID")
    p_tasks.add_argument("--subtasks", action="store_true", help="Include subtasks")
    p_tasks.add_argument("--summary", action="store_true", help="Return summary format")

    # task (single)
    p_task = sub.add_parser("task", help="Get details of a single task")
    p_task.add_argument("task_id", help="Task ID")
    p_task.add_argument("--summary", action="store_true", help="Return summary format")

    # create
    p_create = sub.add_parser("create", help="Create a new task")
    p_create.add_argument("--list", required=True, dest="list", help="List ID or friendly name")
    p_create.add_argument("--title", required=True, help="Task title")
    p_create.add_argument("--description", help="Task description")
    p_create.add_argument("--status", help="Initial status")
    p_create.add_argument("--priority", help="Priority: urgent/high/normal/low or 1-4")
    p_create.add_argument("--due", help="Due date: YYYY-MM-DD or 'today'/'tomorrow'")
    p_create.add_argument("--assignee", help="Assignee user ID(s), comma-separated")
    p_create.add_argument("--tags", help="Tags, comma-separated")

    # update
    p_update = sub.add_parser("update", help="Update a task")
    p_update.add_argument("task_id", help="Task ID to update")
    p_update.add_argument("--title", help="New title")
    p_update.add_argument("--description", help="New description")
    p_update.add_argument("--status", help="New status (e.g. open, in progress, done, closed)")
    p_update.add_argument("--priority", help="New priority: urgent/high/normal/low or 1-4")
    p_update.add_argument("--due", help="New due date: YYYY-MM-DD or 'today'/'tomorrow'")
    p_update.add_argument("--add-assignee", dest="add_assignee", help="Add assignee user ID(s)")
    p_update.add_argument("--remove-assignee", dest="remove_assignee", help="Remove assignee user ID(s)")

    # delete
    p_del = sub.add_parser("delete", help="Delete a task (requires --confirm)")
    p_del.add_argument("task_id", help="Task ID to delete")
    p_del.add_argument("--confirm", action="store_true", help="Confirm deletion (required)")

    # search
    p_search = sub.add_parser("search", help="Search tasks across workspace")
    p_search.add_argument("--query", required=True, help="Search query string")
    p_search.add_argument("--space", dest="space", help="Limit to space ID")
    p_search.add_argument("--status", help="Filter by status")
    p_search.add_argument("--workspace", dest="workspace_id", help="Workspace ID")
    p_search.add_argument("--summary", action="store_true", help="Return summary format")

    # comment
    p_comment = sub.add_parser("comment", help="Get or add task comments")
    p_comment.add_argument("task_id", help="Task ID")
    p_comment.add_argument("--add", help="Add a comment (text)")

    # fields
    p_fields = sub.add_parser("fields", help="List custom fields for a list")
    p_fields.add_argument("list_id", help="List ID")

    # export
    p_export = sub.add_parser("export", help="Export tasks to markdown or CSV")
    p_export.add_argument("list_id", help="List ID")
    p_export.add_argument("--format", choices=["markdown", "csv"], default="markdown", help="Output format")
    p_export.add_argument("--status", help="Filter by status")

    # config
    p_config = sub.add_parser("config", help="Config management")
    p_config_sub = p_config.add_subparsers(dest="config_action")
    p_config_init = p_config_sub.add_parser("init", help="Create config file")
    p_config_init.add_argument("--force", action="store_true", help="Overwrite existing config")
    p_config_sub.add_parser("show", help="Show current config")

    return parser


# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────

COMMAND_MAP = {
    "whoami": cmd_whoami,
    "workspaces": cmd_workspaces,
    "spaces": cmd_spaces,
    "projects": cmd_projects,
    "lists": cmd_lists,
    "tasks": cmd_tasks,
    "task": cmd_task,
    "create": cmd_create,
    "update": cmd_update,
    "delete": cmd_delete,
    "search": cmd_search,
    "comment": cmd_comment,
    "fields": cmd_fields,
    "export": cmd_export,
}


def main():
    parser = build_parser()
    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(0)

    config = load_config()

    # Config sub-commands don't need a valid API key
    if args.command == "config":
        if args.config_action == "init":
            cmd_config_init(None, args, config)
        elif args.config_action == "show":
            cmd_config_show(None, args, config)
        else:
            print("Usage: clickup config [init|show]")
        return

    api_key = get_api_key(config)
    api = ClickUpAPI(api_key, config)

    fn = COMMAND_MAP.get(args.command)
    if fn:
        fn(api, args, config)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
