import io
import os
import zoneinfo
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Any

import httpx
from croniter import croniter
from loguru import logger
from universal_mcp.exceptions import NotAuthorizedError

from universal_mcp.agentr.types import AgentResponse, TemplateResponse


class AgentrClient:
    """Helper class for AgentR API operations.

    This class provides utility methods for interacting with the AgentR API,
    including authentication, authorization, and credential management.

    Args:
        api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var.
        base_url (str, optional): Base URL for AgentR API. Defaults to https://api.agentr.dev.
        auth_token (str, optional): Auth token for AgentR API. If not provided, will look for AGENTR_AUTH_TOKEN env var.
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        auth_token: str | None = None,
        timezone: str = "UTC",
        **kwargs,
    ):
        base_url = base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
        self.base_url = f"{base_url.rstrip('/')}/v1"
        self.api_key = api_key or os.getenv("AGENTR_API_KEY")
        self.auth_token = auth_token
        self.timezone = timezone
        self.user_id = None
        self.org_id = None
        if not self.api_key and not self.auth_token:
            raise ValueError("No API key or auth token provided")

    async def ainit(self):
        me_data = await self.me()
        self.user_id = me_data["id"]
        self.org_id = me_data.get("org_id")

    @asynccontextmanager
    async def aclient(self):
        async with httpx.AsyncClient(
            base_url=self.base_url,
            headers={"Authorization": f"Bearer {self.auth_token}", "accept": "application/json"}
            if self.auth_token
            else {"X-API-KEY": self.api_key, "accept": "application/json"},
            timeout=30,
            follow_redirects=True,
            verify=False,
        ) as client:
            yield client

    async def me(self):
        async with self.aclient() as client:
            response = await client.get("/users/me/")
            logger.debug(f"Me response: {response.status_code}")
            response.raise_for_status()
            data = response.json()
            return data

    def get_credentials(self, app_id: str) -> dict[str, Any]:
        """Get credentials for an integration from the AgentR API.

        Args:
            app_id (str): The ID of the app (e.g., 'asana', 'google-drive').

        Returns:
            dict: Credentials data from API response.

        Raises:
            NotAuthorizedError: If credentials are not found (404 response).
            HTTPError: For other API errors.
        """

        with self.client() as client:
            response = client.get(
                "/credentials/",
                params={"app_id": app_id},
            )
            logger.debug(f"Credentials response: {response.status_code}")
            if response.status_code == 404:
                logger.warning(f"No credentials found for app '{app_id}'. Requesting authorization...")
                action_url = self.get_authorization_url(app_id)
                raise NotAuthorizedError(action_url)
            response.raise_for_status()
            return response.json()

    async def get_authorization_url(self, app_id: str) -> str:
        """Get the authorization URL to connect an app.

        Args:
            app_id (str): The ID of the app to authorize.

        Returns:
            str: A message containing the authorization URL.

        Raises:
            HTTPError: If the API request fails.
        """
        async with self.aclient() as client:
            response = await client.post("/connections/authorize/", json={"app_id": app_id})
            response.raise_for_status()
            url = response.json().get("authorize_url")
            return f"Please ask the user to visit the following url to authorize the application: {url}. Render the url in proper markdown format with a clickable link."

    async def get_credentials_async(self, app_id: str) -> dict[str, Any]:
        """Get credentials for an integration from the AgentR API asynchronously.

        Args:
            app_id (str): The ID of the app (e.g., 'asana', 'google-drive').

        Returns:
            dict: Credentials data from API response.

        Raises:
            NotAuthorizedError: If credentials are not found (404 response).
            HTTPError: For other API errors.
        """
        async with self.aclient() as client:
            response = await client.get(
                "/credentials/",
                params={"app_id": app_id},
            )
            logger.debug(f"Credentials response: {response.status_code}")
            if response.status_code == 404:
                logger.warning(f"No credentials found for app '{app_id}'. Requesting authorization...")
                action_url = await self.get_authorization_url_async(app_id)
                raise NotAuthorizedError(action_url)
            response.raise_for_status()
            return response.json()

    async def get_authorization_url_async(self, app_id: str) -> str:
        """Get the authorization URL to connect an app asynchronously.

        Args:
            app_id (str): The ID of the app to authorize.

        Returns:
            str: A message containing the authorization URL.

        Raises:
            HTTPError: If the API request fails.
        """
        async with self.aclient() as client:
            response = await client.post("/connections/authorize/", json={"app_id": app_id})
            response.raise_for_status()
            url = response.json().get("authorize_url")
            return f"Please ask the user to visit the following url to authorize the application: {url}. Render the url in proper markdown format with a clickable link."

    async def list_all_apps(self):
        """Fetch available apps from AgentR API asynchronously.

        Returns:
            List[Dict[str, Any]]: A list of application data dictionaries.

        Raises:
            httpx.HTTPError: If the API request fails.
        """
        async with self.aclient() as client:
            response = await client.get("/apps/")
            response.raise_for_status()
            return response.json().get("items", [])

    async def list_my_apps(self):
        """Fetch user apps from AgentR API asynchronously.

        Returns:
            List[Dict[str, Any]]: A list of user app data dictionaries.
        """
        async with self.aclient() as client:
            response = await client.get("/apps/me/")
            response.raise_for_status()
            return response.json().get("items", [])

    async def list_my_connections(self):
        """Fetch user connections from AgentR API asynchronously.

        Returns:
            List[Dict[str, Any]]: A list of user connection data dictionaries.
        """
        async with self.aclient() as client:
            response = await client.get("/connections/")
            response.raise_for_status()
            return response.json().get("items", [])

    async def get_app_details(self, app_id: str):
        """Fetch a specific app from AgentR API asynchronously.

        Args:
            app_id (str): ID of the app to fetch.

        Returns:
            dict: App configuration data.

        Raises:
            httpx.HTTPError: If the API request fails.
        """
        async with self.aclient() as client:
            response = await client.get(f"/apps/{app_id}/")
            response.raise_for_status()
            return response.json()

    async def list_all_tools(self, app_id: str | None = None):
        """List all available tools from the AgentR API asynchronously.

        Note: In the backend, tools are globally listed and not tied to a
              specific app at this endpoint.

        Returns:
            List[Dict[str, Any]]: A list of tool configurations.
        """
        params = {}
        if app_id:
            params["app_id"] = app_id
        async with self.aclient() as client:
            response = await client.get("/tools/", params=params)
            response.raise_for_status()
            return response.json().get("items", [])

    async def get_tool_details(self, tool_id: str):
        """Fetch a specific tool configuration from the AgentR API asynchronously.

        Args:
            tool_id (str): ID of the tool to fetch.

        Returns:
            dict: Tool configuration data.

        Raises:
            httpx.HTTPError: If the API request fails.
        """
        async with self.aclient() as client:
            response = await client.get(f"/tools/{tool_id}/")
            response.raise_for_status()
            return response.json()

    async def search_all_apps(self, query: str, limit: int = 2, distance_threshold: float = 0.6):
        """Search for apps from the AgentR API asynchronously.

        Args:
            query (str): The query to search for.
            limit (int, optional): The number of apps to return. Defaults to 2.

        Returns:
            List[Dict[str, Any]]: A list of app data dictionaries.
        """
        async with self.aclient() as client:
            response = await client.get(
                "/apps/", params={"search": query, "limit": limit, "distance_threshold": distance_threshold}
            )
            response.raise_for_status()
            return response.json().get("items", [])

    async def search_all_tools(
        self, query: str, limit: int = 2, app_id: str | None = None, distance_threshold: float = 0.6
    ):
        """Search for tools from the AgentR API asynchronously.

        Args:
            query (str): The query to search for.
            limit (int, optional): The number of tools to return. Defaults to 2.
            app_id (str, optional): The ID of the app to search tools for.
        """
        params = {"limit": limit, "distance_threshold": distance_threshold}
        if query:
            params["search"] = query
        if app_id:
            params["app_id"] = app_id
        async with self.aclient() as client:
            response = await client.get("/tools/", params=params)
            response.raise_for_status()
            return response.json().get("items", [])

    async def _upload_file(self, file_name: str, mime_type: str, base64_data: bytes) -> str:
        """Upload a file to the server asynchronously."""
        files = {"file": (file_name, io.BytesIO(base64_data), mime_type)}
        async with self.aclient() as client:
            response = await client.post("/files/upload/", files=files)
            response.raise_for_status()
            return response.json()

    async def get_agent(self, agent_id: str) -> AgentResponse:
        if not self.user_id or not self.org_id:
            raise ValueError("AgentrClient requires user_id and org_id for get_agent")

        wingmen_id = self.user_id.split(":")[0]
        async with self.aclient() as client:
            response = await client.get(
                f"/projects/{agent_id}/",
                params={"user_id": wingmen_id, "org_id": self.org_id},
            )
            response.raise_for_status()
            data = response.json()
            return AgentResponse.model_validate(data)

    async def get_template(self, template_id: str) -> TemplateResponse:
        async with self.aclient() as client:
            response = await client.get(f"/projects/templates/{template_id}/", params={"resolved": "true"})
            response.raise_for_status()
            data = response.json()
            return TemplateResponse.model_validate(data)

    async def create_agent(
        self,
        *,
        name: str | None = None,
        description: str | None = None,
        instructions: dict[str, Any] | None = None,
        tools: dict[str, list[str]] | None = None,
        html: str | None = None,
        details: str | None = None,
    ) -> AgentResponse:
        if not self.user_id or not self.org_id:
            raise ValueError("AgentrClient requires user_id and org_id for create_agent")
        wingmen_id = self.user_id.split(":")[0]
        body = {
            k: v
            for k, v in {
                "name": name,
                "description": description,
                "instructions": instructions,
                "tools": tools,
                "html": html,
                "details": details,
            }.items()
            if v is not None
        }
        async with self.aclient() as client:
            response = await client.post(
                "/projects/",
                params={"user_id": wingmen_id, "org_id": self.org_id},
                json=body,
            )
            response.raise_for_status()
            data = response.json()
            return AgentResponse.model_validate(data)

    def _convert_to_utc_cron(self, cron: str) -> str:
        """Convert a local cron expression to UTC."""
        if not self.timezone or self.timezone.upper() == "UTC":
            return cron

        if croniter is None:
            logger.warning("croniter not installed, skipping timezone conversion")
            return cron

        try:
            tz = zoneinfo.ZoneInfo(self.timezone)
        except Exception:
            logger.warning(f"Invalid timezone {self.timezone}, skipping conversion")
            return cron

        try:
            now = datetime.now(tz)
            iter_ = croniter(cron, now)
            next_local = iter_.get_next(datetime)
            next_utc = next_local.astimezone(zoneinfo.ZoneInfo("UTC"))

            parts = cron.split()
            if len(parts) != 5:
                return cron  # Non-standard cron or extended syntax

            parts[0] = str(next_utc.minute)
            parts[1] = str(next_utc.hour)

            return " ".join(parts)

        except Exception as e:
            logger.error(f"Failed to convert cron to UTC: {e}")
            return cron

    async def create_job(
        self,
        *,
        task_name: str,
        cron: str,
        agent_id: str,
        params: dict[str, Any],
        enabled: bool = True,
        webhook: str = f"{os.getenv('FRONTEND_URL', 'https://ruzo.ai')}/api/jobs/webhook",
    ) -> dict[str, Any]:
        """Create a scheduled job."""
        utc_cron = self._convert_to_utc_cron(cron)

        body = {
            "task_name": task_name,
            "cron": utc_cron,
            "agent_id": agent_id,
            "enabled": enabled,
            "params": params,
            "webhook": webhook,
        }
        async with self.aclient() as client:
            response = await client.post("/jobs/", json=body)
            response.raise_for_status()
            return response.json()

    async def list_jobs(self, agent_id: str | None = None) -> list[dict[str, Any]]:
        """List jobs."""
        params = {}
        params["user_id"] = self.user_id
        if agent_id:
            params["agent_id"] = agent_id

        async with self.aclient() as client:
            response = await client.get("/jobs/", params=params)
            response.raise_for_status()
            return response.json().get("items", [])

    async def update_job(
        self,
        *,
        job_id: str,
        cron: str,
        enabled: bool = True,
        agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Update a scheduled job."""
        utc_cron = self._convert_to_utc_cron(cron)

        body = {
            "task_name": "invoke_agent",
            "cron": utc_cron,
            "enabled": enabled,
            "agent_id": agent_id,
        }
        async with self.aclient() as client:
            response = await client.put(f"/jobs/{job_id}/", json=body)
            response.raise_for_status()
            return response.json()

    async def update_agent(
        self,
        *,
        agent_id: str,
        name: str | None = None,
        description: str | None = None,
        instructions: dict[str, Any] | None = None,
        tools: dict[str, list[str]] | None = None,
        html: str | None = None,
        details: str | None = None,
    ) -> AgentResponse:
        if not self.user_id or not self.org_id:
            raise ValueError("AgentrClient requires user_id and org_id for update_agent")
        wingmen_id = self.user_id.split(":")[0]
        body = {
            k: v
            for k, v in {
                "name": name,
                "description": description,
                "instructions": instructions,
                "tools": tools,
                "html": html,
                "details": details,
            }.items()
            if v is not None
        }
        async with self.aclient() as client:
            response = await client.patch(
                f"/projects/{agent_id}/",
                params={"user_id": wingmen_id, "org_id": self.org_id},
                json=body,
            )
            response.raise_for_status()
            data = response.json()
            return AgentResponse.model_validate(data)
