"""
Poping SDK client – lightweight wrapper over backend HTTP API.
"""

from __future__ import annotations

import json
import os
from typing import Any, Dict, Iterable, List, Optional

import requests

from .exceptions import (
    APIError,
    AuthenticationError,
    RateLimitError,
    ResourceNotFoundError,
)


class _HTTPClient:
    """Low-level HTTP client with basic error handling."""

    def __init__(self, api_key: str, base_url: str, timeout: int = 310):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    def request(
        self,
        method: str,
        endpoint: str,
        *,
        data: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        files: Optional[Dict[str, Any]] = None,
        stream: bool = False,
    ):
        url = f"{self.base_url}{endpoint}"
        try:
            resp = self.session.request(
                method=method,
                url=url,
                json=data if not files else None,
                data=data if files else None,
                params=params,
                files=files,
                timeout=self.timeout,
                stream=stream,
            )
        except requests.RequestException as exc:
            raise APIError(status_code=0, message=f"Network error: {exc}")

        if resp.status_code == 401:
            raise AuthenticationError(401, "Invalid API key")
        if resp.status_code == 429:
            retry_after = resp.headers.get("Retry-After")
            raise RateLimitError(retry_after=retry_after)
        if resp.status_code == 404:
            raise ResourceNotFoundError(404, "Resource not found", {})
        if resp.status_code >= 400:
            try:
                err = resp.json()
            except Exception:
                err = {"message": f"HTTP {resp.status_code} error"}
            raise APIError(
                status_code=resp.status_code,
                message=err.get("message") or err.get("detail") or f"HTTP {resp.status_code} error",
                details=err,
            )

        return resp if stream else resp.json()


