"""
Supabase REST API Client (Self-Hosted)

Handles all HTTP communication with Supabase Self-Hosted APIs.
All requests go through Kong API Gateway on single base URL.

APIs:
- PostgREST (/rest/v1/) - Database CRUD
- GoTrue (/auth/v1/) - Authentication
- Storage (/storage/v1/) - File storage
- Edge Functions (/functions/v1/) - Serverless
- postgres-meta (/pg/) - Database admin
"""

import base64
import logging
from typing import Any

import aiohttp


class SupabaseClient:
    """
    Supabase Self-Hosted API client.

    All requests go through Kong gateway on single base URL.
    Uses JWT-based authentication with anon_key or service_role_key.
    """

    def __init__(
        self,
        base_url: str,
        anon_key: str,
        service_role_key: str,
        meta_url: str | None = None,
        meta_auth: str | None = None,
    ):
        """
        Initialize Supabase API client.

        Args:
            base_url: Supabase instance URL (Kong gateway or supabase.co)
            anon_key: Public API key (RLS protected). Optional — if empty,
                service_role_key is used for all calls.
            service_role_key: Admin API key (bypasses RLS). Required.
            meta_url: Optional direct postgres-meta URL (e.g. http://localhost:5555).
                When provided, postgres-meta calls hit this URL directly instead of
                the Kong /pg/ route. Useful when /pg/ is not exposed through Kong.
            meta_auth: Optional Basic Auth credentials for postgres-meta (username:password).
                When provided, requests to meta_base_url use Basic Auth instead of JWT.
                Recommended when postgres-meta is exposed via a public URL.
        """
        self.base_url = base_url.rstrip("/")
        self.anon_key = anon_key
        self.service_role_key = service_role_key
        # postgres-meta base: custom URL or Kong /pg/ prefix
        self.meta_base_url = (meta_url or f"{self.base_url}/pg").rstrip("/")
        self.meta_auth = meta_auth

        # Initialize logger
        self.logger = logging.getLogger(f"SupabaseClient.{base_url}")

    def _get_headers(
        self, use_service_role: bool = False, additional_headers: dict | None = None
    ) -> dict[str, str]:
        """
        Get request headers with API key authentication.

        Args:
            use_service_role: Use service_role_key (bypasses RLS)
            additional_headers: Additional headers to include

        Returns:
            Dict: Headers with authentication
        """
        # If anon_key is not configured, fall back to service_role_key for all calls
        key = self.service_role_key if (use_service_role or not self.anon_key) else self.anon_key

        headers = {
            "apikey": key,
            "Authorization": f"Bearer {key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

        if additional_headers:
            headers.update(additional_headers)

        return headers

    def _get_meta_headers(self, additional_headers: dict | None = None) -> dict[str, str]:
        """Get headers for postgres-meta requests (Basic Auth if configured, else JWT)."""
        headers: dict[str, str] = {
            "Content-Type": "application/json",
            "Accept": "application/json",
        }
        if self.meta_auth:
            encoded = base64.b64encode(self.meta_auth.encode()).decode()
            headers["Authorization"] = f"Basic {encoded}"
        else:
            key = self.service_role_key
            headers["apikey"] = key
            headers["Authorization"] = f"Bearer {key}"
        if additional_headers:
            headers.update(additional_headers)
        return headers

    async def _head_request_headers(
        self,
        endpoint: str,
        params: dict | None = None,
        headers_override: dict | None = None,
        use_service_role: bool = False,
        base_url_override: str | None = None,
    ) -> dict[str, str]:
        """
        Make HEAD request and return response headers.

        Used for operations like count_rows where the result is in a response
        header (Content-Range) rather than the body.
        """
        url = f"{base_url_override or self.base_url}{endpoint}"
        headers = self._get_headers(use_service_role, headers_override)
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        async with aiohttp.ClientSession() as session:
            async with session.head(url, params=params or None, headers=headers) as response:
                # Normalise to lowercase so callers can use consistent keys
                # (aiohttp CIMultiDictProxy is case-insensitive but dict() is not)
                return {k.lower(): v for k, v in response.headers.items()}

    async def request(
        self,
        method: str,
        endpoint: str,
        params: dict | None = None,
        json_data: Any = None,
        data: bytes | None = None,
        headers_override: dict | None = None,
        use_service_role: bool = False,
        base_url_override: str | None = None,
    ) -> Any:
        """
        Make authenticated request to Supabase API.

        Args:
            method: HTTP method
            endpoint: API endpoint (with leading /)
            params: Query parameters
            json_data: JSON body data (dict or list)
            data: Raw binary data (for file uploads)
            headers_override: Override/add headers
            use_service_role: Use service_role_key
            base_url_override: Override base URL (used for postgres-meta calls)

        Returns:
            API response

        Raises:
            Exception: On API errors
        """
        url = f"{base_url_override or self.base_url}{endpoint}"

        # Use Basic Auth headers for postgres-meta requests when meta_auth is configured
        if base_url_override and base_url_override == self.meta_base_url:
            headers = self._get_meta_headers(headers_override)
        else:
            headers = self._get_headers(use_service_role, headers_override)

        # Remove Content-Type for binary data
        if data is not None:
            headers.pop("Content-Type", None)

        # Filter None values from params and dict json bodies
        if params:
            params = {k: v for k, v in params.items() if v is not None}
        if json_data and isinstance(json_data, dict):
            json_data = {k: v for k, v in json_data.items() if v is not None}

        self.logger.debug(f"{method} {url}")

        async with aiohttp.ClientSession() as session:
            kwargs = {
                "method": method,
                "url": url,
                "headers": headers,
            }

            if params:
                kwargs["params"] = params
            if json_data is not None:
                kwargs["json"] = json_data
            if data:
                kwargs["data"] = data

            async with session.request(**kwargs) as response:
                self.logger.debug(f"Response status: {response.status}")

                # Handle 204 No Content
                if response.status == 204:
                    return {"success": True}

                # Handle binary responses (file downloads)
                content_type = response.headers.get("Content-Type", "")
                if "application/json" not in content_type and response.status < 400:
                    # Return binary data as base64
                    binary_data = await response.read()
                    return {
                        "data": base64.b64encode(binary_data).decode(),
                        "content_type": content_type,
                        "size": len(binary_data),
                    }

                # Parse JSON response
                try:
                    response_data = await response.json()
                except Exception:
                    response_text = await response.text()
                    if response.status >= 400:
                        raise Exception(
                            f"Supabase API error (status {response.status}): {response_text}"
                        )
                    return {"success": True, "message": response_text}

                # Check for errors
                if response.status >= 400:
                    error_msg = self._extract_error_message(response_data)
                    raise Exception(f"Supabase API error (status {response.status}): {error_msg}")

                return response_data

    def _extract_error_message(self, response_data: Any) -> str:
        """Extract error message from various response formats."""
        if isinstance(response_data, dict):
            # PostgREST error format — includes code, details, hint
            if "message" in response_data:
                parts = [response_data["message"]]
                if response_data.get("code"):
                    parts.insert(0, f"[{response_data['code']}]")
                if response_data.get("details"):
                    parts.append(f"Details: {response_data['details']}")
                if response_data.get("detail"):
                    parts.append(f"Detail: {response_data['detail']}")
                if response_data.get("hint"):
                    parts.append(f"Hint: {response_data['hint']}")
                return " | ".join(parts)
            # GoTrue error format
            if "error_description" in response_data:
                return response_data["error_description"]
            if "msg" in response_data:
                return response_data["msg"]
            # postgres-meta error format — "error" key with optional PG fields
            if "error" in response_data:
                parts = [str(response_data["error"])]
                if response_data.get("code"):
                    parts.insert(0, f"[{response_data['code']}]")
                if response_data.get("hint"):
                    parts.append(f"Hint: {response_data['hint']}")
                if response_data.get("detail"):
                    parts.append(f"Detail: {response_data['detail']}")
                if response_data.get("position"):
                    parts.append(f"Position: {response_data['position']}")
                return " | ".join(parts)
        return str(response_data)

    def _build_filter_params(self, filters: list[dict]) -> dict[str, str]:
        """
        Convert a filter list to PostgREST query parameters.

        Handles special cases:
        - ``is`` operator with Python ``None`` → ``is.null``
        - ``is`` operator with Python bool → ``is.true`` / ``is.false``
        - ``in`` operator with a list → ``in.(a,b,c)``
        - Any other operator with ``None`` value is skipped (prevents sending
          a bare ``col=eq.None`` which PostgREST would reject).
        """
        params: dict[str, str] = {}
        for f in filters:
            col = f.get("column")
            op = f.get("operator", "eq")
            val = f.get("value")
            if not col:
                continue
            if val is None:
                # Only ``is`` / ``not.is`` accept null
                if op in ("is", "not.is"):
                    val = "null"
                else:
                    continue
            elif isinstance(val, bool):
                # Convert Python bool to lowercase string for PostgREST
                val = "true" if val else "false"
            elif op == "in" and isinstance(val, list):
                # PostgREST ``in`` format: status=in.(active,inactive)
                val = f"({','.join(str(v) for v in val)})"
            params[col] = f"{op}.{val}"
        return params

    # =====================
    # POSTGREST (Database)
    # =====================

    async def query_table(
        self,
        table: str,
        select: str = "*",
        filters: list[dict] | None = None,
        order: str | None = None,
        limit: int = 100,
        offset: int = 0,
        use_service_role: bool = False,
    ) -> list[dict]:
        """
        Query data from a table.

        Args:
            table: Table name
            select: Columns to select
            filters: List of filter conditions
            order: Order by clause (e.g., "created_at.desc")
            limit: Maximum rows
            offset: Offset for pagination
        """
        params: dict[str, Any] = {"select": select, "limit": limit, "offset": offset}

        if order:
            params["order"] = order

        if filters:
            params.update(self._build_filter_params(filters))

        headers = {}

        # Request single objects as array
        headers["Accept"] = "application/json"

        return await self.request(
            "GET",
            f"/rest/v1/{table}",
            params=params,
            headers_override=headers,
            use_service_role=use_service_role,
        )

    async def insert_rows(
        self,
        table: str,
        rows: list[dict],
        upsert: bool = False,
        on_conflict: str | None = None,
        use_service_role: bool = False,
    ) -> list[dict]:
        """Insert rows into a table."""
        headers = {"Prefer": "return=representation"}
        upsert_params: dict[str, str] = {}

        if upsert:
            headers["Prefer"] = "return=representation,resolution=merge-duplicates"
            if on_conflict:
                # PostgREST expects on_conflict as a query parameter, not a header
                upsert_params["on_conflict"] = on_conflict

        return await self.request(
            "POST",
            f"/rest/v1/{table}",
            params=upsert_params or None,
            json_data=rows if isinstance(rows, list) else [rows],
            headers_override=headers,
            use_service_role=use_service_role,
        )

    async def update_rows(
        self, table: str, data: dict, filters: list[dict], use_service_role: bool = False
    ) -> list[dict]:
        """Update rows matching filters."""
        params = self._build_filter_params(filters)
        headers = {"Prefer": "return=representation"}

        return await self.request(
            "PATCH",
            f"/rest/v1/{table}",
            params=params,
            json_data=data,
            headers_override=headers,
            use_service_role=use_service_role,
        )

    async def delete_rows(
        self, table: str, filters: list[dict], use_service_role: bool = False
    ) -> list[dict]:
        """Delete rows matching filters."""
        params = self._build_filter_params(filters)
        headers = {"Prefer": "return=representation"}

        return await self.request(
            "DELETE",
            f"/rest/v1/{table}",
            params=params,
            headers_override=headers,
            use_service_role=use_service_role,
        )

    async def execute_rpc(
        self, function_name: str, params: dict | None = None, use_service_role: bool = False
    ) -> Any:
        """Execute a stored procedure/function."""
        return await self.request(
            "POST",
            f"/rest/v1/rpc/{function_name}",
            json_data=params or {},
            use_service_role=use_service_role,
        )

    async def count_rows(
        self, table: str, filters: list[dict] | None = None, use_service_role: bool = False
    ) -> int:
        """Count rows in a table using HEAD + Content-Range header."""
        filter_params = self._build_filter_params(filters) if filters else {}

        response_headers = await self._head_request_headers(
            f"/rest/v1/{table}",
            params=filter_params or None,
            headers_override={"Prefer": "count=exact"},
            use_service_role=use_service_role,
        )

        # PostgREST returns count in Content-Range: 0-N/TOTAL or */TOTAL
        content_range = response_headers.get("content-range", "")
        if "/" in content_range:
            try:
                total = content_range.split("/")[-1]
                if total != "*":
                    return int(total)
            except (ValueError, IndexError):
                pass
        return 0

    # =====================
    # POSTGRES-META (Admin)
    # =====================
    # All postgres-meta requests use self.meta_base_url (default: {base_url}/pg).
    # Set META_URL env var for direct postgres-meta access (e.g., http://host:5555).
    # Note: postgres-meta /columns, /policies, /triggers do NOT support filtering
    # by table_name as a query param — we filter the results in Python instead.

    async def list_tables(self, schema: str = "public") -> list[dict]:
        """List all tables via postgres-meta."""
        return await self.request(
            "GET",
            "/tables",
            params={"include_system_schemas": "false"},
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    async def get_table_schema(self, table: str, schema: str = "public") -> dict:
        """Get table schema/columns via postgres-meta, filtered by table+schema in Python."""
        all_columns = await self.request(
            "GET",
            "/columns",
            params={"include_system_schemas": "false"},
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )
        # postgres-meta /columns does not support table_name filtering — filter here
        columns = [
            col
            for col in (all_columns if isinstance(all_columns, list) else [])
            if col.get("table") == table and col.get("schema") == schema
        ]
        return {"table": table, "schema": schema, "columns": columns}

    async def list_schemas(self) -> list[dict]:
        """List all database schemas via postgres-meta."""
        return await self.request(
            "GET",
            "/schemas",
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    async def list_extensions(self) -> list[dict]:
        """List installed extensions via postgres-meta."""
        return await self.request(
            "GET",
            "/extensions",
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    async def list_policies(self, table: str | None = None) -> list[dict]:
        """List RLS policies via postgres-meta, optionally filtered by table name."""
        all_policies = await self.request(
            "GET",
            "/policies",
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )
        # postgres-meta /policies does not support table_name filtering — filter here
        if table and isinstance(all_policies, list):
            return [p for p in all_policies if p.get("table") == table]
        return all_policies

    async def list_roles(self) -> list[dict]:
        """List database roles via postgres-meta."""
        return await self.request(
            "GET",
            "/roles",
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    async def list_triggers(self, table: str | None = None) -> list[dict]:
        """List triggers via postgres-meta, optionally filtered by table name."""
        all_triggers = await self.request(
            "GET",
            "/triggers",
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )
        # postgres-meta /triggers does not support table_name filtering — filter here
        if table and isinstance(all_triggers, list):
            return [t for t in all_triggers if t.get("table") == table]
        return all_triggers

    async def list_functions(self, schema: str = "public") -> list[dict]:
        """List database functions via postgres-meta."""
        return await self.request(
            "GET",
            "/functions",
            params={"schema": schema},
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    async def execute_sql(self, query: str) -> Any:
        """Execute raw SQL query via postgres-meta."""
        return await self.request(
            "POST",
            "/query",
            json_data={"query": query},
            use_service_role=True,
            base_url_override=self.meta_base_url,
        )

    # =====================
    # GOTRUE (Auth)
    # =====================

    async def list_users(self, page: int = 1, per_page: int = 50) -> dict[str, Any]:
        """List all users (admin)."""
        return await self.request(
            "GET",
            "/auth/v1/admin/users",
            params={"page": page, "per_page": per_page},
            use_service_role=True,
        )

    async def get_user(self, user_id: str) -> dict[str, Any]:
        """Get user by ID."""
        return await self.request("GET", f"/auth/v1/admin/users/{user_id}", use_service_role=True)

    async def create_user(
        self,
        email: str,
        password: str,
        email_confirm: bool = False,
        phone: str | None = None,
        user_metadata: dict | None = None,
        app_metadata: dict | None = None,
    ) -> dict[str, Any]:
        """Create a new user."""
        data = {"email": email, "password": password, "email_confirm": email_confirm}
        if phone:
            data["phone"] = phone
        if user_metadata:
            data["user_metadata"] = user_metadata
        if app_metadata:
            data["app_metadata"] = app_metadata

        return await self.request(
            "POST", "/auth/v1/admin/users", json_data=data, use_service_role=True
        )

    async def update_user(
        self,
        user_id: str,
        email: str | None = None,
        password: str | None = None,
        phone: str | None = None,
        email_confirm: bool | None = None,
        phone_confirm: bool | None = None,
        user_metadata: dict | None = None,
        app_metadata: dict | None = None,
        ban_duration: str | None = None,
    ) -> dict[str, Any]:
        """Update user."""
        data = {}
        if email:
            data["email"] = email
        if password:
            data["password"] = password
        if phone:
            data["phone"] = phone
        if email_confirm is not None:
            data["email_confirm"] = email_confirm
        if phone_confirm is not None:
            data["phone_confirm"] = phone_confirm
        if user_metadata:
            data["user_metadata"] = user_metadata
        if app_metadata:
            data["app_metadata"] = app_metadata
        if ban_duration:
            data["ban_duration"] = ban_duration

        return await self.request(
            "PUT", f"/auth/v1/admin/users/{user_id}", json_data=data, use_service_role=True
        )

    async def delete_user(self, user_id: str) -> dict[str, Any]:
        """Delete a user."""
        return await self.request(
            "DELETE", f"/auth/v1/admin/users/{user_id}", use_service_role=True
        )

    async def generate_link(
        self, email: str, link_type: str = "magiclink", redirect_to: str | None = None
    ) -> dict[str, Any]:
        """Generate magic link or recovery link."""
        data = {"email": email, "type": link_type}
        if redirect_to:
            data["redirect_to"] = redirect_to

        return await self.request(
            "POST", "/auth/v1/admin/generate_link", json_data=data, use_service_role=True
        )

    async def list_user_factors(self, user_id: str) -> list[dict]:
        """List user MFA factors."""
        return await self.request(
            "GET", f"/auth/v1/admin/users/{user_id}/factors", use_service_role=True
        )

    async def delete_user_factor(self, user_id: str, factor_id: str) -> dict:
        """Delete a user's MFA factor."""
        return await self.request(
            "DELETE", f"/auth/v1/admin/users/{user_id}/factors/{factor_id}", use_service_role=True
        )

    # =====================
    # STORAGE
    # =====================

    async def list_buckets(self) -> list[dict]:
        """List all storage buckets."""
        return await self.request("GET", "/storage/v1/bucket", use_service_role=True)

    async def get_bucket(self, bucket_id: str) -> dict:
        """Get bucket details."""
        return await self.request("GET", f"/storage/v1/bucket/{bucket_id}", use_service_role=True)

    async def create_bucket(
        self,
        name: str,
        public: bool = False,
        file_size_limit: int | None = None,
        allowed_mime_types: list[str] | None = None,
    ) -> dict:
        """Create a new bucket."""
        data = {"name": name, "public": public}
        if file_size_limit:
            data["file_size_limit"] = file_size_limit
        if allowed_mime_types:
            data["allowed_mime_types"] = allowed_mime_types

        return await self.request(
            "POST", "/storage/v1/bucket", json_data=data, use_service_role=True
        )

    async def update_bucket(
        self,
        bucket_id: str,
        public: bool | None = None,
        file_size_limit: int | None = None,
        allowed_mime_types: list[str] | None = None,
    ) -> dict:
        """Update bucket settings."""
        data = {}
        if public is not None:
            data["public"] = public
        if file_size_limit:
            data["file_size_limit"] = file_size_limit
        if allowed_mime_types:
            data["allowed_mime_types"] = allowed_mime_types

        return await self.request(
            "PUT", f"/storage/v1/bucket/{bucket_id}", json_data=data, use_service_role=True
        )

    async def delete_bucket(self, bucket_id: str) -> dict:
        """Delete a bucket."""
        return await self.request(
            "DELETE", f"/storage/v1/bucket/{bucket_id}", use_service_role=True
        )

    async def empty_bucket(self, bucket_id: str) -> dict:
        """Empty a bucket (delete all files)."""
        return await self.request(
            "POST",
            f"/storage/v1/bucket/{bucket_id}/empty",
            json_data={},
            use_service_role=True,
        )

    async def list_files(
        self, bucket: str, path: str = "", limit: int = 100, offset: int = 0
    ) -> list[dict]:
        """List files in a bucket/path."""
        data = {"prefix": path, "limit": limit, "offset": offset}
        return await self.request(
            "POST", f"/storage/v1/object/list/{bucket}", json_data=data, use_service_role=True
        )

    async def upload_file(
        self,
        bucket: str,
        path: str,
        content: bytes,
        content_type: str = "application/octet-stream",
        upsert: bool = False,
    ) -> dict:
        """Upload a file."""
        headers = {"Content-Type": content_type}
        if upsert:
            headers["x-upsert"] = "true"

        return await self.request(
            "POST",
            f"/storage/v1/object/{bucket}/{path}",
            data=content,
            headers_override=headers,
            use_service_role=True,
        )

    async def download_file(self, bucket: str, path: str) -> dict:
        """Download a file (returns base64)."""
        return await self.request(
            "GET", f"/storage/v1/object/{bucket}/{path}", use_service_role=True
        )

    async def delete_files(self, bucket: str, paths: list[str]) -> dict:
        """Delete files from bucket."""
        return await self.request(
            "DELETE",
            f"/storage/v1/object/{bucket}",
            json_data={"prefixes": paths},
            use_service_role=True,
        )

    async def move_file(self, bucket: str, from_path: str, to_path: str) -> dict:
        """Move/rename a file."""
        return await self.request(
            "POST",
            "/storage/v1/object/move",
            json_data={"bucketId": bucket, "sourceKey": from_path, "destinationKey": to_path},
            use_service_role=True,
        )

    async def get_public_url(self, bucket: str, path: str) -> str:
        """Get public URL for a file."""
        return f"{self.base_url}/storage/v1/object/public/{bucket}/{path}"

    # =====================
    # EDGE FUNCTIONS
    # =====================

    async def invoke_function(
        self, function_name: str, body: dict | None = None, method: str = "POST"
    ) -> Any:
        """Invoke an Edge Function."""
        return await self.request(
            method,
            f"/functions/v1/{function_name}",
            json_data=body,
            use_service_role=False,  # Use anon key for functions
        )

    # =====================
    # HEALTH CHECK
    # =====================

    async def health_check(self) -> dict[str, Any]:
        """Check Supabase instance health."""
        results = {"healthy": True, "services": {}}

        # Check PostgREST
        try:
            await self.request("GET", "/rest/v1/", use_service_role=True)
            results["services"]["postgrest"] = "ok"
        except Exception as e:
            # A 404 on the PostgREST root still confirms connectivity — some versions
            # return 404 for the root endpoint while working normally for table queries.
            if "status 404" in str(e).lower() or "404" in str(e):
                results["services"]["postgrest"] = "ok"
            else:
                results["services"]["postgrest"] = f"error: {str(e)}"
                results["healthy"] = False

        # Check GoTrue
        try:
            await self.request("GET", "/auth/v1/health", use_service_role=False)
            results["services"]["gotrue"] = "ok"
        except Exception as e:
            results["services"]["gotrue"] = f"error: {str(e)}"
            results["healthy"] = False

        # Check Storage
        try:
            await self.list_buckets()
            results["services"]["storage"] = "ok"
        except Exception as e:
            results["services"]["storage"] = f"error: {str(e)}"
            results["healthy"] = False

        return results
