"""
Remote Document client for IQ host CRUD API.

Provides a generic REST client for document operations on IQ hosts.
"""

from __future__ import annotations

import base64
import json
from dataclasses import dataclass, field
from typing import Any, ClassVar, TypeVar

import httpx

from iq_sdk.config import SiteConfig

T = TypeVar("T", bound="RemoteDocument")


@dataclass
class APIResult:
    """Result of an API operation."""

    success: bool
    data: dict[str, Any] | list[dict[str, Any]] | None = None
    error: str | None = None
    status_code: int | None = None


@dataclass
class PingResult:
    """Result of a ping/health check."""

    reachable: bool
    status_code: int | None = None
    latency_ms: float | None = None
    error: str | None = None
    version: str | None = None


@dataclass
class SiteStatus:
    """Connectivity and permission status for a site."""

    site_id: str
    reachable: bool = False
    latency_ms: float | None = None
    authenticated: bool = False
    can_read: bool = False
    can_create: bool = False
    can_update: bool = False
    can_delete: bool = False
    can_register_worker: bool = False
    socket_io_url: str | None = None  # Only set if can_register_worker is True
    socket_io_ns: str | None = None  # Socket.IO namespace (e.g. "/iq_site")
    alive_timeout: int = 3600  # Seconds before worker is considered outdated
    reconnect_interval: int = 5  # Seconds between reconnect attempts
    error: str | None = None