class PopingClient:
    """
    High-level client mirroring reference SDK shape but backed by our API.
    """

    def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: int = 310):
        key = api_key or os.getenv("POPING_API_KEY")
        if not key:
            raise AuthenticationError(401, "API key required")
        url = base_url or os.getenv("POPING_BASE_URL") or "http://localhost:8000"
        self.http = _HTTPClient(key, url, timeout=timeout)

    # ------------------------------------------------------------------ #
    # Agents
    # ------------------------------------------------------------------ #
    def create_agent(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/agents", data=payload)

    def list_agents(self, project_id: Optional[str] = None, name: Optional[str] = None) -> List[Dict[str, Any]]:
        params = {}
        if project_id:
            params["project_id"] = project_id
        if name:
            params["name"] = name
        return self.http.request("GET", "/api/v1/agents", params=params or None)

    def get_agent(self, agent_id: str, project_id: Optional[str] = None) -> Dict[str, Any]:
        params = {"project_id": project_id} if project_id else None
        return self.http.request("GET", f"/api/v1/agents/{agent_id}", params=params)

    def update_agent(self, agent_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/agents/{agent_id}", data=payload)

    def delete_agent(self, agent_id: str, project_id: Optional[str] = None) -> Dict[str, Any]:
        params = {"project_id": project_id} if project_id else None
        return self.http.request("DELETE", f"/api/v1/agents/{agent_id}", params=params)

    def chat(self, agent_id: str, *, message: str, session_id: Optional[str] = None, project_id: Optional[str] = None, end_user_id: Optional[str] = None, response_model_schema: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        payload: Dict[str, Any] = {"message": message, "session_id": session_id, "project_id": project_id, "end_user_id": end_user_id}
        if response_model_schema:
            payload["response_model_schema"] = response_model_schema
        return self.http.request("POST", f"/api/v1/agents/{agent_id}/chat", data=payload)

    def chat_stream(self, agent_id: str, *, message: str, session_id: Optional[str] = None, project_id: Optional[str] = None, end_user_id: Optional[str] = None) -> Iterable[bytes]:
        payload = {"message": message, "session_id": session_id, "project_id": project_id, "end_user_id": end_user_id}
        resp = self.http.request("POST", f"/api/v1/agents/{agent_id}/chat/stream", data=payload, stream=True)
        return resp.iter_lines()

    def chat_stream_events(self, agent_id: str, **kwargs) -> Iterable[Dict[str, Any]]:
        """Yield parsed NDJSON events from streaming chat."""
        try:
            for line in self.chat_stream(agent_id, **kwargs):
                if not line:
                    continue
                try:
                    yield json.loads(line.decode("utf-8") if isinstance(line, (bytes, bytearray)) else line)
                except json.JSONDecodeError:
                    continue
        except Exception:
            # Handle stream termination gracefully (ChunkedEncodingError, etc.)
            return

    # ------------------------------------------------------------------ #
    # Sessions
    # ------------------------------------------------------------------ #
    def append_session_message(self, agent_id: str, session_id: str, *, role: str, content: str, project_id: Optional[str] = None, end_user_id: Optional[str] = None) -> Dict[str, Any]:
        payload = {
            "role": role,
            "content": content,
            "project_id": project_id,
            "end_user_id": end_user_id,
        }
        return self.http.request("PUT", f"/api/v1/agents/{agent_id}/sessions/{session_id}/message", data=payload)

    def create_session(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/sessions", data=payload)

    def list_sessions(self, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", "/api/v1/sessions", params=params)

    def get_session(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}", params=params)

    def update_session(self, session_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/sessions/{session_id}", data=payload)

    def delete_session(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/sessions/{session_id}", params=params)

    def list_messages(self, session_id: str, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}/messages", params=params)

    def get_message(self, session_id: str, message_key: int, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}/messages/{message_key}", params=params)

    def delete_message(self, session_id: str, message_key: int, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/sessions/{session_id}/messages/{message_key}", params=params)

    def token_usage(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}/token-usage", params=params)

    def list_artifacts(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}/artifacts", params=params)

    def upload_artifact(self, session_id: str, file_path: str, **params) -> Dict[str, Any]:
        with open(file_path, "rb") as f:
            files = {"file": (os.path.basename(file_path), f)}
            return self.http.request("POST", f"/api/v1/sessions/{session_id}/artifacts", params=params, files=files)

    def delete_artifacts(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/sessions/{session_id}/artifacts", params=params)

    def grouped_artifacts(self, session_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/sessions/{session_id}/artifacts/grouped", params=params)

    def download_artifact(self, session_id: str, filename: str, **params) -> bytes:
        params = {"filename": filename, **params}
        resp = self.http.request("GET", f"/api/v1/sessions/{session_id}/artifacts/download", params=params, stream=True)
        return resp.content

    # ------------------------------------------------------------------ #
    # Projects
    # ------------------------------------------------------------------ #
    def list_projects(self) -> List[Dict[str, Any]]:
        return self.http.request("GET", "/api/v1/projects")

    def create_project(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/projects", data=payload)

    def update_project(self, project_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/projects/{project_id}", data=payload)

    def delete_project(self, project_id: str) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/projects/{project_id}")

    # ------------------------------------------------------------------ #
    # Datasets (DB + storage dual write on backend)
    # ------------------------------------------------------------------ #
    def list_datasets(self, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", "/api/v1/datasets", params=params)

    def create_dataset(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/datasets", data=payload)

    def get_dataset(self, dataset_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/datasets/{dataset_id}", params=params)

    def update_dataset(self, dataset_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/datasets/{dataset_id}", data=payload)

    def delete_dataset(self, dataset_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/datasets/{dataset_id}", params=params)

    def add_record(self, dataset_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("POST", f"/api/v1/datasets/{dataset_id}/records", data=payload)

    def list_records(self, dataset_id: str, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", f"/api/v1/datasets/{dataset_id}/records", params=params)

    def get_record(self, dataset_id: str, record_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/datasets/{dataset_id}/records/{record_id}", params=params)

    def delete_record(self, dataset_id: str, record_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/datasets/{dataset_id}/records/{record_id}", params=params)

    # ------------------------------------------------------------------ #
    # Knowledge (DB metadata + storage content)
    # ------------------------------------------------------------------ #
    def list_kbs(self, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", "/api/v1/knowledge", params=params)

    def create_kb(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/knowledge", data=payload)

    def get_kb(self, kb_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/knowledge/{kb_id}", params=params)

    def update_kb(self, kb_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/knowledge/{kb_id}", data=payload)

    def delete_kb(self, kb_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/knowledge/{kb_id}", params=params)

    def upload_document(self, kb_id: str, file_path: str, **params) -> Dict[str, Any]:
        with open(file_path, "rb") as f:
            files = {"file": (os.path.basename(file_path), f)}
            return self.http.request("POST", f"/api/v1/knowledge/{kb_id}/documents", params=params, files=files)

    def list_documents(self, kb_id: str, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", f"/api/v1/knowledge/{kb_id}/documents", params=params)

    def get_document_meta(self, kb_id: str, document_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/knowledge/{kb_id}/documents/{document_id}", params=params)

    def delete_document(self, kb_id: str, document_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/knowledge/{kb_id}/documents/{document_id}", params=params)

    def download_document(self, kb_id: str, document_id: str, dest_path: str, **params) -> str:
        resp = self.http.request("GET", f"/api/v1/knowledge/{kb_id}/documents/{document_id}/download", params=params, stream=True)
        with open(dest_path, "wb") as f:
            for chunk in resp.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
        return dest_path

    # ------------------------------------------------------------------ #
    # Storage (user/shared)
    # ------------------------------------------------------------------ #
    def create_folder(self, name: str, **params) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/storage/folders", data={"name": name, **params})

    def list_folders(self, **params) -> List[str]:
        return self.http.request("GET", "/api/v1/storage/folders", params=params)

    def rename_folder(self, folder_name: str, new_name: str, **params) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/storage/folders/{folder_name}", data={"new_name": new_name, **params})

    def delete_folder(self, folder_name: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/storage/folders/{folder_name}", params=params)

    def upload_file(self, file_path: str, folder: Optional[str] = None, **params) -> Dict[str, Any]:
        with open(file_path, "rb") as f:
            files = {"file": (os.path.basename(file_path), f)}
            query = {"folder": folder, **params}
            query = {k: v for k, v in query.items() if v is not None}
            return self.http.request("POST", "/api/v1/storage/files", params=query, files=files)

    def list_files(self, **params) -> Dict[str, Any]:
        return self.http.request("GET", "/api/v1/storage/files", params=params)

    def download_file(self, file_id: str, dest_path: str, **params) -> str:
        resp = self.http.request("GET", f"/api/v1/storage/files/{file_id}", params=params, stream=True)
        with open(dest_path, "wb") as f:
            for chunk in resp.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
        return dest_path

    def delete_file(self, file_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/storage/files/{file_id}", params=params)

    # Shared
    def create_shared_folder(self, name: str, **params) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/storage/shared/folders", data={"name": name, **params})

    def rename_shared_folder(self, folder_name: str, new_name: str, **params) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/storage/shared/folders/{folder_name}", data={"new_name": new_name, **params})

    def delete_shared_folder(self, folder_name: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/storage/shared/folders/{folder_name}", params=params)

    def upload_shared_file(self, file_path: str, folder: Optional[str] = None, **params) -> Dict[str, Any]:
        with open(file_path, "rb") as f:
            files = {"file": (os.path.basename(file_path), f)}
            query = {"folder": folder, **params}
            query = {k: v for k, v in query.items() if v is not None}
            return self.http.request("POST", "/api/v1/storage/shared/files", params=query, files=files)

    def list_shared_files(self, **params) -> Dict[str, Any]:
        return self.http.request("GET", "/api/v1/storage/shared/files", params=params)

    def download_shared_file(self, file_id: str, dest_path: str, **params) -> str:
        resp = self.http.request("GET", f"/api/v1/storage/shared/files/{file_id}", params=params, stream=True)
        with open(dest_path, "wb") as f:
            for chunk in resp.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
        return dest_path

    def delete_shared_file(self, file_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/storage/shared/files/{file_id}", params=params)

    # ------------------------------------------------------------------ #
    # MCP
    # ------------------------------------------------------------------ #
    def list_mcps(self, **params) -> List[Dict[str, Any]]:
        return self.http.request("GET", "/api/v1/mcps", params=params)

    def create_mcp(self, **payload) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/mcps", data=payload)

    def get_mcp(self, mcp_id: str, **params) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/mcps/{mcp_id}", params=params)

    def update_mcp(self, mcp_id: str, **payload) -> Dict[str, Any]:
        return self.http.request("PATCH", f"/api/v1/mcps/{mcp_id}", data=payload)

    def delete_mcp(self, mcp_id: str, **params) -> Dict[str, Any]:
        return self.http.request("DELETE", f"/api/v1/mcps/{mcp_id}", params=params)

    # ------------------------------------------------------------------ #
    # Market / payments / admin
    # ------------------------------------------------------------------ #
    def list_market(self) -> Dict[str, Any]:
        return self.http.request("GET", "/api/v1/tools/market")

    def create_payment(self, amount: float, subject: str = "Top-up", return_url: Optional[str] = None) -> Dict[str, Any]:
        return self.http.request("POST", "/api/v1/payments/create", data={"amount": amount, "subject": subject, "return_url": return_url})

    def payment_status(self, order_id: str) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/v1/payments/status/{order_id}")

    # Admin
    def admin_list_users(self, **params) -> Dict[str, Any]:
        return self.http.request("GET", "/api/admin/users", params=params)

    def admin_set_user_status(self, user_id: str, enabled: bool) -> Dict[str, Any]:
        return self.http.request("PUT", f"/api/admin/users/{user_id}/status", data={"enabled": enabled})

    def admin_adjust_points(self, user_id: str, delta: float, reason: Optional[str] = None) -> Dict[str, Any]:
        return self.http.request("POST", f"/api/admin/users/{user_id}/points/adjust", data={"delta": delta, "reason": reason})

    def admin_set_multiplier(self, user_id: str, points_multiplier: float) -> Dict[str, Any]:
        return self.http.request("PUT", f"/api/admin/users/{user_id}/multiplier", data={"points_multiplier": points_multiplier})

    def admin_user_analytics(self, user_id: str) -> Dict[str, Any]:
        return self.http.request("GET", f"/api/admin/users/{user_id}/analytics")


# ==============================================================================
# Poping - Class-based API (Alternative to Module-level API)
# ==============================================================================

class Poping:
    """
    Poping SDK client (class-based API)

    This is an alternative to the module-level API (poping.set() / poping.agent()).
    Use this when you need multiple client instances or prefer explicit instantiation.

    [Class: Poping]
    ===============
    - Input: api_key, base_url
    - Output: Client instance with agent() method
    - Role in Flow: Alternative entry point for SDK usage
    - Logic:
        1. Initialize with credentials
        2. Expose agent() method that returns AgentBuilder
        3. Expose convenience methods (list_agents, etc.)

    Example:
        from poping import Poping

        # Create client instance
        poping = Poping(api_key="your_api_key")

        # Create agent
        agent = poping.agent(llm="claude-3-5-sonnet-20241022").build(name="assistant")

        # Chat
        with agent.session(end_user_id="user_123") as conv:
            response = conv.chat("Hello!")
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: int = 310,
    ):
        """
        Initialize Poping client

        Args:
            api_key: API key for authentication (or set POPING_API_KEY env var)
            base_url: Backend URL (default: http://localhost:8000 or POPING_BASE_URL env var)
            timeout: Request timeout in seconds (default: 310)

        Raises:
            AuthenticationError: If API key not provided and not in environment
        """
        key = api_key or os.getenv("POPING_API_KEY")
        if not key:
            raise AuthenticationError(401, "API key required")

        url = base_url or os.getenv("POPING_BASE_URL", "http://localhost:8000")

        # Internal HTTP client
        self._client = PopingClient(api_key=key, base_url=url, timeout=timeout)

    def agent(
        self,
        agent_id_or_llm: Optional[str] = None,
        *,
        llm: Optional[str] = None,
        project: Optional[str] = None,
    ):
        """
        Create new agent or load existing agent

        This method is overloaded to support multiple use cases:

        1. Create new agent:
           poping.agent(llm="claude-3-5-sonnet-20241022")
           poping.agent("claude-3-5-sonnet-20241022")  # shorthand

        2. Load by ID:
           poping.agent("agt_abc123")

        Args:
            agent_id_or_llm: Agent ID (if starts with "agt_"), or LLM model name
            llm: LLM model (for creating new agent)
            project: Project ID scope

        Returns:
            AgentBuilder instance

        Example:
            # Create new agent
            agent = poping.agent(llm="claude-3-5-sonnet-20241022").build(name="assistant")

            # Load by ID
            agent = poping.agent("agt_abc123")
        """
        from .agent import AgentBuilder

        # Case 1: Load by ID (starts with "agt_")
        if agent_id_or_llm and agent_id_or_llm.startswith("agt_"):
            return AgentBuilder(
                client=self._client,
                agent_id=agent_id_or_llm,
                project_id=project,
            )

        # Case 2: Create new agent
        llm_final = llm or agent_id_or_llm
        if not llm_final:
            raise ValueError("Either agent_id_or_llm or llm parameter required")

        return AgentBuilder(
            client=self._client,
            llm=llm_final,
            project_id=project,
        )

    def list_agents(self, project: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        List all agents for current user

        Args:
            project: Project ID filter (optional)

        Returns:
            List of agent summaries with metadata

        Example:
            agents = poping.list_agents()
            for agent in agents:
                print(f"{agent['name']}: {agent['agent_id']}")
        """
        return self._client.list_agents(project_id=project)

    # Expose common client methods
    def create_agent(self, **payload) -> Dict[str, Any]:
        """Create agent (low-level - prefer agent().build())"""
        return self._client.create_agent(**payload)

    def get_agent(self, agent_id: str, project_id: Optional[str] = None) -> Dict[str, Any]:
        """Get agent details"""
        return self._client.get_agent(agent_id, project_id=project_id)

    def delete_agent(self, agent_id: str, project_id: Optional[str] = None) -> Dict[str, Any]:
        """Delete agent"""
        return self._client.delete_agent(agent_id, project_id=project_id)

    def list_projects(self) -> List[Dict[str, Any]]:
        """List all projects"""
        return self._client.list_projects()

    def create_project(self, **payload) -> Dict[str, Any]:
        """Create new project"""
        return self._client.create_project(**payload)


__all__ = ["PopingClient", "Poping"]
