# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""ClickUp API service (OAuth or personal token via UserConnection)."""

import json
import logging
import asyncio
from typing import Any, Callable, Dict, List, Optional, Union

import aiohttp

logger = logging.getLogger(__name__)

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


def clickup_authorization_header(token: str) -> str:
    """ClickUp: personal tokens are `pk_...` without Bearer; OAuth uses Bearer."""
    t = (token or "").strip()
    if t.startswith("pk_"):
        return t
    return f"Bearer {t}"


class ClickupService:
    """ClickUp REST API v2 — teams, hierarchy, lists as tables (tasks + custom fields)."""

    def __init__(self, context, get_service: Callable[[str], Any]):
        self._context = context
        self._get_service = get_service
        self.accessToken: Optional[str] = None

    def setAccessTokenFromConnection(self, userConnection) -> bool:
        """Load OAuth/personal token from SecurityService for this UserConnection."""
        try:
            if not userConnection:
                logger.error("UserConnection is required to set access token")
                return False
            if isinstance(userConnection, dict):
                connection_id = userConnection.get("id")
            else:
                connection_id = getattr(userConnection, "id", None)
            if not connection_id:
                logger.error("UserConnection must have an 'id' field")
                return False
            security = self._get_service("security")
            if not security:
                logger.error("Security service not available for token access")
                return False
            token = security.getFreshToken(connection_id)
            if not token:
                logger.error(f"No token found for connection {connection_id}")
                return False
            self.accessToken = token.tokenAccess
            return True
        except Exception as e:
            logger.error(f"Error setting ClickUp access token: {e}")
            return False

    def setAccessToken(self, token: str) -> None:
        """Set token directly (e.g. connector adapter)."""
        self.accessToken = token

    async def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
        data: Optional[aiohttp.FormData] = None,
    ) -> Union[Dict[str, Any], List[Any], bytes, None]:
        if not self.accessToken:
            return {"error": "Access token is not set. Call setAccessTokenFromConnection first."}
        url = f"{_CLICKUP_API_BASE}/{path.lstrip('/')}"
        headers: Dict[str, str] = {
            "Authorization": clickup_authorization_header(self.accessToken),
        }
        if json_body is not None:
            headers["Content-Type"] = "application/json"

        timeout = aiohttp.ClientTimeout(total=60)
        try:
            async with aiohttp.ClientSession(timeout=timeout) as session:
                kwargs: Dict[str, Any] = {"headers": headers, "params": params}
                if json_body is not None:
                    kwargs["json"] = json_body
                if data is not None:
                    kwargs["data"] = data

                async with session.request(method.upper(), url, **kwargs) as resp:
                    if resp.status == 204:
                        return {}
                    text = await resp.text()
                    if resp.status >= 400:
                        # 404 on GET is common (wrong id / preview) — avoid ERROR noise in logs
                        log = logger.warning if resp.status == 404 else logger.error
                        log(f"ClickUp API {method} {url} -> {resp.status}: {text[:500]}")
                        return {"error": f"HTTP {resp.status}", "body": text}
                    if not text:
                        return {}
                    try:
                        return json.loads(text)
                    except Exception:
                        return {"raw": text}
        except asyncio.TimeoutError:
            return {"error": f"ClickUp API timeout: {path}"}
        except Exception as e:
            logger.error(f"ClickUp API error: {e}")
            return {"error": str(e)}

    async def requestRaw(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
    ) -> Union[Dict[str, Any], List[Any], None]:
        """Escape hatch: call any v2 path under /api/v2 (path without leading /api/v2)."""
        return await self._request(method, path, params=params, json_body=json_body)

    # --- Teams / user ---

    async def getAuthorizedUser(self) -> Dict[str, Any]:
        return await self._request("GET", "/user")

    async def getAuthorizedTeams(self) -> Dict[str, Any]:
        return await self._request("GET", "/team")

    async def getTeam(self, team_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/team/{team_id}")

    # --- Hierarchy ---

    async def getSpaces(self, team_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/team/{team_id}/space")

    async def getSpace(self, space_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/space/{space_id}")

    async def getFolders(self, space_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/space/{space_id}/folder")

    async def getFolder(self, folder_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/folder/{folder_id}")

    async def getListsInFolder(self, folder_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/folder/{folder_id}/list")

    async def getFolderlessLists(self, space_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/space/{space_id}/list")

    async def getList(self, list_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/list/{list_id}")

    async def getListFields(self, list_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/list/{list_id}/field")

    # --- Tasks (rows) ---

    async def getTasksInList(
        self,
        list_id: str,
        *,
        page: int = 0,
        include_closed: bool = False,
        subtasks: bool = True,
    ) -> Dict[str, Any]:
        params: Dict[str, Any] = {
            "page": page,
            "subtasks": str(subtasks).lower(),
            "include_closed": str(include_closed).lower(),
        }
        return await self._request("GET", f"/list/{list_id}/task", params=params)

    async def getTask(self, task_id: str, *, include_subtasks: bool = True) -> Dict[str, Any]:
        params = {"include_subtasks": str(include_subtasks).lower()}
        return await self._request("GET", f"/task/{task_id}", params=params)

    async def createTask(self, list_id: str, body: Dict[str, Any]) -> Dict[str, Any]:
        return await self._request("POST", f"/list/{list_id}/task", json_body=body)

    async def updateTask(self, task_id: str, body: Dict[str, Any]) -> Dict[str, Any]:
        return await self._request("PUT", f"/task/{task_id}", json_body=body)

    async def deleteTask(self, task_id: str) -> Dict[str, Any]:
        return await self._request("DELETE", f"/task/{task_id}")

    async def searchTeamTasks(
        self,
        team_id: str,
        *,
        query: str,
        page: int = 0,
    ) -> Dict[str, Any]:
        """Search tasks in a workspace (team)."""
        params = {"query": query, "page": page}
        return await self._request("GET", f"/team/{team_id}/task", params=params)

    async def uploadTaskAttachment(self, task_id: str, file_bytes: bytes, file_name: str) -> Dict[str, Any]:
        """Upload a file attachment to a task (multipart)."""
        if not self.accessToken:
            return {"error": "Access token is not set."}
        url = f"{_CLICKUP_API_BASE}/task/{task_id}/attachment"
        headers = {"Authorization": clickup_authorization_header(self.accessToken)}
        data = aiohttp.FormData()
        data.add_field(
            "attachment",
            file_bytes,
            filename=file_name,
            content_type="application/octet-stream",
        )
        timeout = aiohttp.ClientTimeout(total=120)
        try:
            async with aiohttp.ClientSession(timeout=timeout) as session:
                async with session.post(url, headers=headers, data=data) as resp:
                    text = await resp.text()
                    if resp.status >= 400:
                        return {"error": f"HTTP {resp.status}", "body": text}
                    return json.loads(text) if text else {}
        except Exception as e:
            return {"error": str(e)}