class IQApiClient:
    """
    Base HTTP client for IQ host API.

    Handles authentication and common API operations.
    """

    def __init__(
        self,
        host: str,
        api_key: str | None = None,
        api_secret: str | None = None,
        timeout: float = 30.0,
    ):
        """
        Initialize API client.

        Args:
            host: Base URL of the IQ host
            api_key: API key for authentication
            api_secret: API secret for authentication
            timeout: Request timeout in seconds
        """
        self.host = host.rstrip("/")
        self.api_key = api_key
        self.api_secret = api_secret
        self.timeout = timeout
        self._client: httpx.AsyncClient | None = None

    @classmethod
    def from_site(cls, site: SiteConfig) -> IQApiClient:
        """Create client from site configuration."""
        return cls(
            host=site.get_host(),
            api_key=site.get_api_key(),
            api_secret=site.load_api_secret(),
            timeout=30.0,
        )

    def _get_auth_headers(self) -> dict[str, str]:
        """Get authorization headers with Base64 encoding."""
        if self.api_key and self.api_secret:
            credentials = f"{self.api_key}:{self.api_secret}"
            encoded = base64.b64encode(credentials.encode()).decode()
            return {"Authorization": f"Basic {encoded}"}
        return {}

    async def __aenter__(self) -> IQApiClient:
        """Async context manager entry."""
        self._client = httpx.AsyncClient(
            base_url=self.host,
            timeout=self.timeout,
            headers=self._get_auth_headers(),
        )
        return self

    async def __aexit__(self, *args: Any) -> None:
        """Async context manager exit."""
        if self._client:
            await self._client.aclose()
            self._client = None

    @property
    def client(self) -> httpx.AsyncClient:
        """Get HTTP client, creating if needed."""
        if self._client is None:
            self._client = httpx.AsyncClient(
                base_url=self.host,
                timeout=self.timeout,
                headers=self._get_auth_headers(),
            )
        return self._client

    async def request_get(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make a raw GET request."""
        return await self.client.get(url, **kwargs)

    async def request_post(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make a raw POST request."""
        return await self.client.post(url, **kwargs)

    async def request_put(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make a raw PUT request."""
        return await self.client.put(url, **kwargs)

    async def request_delete(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make a raw DELETE request."""
        return await self.client.delete(url, **kwargs)

    # Alias for backward compatibility
    async def post(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make a POST request (alias for request_post)."""
        return await self.request_post(url, **kwargs)

    async def check_status(self, namespace: str | None = None) -> SiteStatus:
        """
        Check connectivity and permissions.

        Args:
            namespace: Project namespace for permission check (required for scoped auth)
        """
        import time

        status = SiteStatus(site_id="unknown")
        start = time.monotonic()

        try:
            # Check reachability
            await self.client.get("/api/method/ping")
            status.latency_ms = round((time.monotonic() - start) * 1000, 2)
            status.reachable = True

            # Check authentication and permissions (with namespace for scoped auth)
            params = {"namespace": namespace} if namespace else {}
            perm_response = await self.client.get(
                "/api/method/iq_core.iq_core.doctype.automation_tool.automation_tool.check_permissions",
                params=params,
            )
            if perm_response.status_code == 200:
                data = perm_response.json()
                message = data.get("message", {})
                status.authenticated = True
                status.can_read = message.get("can_read", False)
                status.can_create = message.get("can_create", False)
                status.can_update = message.get("can_update", False)
                status.can_delete = message.get("can_delete", False)
                status.can_register_worker = message.get("can_register_worker", False)
                # Socket.IO URL and namespace only provided if worker registration is allowed
                if status.can_register_worker:
                    status.socket_io_url = message.get("socket_io_url")
                    status.socket_io_ns = message.get("socket_io_ns")
                    # Worker timing configuration from host
                    status.alive_timeout = int(message["alive_timeout"])
                    if message.get("reconnect_interval"):
                        status.reconnect_interval = int(message["reconnect_interval"])
            elif perm_response.status_code == 401:
                status.authenticated = False
                status.error = (
                    f"No auth for Project: {namespace}" if namespace else "Authentication failed"
                )
            elif perm_response.status_code == 403:
                status.authenticated = True
                status.error = (
                    f"No permission for Project: {namespace}" if namespace else "Permission denied"
                )

        except httpx.ConnectError as e:
            status.error = f"Connection failed: {e}"
        except httpx.TimeoutException:
            status.error = f"Timeout after {self.timeout}s"
        except Exception as e:
            status.error = str(e)

        return status

    async def ping(self) -> PingResult:
        """
        Simple ping to check if host is reachable.

        Returns:
            PingResult with reachability info
        """
        import time

        start = time.monotonic()

        try:
            response = await self.client.get("/api/method/ping")
            latency = (time.monotonic() - start) * 1000

            return PingResult(
                reachable=True,
                status_code=response.status_code,
                latency_ms=round(latency, 2),
            )

        except httpx.ConnectError as e:
            return PingResult(
                reachable=False,
                error=f"Connection failed: {e}",
            )
        except httpx.TimeoutException:
            return PingResult(
                reachable=False,
                error=f"Timeout after {self.timeout}s",
            )
        except Exception as e:
            return PingResult(
                reachable=False,
                error=str(e),
            )

    async def get(
        self,
        doctype: str,
        name: str,
        fields: list[str] | None = None,
    ) -> APIResult:
        """
        Get a single document.

        Args:
            doctype: Document type (e.g., "IQ Tool")
            name: Document name/ID
            fields: Fields to return (default: all)

        Returns:
            APIResult with document data
        """
        try:
            params: dict[str, Any] = {}
            if fields:
                params["fields"] = json.dumps(fields)

            response = await self.client.get(
                f"/api/resource/{doctype}/{name}",
                params=params if params else None,
            )

            if response.status_code == 200:
                data = response.json()
                return APIResult(
                    success=True,
                    data=data.get("data", data),
                    status_code=response.status_code,
                )
            elif response.status_code == 404:
                return APIResult(
                    success=False,
                    error="Not found",
                    status_code=response.status_code,
                )
            else:
                return APIResult(
                    success=False,
                    error=f"HTTP {response.status_code}",
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def get_list(
        self,
        doctype: str,
        fields: list[str] | None = None,
        filters: list[list[Any]] | None = None,
        limit_page_length: int = 0,
        order_by: str | None = None,
    ) -> APIResult:
        """
        Get list of documents.

        Args:
            doctype: Document type
            fields: Fields to return
            filters: Filter conditions [[field, op, value], ...]
            limit_page_length: Max results (0 = all)
            order_by: Order by field

        Returns:
            APIResult with list of documents
        """
        try:
            params: dict[str, Any] = {}
            if fields:
                params["fields"] = json.dumps(fields)
            if filters:
                params["filters"] = json.dumps(filters)
            if limit_page_length:
                params["limit_page_length"] = limit_page_length
            if order_by:
                params["order_by"] = order_by

            response = await self.client.get(
                f"/api/resource/{doctype}",
                params=params if params else None,
            )

            if response.status_code == 200:
                data = response.json()
                return APIResult(
                    success=True,
                    data=data.get("data", []),
                    status_code=response.status_code,
                )
            else:
                # Extract error message from response
                error_msg = f"HTTP {response.status_code}"
                try:
                    error_data = response.json()
                    if "exception" in error_data:
                        error_msg = error_data.get("exc_type", "Error")
                        if "_server_messages" in error_data:
                            import json as json_module

                            msgs = json_module.loads(error_data["_server_messages"])
                            if msgs:
                                msg_data = json_module.loads(msgs[0])
                                error_msg = msg_data.get("message", error_msg)
                except Exception:
                    pass
                return APIResult(
                    success=False,
                    error=error_msg,
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def create(
        self,
        doctype: str,
        data: dict[str, Any],
    ) -> APIResult:
        """
        Create a new document.

        Args:
            doctype: Document type
            data: Document data

        Returns:
            APIResult with created document
        """
        try:
            response = await self.client.post(
                f"/api/resource/{doctype}",
                json=data,
            )

            if response.status_code in (200, 201):
                result = response.json()
                return APIResult(
                    success=True,
                    data=result.get("data", result),
                    status_code=response.status_code,
                )
            else:
                error_data = response.json() if response.content else {}
                error_msg = f"HTTP {response.status_code}: {error_data.get('exc_type', '')}\n\t{error_data.get('exception', '')}"
                return APIResult(
                    success=False,
                    error=error_msg,
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def update(
        self,
        doctype: str,
        name: str,
        data: dict[str, Any],
    ) -> APIResult:
        """
        Update a document.

        Args:
            doctype: Document type
            name: Document name/ID
            data: Updated data

        Returns:
            APIResult with updated document
        """
        try:
            response = await self.client.put(
                f"/api/resource/{doctype}/{name}",
                json=data,
            )

            # print(response.json())

            if response.status_code == 200:
                result = response.json()
                return APIResult(
                    success=True,
                    data=result.get("data", result),
                    status_code=response.status_code,
                )
            else:
                error_data = response.json() if response.content else {}
                error_msg = error_data.get("message", f"HTTP {response.status_code}")
                return APIResult(
                    success=False,
                    error=error_msg,
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def delete(
        self,
        doctype: str,
        name: str,
    ) -> APIResult:
        """
        Delete a document.

        Args:
            doctype: Document type
            name: Document name/ID

        Returns:
            APIResult
        """
        try:
            response = await self.client.delete(
                f"/api/resource/{doctype}/{name}",
            )

            if response.status_code in (200, 202, 204):
                return APIResult(
                    success=True,
                    status_code=response.status_code,
                )
            else:
                error_data = response.json() if response.content else {}
                error_msg = error_data.get("message", f"HTTP {response.status_code}")
                return APIResult(
                    success=False,
                    error=error_msg,
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def call_method(
        self,
        method: str,
        data: dict[str, Any] | None = None,
        http_method: str = "POST",
    ) -> APIResult:
        """
        Call a server method.

        Args:
            method: Method path (e.g., "module.name.method")
            data: Method arguments
            http_method: HTTP method (GET or POST)

        Returns:
            APIResult with method response
        """
        try:
            url = f"/api/method/{method}"

            if http_method.upper() == "GET":
                response = await self.client.get(url, params=data)
            else:
                response = await self.client.post(url, json=data or {})

            if response.status_code == 200:
                result = response.json()
                return APIResult(
                    success=True,
                    data=result.get("message", result),
                    status_code=response.status_code,
                )
            else:
                error_data = response.json() if response.content else {}
                error_msg = error_data.get("message", f"HTTP {response.status_code}")
                return APIResult(
                    success=False,
                    error=error_msg,
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))

    async def upload_file(
        self,
        file_path: str,
        file_content: bytes,
        doctype: str | None = None,
        docname: str | None = None,
        fieldname: str | None = None,
        is_private: bool = True,
    ) -> APIResult:
        """
        Upload a file.

        Args:
            file_path: Original file path/name
            file_content: File content as bytes
            doctype: Optional attached doctype
            docname: Optional attached document name
            fieldname: Optional field name
            is_private: Whether file is private

        Returns:
            APIResult with file info
        """
        try:
            files = {"file": (file_path, file_content)}
            data: dict[str, Any] = {"is_private": int(is_private)}
            if doctype:
                data["doctype"] = doctype
            if docname:
                data["docname"] = docname
            if fieldname:
                data["fieldname"] = fieldname

            response = await self.client.post(
                "/api/method/upload_file",
                data=data,
                files=files,
            )

            if response.status_code == 200:
                result = response.json()
                return APIResult(
                    success=True,
                    data=result.get("message", result),
                    status_code=response.status_code,
                )
            else:
                return APIResult(
                    success=False,
                    error=f"HTTP {response.status_code}",
                    status_code=response.status_code,
                )

        except Exception as e:
            return APIResult(success=False, error=str(e))


@dataclass
class RemoteDocument:
    """
    Base class for remote documents.

    Provides CRUD operations for a specific document type.
    Subclasses should set `doctype` and define fields.
    """

    # Document type for REST API (e.g., "IQ Tool")
    doctype: ClassVar[str] = ""

    # Standard fields included in all queries
    standard_fields: ClassVar[list[str]] = ["name", "creation", "modified"]

    # Document identifier
    name: str = ""

    # Timestamps
    creation: str | None = None
    modified: str | None = None

    # Associated client (set during queries)
    _client: IQApiClient | None = field(default=None, repr=False)

    @classmethod
    def get_fields(cls) -> list[str]:
        """Get all fields for this document type."""
        # Get dataclass fields, excluding private ones
        from dataclasses import fields as dc_fields

        doc_fields = [
            f.name
            for f in dc_fields(cls)
            if not f.name.startswith("_") and f.name not in ("doctype", "standard_fields")
        ]
        # Ensure standard fields are included
        return list(set(cls.standard_fields + doc_fields))

    @classmethod
    async def find(
        cls: type[T],
        client: IQApiClient,
        name: str,
        fields: list[str] | None = None,
    ) -> T | None:
        """
        Find a document by name.

        Args:
            client: API client
            name: Document name/ID
            fields: Fields to return (default: all)

        Returns:
            Document instance or None
        """
        result = await client.get(
            cls.doctype,
            name,
            fields=fields or cls.get_fields(),
        )

        if result.success and result.data:
            doc = cls(**result.data)  # type: ignore
            doc._client = client
            return doc
        return None

    @classmethod
    async def find_all(
        cls: type[T],
        client: IQApiClient,
        filters: dict[str, tuple[str, Any]] | None = None,
        fields: list[str] | None = None,
        limit: int = 0,
        order_by: str | None = None,
    ) -> list[T]:
        """
        Find all documents matching filters.

        Args:
            client: API client
            filters: Filter dict {field: (op, value)}
            fields: Fields to return
            limit: Max results (0 = all)
            order_by: Order by field

        Returns:
            List of document instances
        """
        # Convert filter dict to list format
        filter_list = None
        if filters:
            filter_list = [[field, op, value] for field, (op, value) in filters.items()]

        result = await client.get_list(
            cls.doctype,
            fields=fields or cls.get_fields(),
            filters=filter_list,
            limit_page_length=limit,
            order_by=order_by,
        )

        if result.success and isinstance(result.data, list):
            docs = []
            for item in result.data:
                doc = cls(**item)  # type: ignore
                doc._client = client
                docs.append(doc)
            return docs

        # Raise exception on error so caller can handle it
        if result.error:
            raise RuntimeError(f"API Error: {result.error}")
        return []

    async def save(self, client: IQApiClient | None = None) -> APIResult:
        """
        Save document (create or update).

        Args:
            client: API client (uses self._client if not provided)

        Returns:
            APIResult
        """
        api_client = client or self._client
        if not api_client:
            return APIResult(success=False, error="No client configured")

        data = self.to_dict()

        if self.name:
            # Update existing
            result = await api_client.update(self.doctype, self.name, data)
        else:
            # Create new
            result = await api_client.create(self.doctype, data)
            if result.success and result.data and isinstance(result.data, dict):
                self.name = result.data.get("name", "")

        # Raise exception on error so caller can handle it
        if result.error:
            print(result)
            raise RuntimeError(f"API Error: {result.error}")

        return result

    async def delete(self, client: IQApiClient | None = None) -> APIResult:
        """
        Delete this document.

        Args:
            client: API client

        Returns:
            APIResult
        """
        api_client = client or self._client
        if not api_client:
            return APIResult(success=False, error="No client configured")

        if not self.name:
            return APIResult(success=False, error="Document has no name")

        return await api_client.delete(self.doctype, self.name)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for API."""
        from dataclasses import asdict

        data = asdict(self)
        # Remove private fields
        return {k: v for k, v in data.items() if not k.startswith("_") and v is not None}


def ping_sync(host: str, timeout: float = 5.0) -> PingResult:
    """
    Synchronous ping helper.

    Args:
        host: Host URL to ping
        timeout: Timeout in seconds

    Returns:
        PingResult
    """
    import asyncio

    async def _ping() -> PingResult:
        async with IQApiClient(host, timeout=timeout) as client:
            return await client.ping()

    return asyncio.run(_ping())
