"""API client for Promethian central server."""

from __future__ import annotations

import os
from datetime import datetime
from dataclasses import dataclass, field
from typing import Any

import httpx

from promethian.core.types import Skill, Tool, SkillSearchResult, ToolSearchResult


@dataclass
class SessionInfo:
    """Information about an active session."""
    session_id: str
    expires_at: datetime
    items_retrieved: list[str] = field(default_factory=list)


@dataclass
class ContributionResult:
    """Result of contributing a skill or tool."""
    status: str
    item_id: str | None = None
    quality_score: float | None = None
    message: str = ""
    merged_with: str | None = None


class PromethianAPIClient:
    """
    Client for the Promethian central knowledge API.

    Handles authentication, session management, and all API calls.
    """

    def __init__(
        self,
        api_url: str | None = None,
        api_key: str | None = None,
        timeout: float = 120.0,
    ):
        self.api_url = (
            api_url
            or os.environ.get("PROMETHIAN_API_URL")
            or "https://promethian-api.fly.dev"
        )
        self.api_key = api_key or os.environ.get("PROMETHIAN_API_KEY") or os.environ.get("PROMETHIAN_AUTH_TOKEN")

        if not self.api_key:
            raise ValueError(
                "Promethian auth token is required. Run setup again."
            )

        self.timeout = timeout
        self._token: str | None = None
        self._token_expires: datetime | None = None
        self._session: SessionInfo | None = None
        self._client = httpx.AsyncClient(timeout=timeout)

    async def close(self) -> None:
        """Close the HTTP client."""
        await self._client.aclose()

    async def __aenter__(self) -> "PromethianAPIClient":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()

    # ============================================
    # AUTHENTICATION
    # ============================================

    async def _ensure_token(self) -> str:
        """Ensure we have a valid access token."""
        # Device tokens (pmt_...) are used directly
        if self.api_key.startswith("pmt_"):
            return self.api_key

        # Legacy API keys need JWT exchange
        if self._token and self._token_expires:
            if datetime.utcnow() < self._token_expires:
                return self._token

        response = await self._client.post(
            f"{self.api_url}/auth/token",
            json={"api_key": self.api_key},
        )
        response.raise_for_status()
        data = response.json()

        self._token = data["access_token"]
        self._token_expires = datetime.fromisoformat(
            data["expires_at"].replace("Z", "+00:00")
        )

        return self._token

    async def _headers(self) -> dict:
        """Get headers for authenticated requests."""
        token = await self._ensure_token()
        return {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }

    # ============================================
    # SESSION MANAGEMENT
    # ============================================

    async def start_session(self, task_description: str, auto_recover: bool = True) -> SessionInfo:
        """
        Start a new task session.

        Sessions are required before searching for skills/tools.

        Args:
            task_description: Description of the task
            auto_recover: If True, automatically close stale sessions and retry
        """
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/session/start",
            json={"task_description": task_description},
            headers=headers,
        )

        if response.status_code == 400:
            error = response.json()
            detail = error.get("detail", "Session error")

            # Check if it's a stale session error and auto-recover
            if auto_recover and "Active session already exists" in detail:
                # Extract session ID from error message
                # Format: "Active session already exists. Complete it first. Session ID: xxx"
                import re
                match = re.search(r"Session ID: ([a-f0-9-]+)", detail)
                if match:
                    stale_session_id = match.group(1)
                    # Complete the stale session
                    try:
                        await self.complete_session(
                            session_id=stale_session_id,
                            overall_success=False,
                            notes="Auto-closed stale session",
                            user_initiated_close=False,
                        )
                    except Exception:
                        pass  # Ignore errors completing stale session

                    # Retry starting session
                    return await self.start_session(task_description, auto_recover=False)

            raise ValueError(detail)

        # Handle usage limit errors (402)
        if response.status_code == 402:
            error = response.json()
            detail = error.get("detail", "Payment required")
            # Try to get billing info for better error message
            try:
                billing = await self.get_billing_overview()
                usage = billing.get("usage", {})
                sessions_used = usage.get("sessions_used", "?")
                sessions_limit = usage.get("sessions_limit", "?")
                period_end = billing.get("current_period_end", "unknown")
                raise ValueError(
                    f"Usage limit reached: {sessions_used}/{sessions_limit} sessions used. "
                    f"Period resets: {period_end}. "
                    f"Upgrade at https://promethian.dev/billing or wait for reset."
                )
            except ValueError:
                raise
            except Exception:
                raise ValueError(f"Usage limit reached: {detail}")

        response.raise_for_status()
        data = response.json()

        self._session = SessionInfo(
            session_id=data["session_id"],
            expires_at=datetime.fromisoformat(
                data["expires_at"].replace("Z", "+00:00")
            ),
        )

        return self._session

    async def get_current_session(self) -> SessionInfo | None:
        """Get the current active session if any."""
        headers = await self._headers()
        response = await self._client.get(
            f"{self.api_url}/session/current",
            headers=headers,
        )
        response.raise_for_status()

        if response.status_code == 200 and response.json():
            data = response.json()
            self._session = SessionInfo(
                session_id=data["session_id"],
                expires_at=datetime.fromisoformat(
                    data["expires_at"].replace("Z", "+00:00")
                ),
                items_retrieved=[],  # Will be populated as we search
            )
            return self._session

        return None

    async def complete_session(
        self,
        overall_success: bool | None = None,
        items_feedback: list[dict] | None = None,
        new_skills: list[dict] | None = None,
        new_tools: list[dict] | None = None,
        self_heal_steps: int = 0,
        notes: str | None = None,
        # New parameters for enhanced feedback
        session_id: str | None = None,  # Allow explicit session_id
        success_score: float | None = None,  # 0-1 nuanced score
        satisfaction_signals: list[dict] | None = None,
        conversation_length: int = 1,
        had_refinements: bool = False,
        user_initiated_close: bool = True,
    ) -> dict:
        """
        Complete the current session with feedback.

        This MUST be called before starting a new session.

        Args:
            overall_success: Legacy boolean success (optional if success_score provided)
            items_feedback: Feedback for each retrieved item
            new_skills: New skills to contribute
            new_tools: New tools to contribute
            self_heal_steps: Number of self-healing attempts
            notes: Additional notes
            session_id: Explicit session ID (optional, uses current session if not provided)
            success_score: Nuanced success score 0-1 (preferred over overall_success)
            satisfaction_signals: List of satisfaction signals from conversation
            conversation_length: Number of user turns in conversation
            had_refinements: Whether user needed to correct/refine
            user_initiated_close: Whether user explicitly ended (vs timeout)
        """
        # Determine session ID
        sid = session_id or (self._session.session_id if self._session else None)
        if not sid:
            raise ValueError("No active session to complete")

        # Build request body
        body = {
            "session_id": sid,
            "items_feedback": items_feedback or [],
            "new_skills": new_skills or [],
            "new_tools": new_tools or [],
            "self_heal_steps": self_heal_steps,
            "notes": notes,
            "conversation_length": conversation_length,
            "had_refinements": had_refinements,
            "user_initiated_close": user_initiated_close,
        }

        # Prefer success_score over overall_success
        if success_score is not None:
            body["success_score"] = success_score
        elif overall_success is not None:
            body["overall_success"] = overall_success

        # Add satisfaction signals if provided
        if satisfaction_signals:
            body["satisfaction_signals"] = satisfaction_signals

        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/session/complete",
            json=body,
            headers=headers,
        )
        response.raise_for_status()

        self._session = None
        return response.json()

    @property
    def has_active_session(self) -> bool:
        """Check if there's an active session."""
        if not self._session:
            return False
        return datetime.utcnow() < self._session.expires_at

    # ============================================
    # SEARCH
    # ============================================

    async def search_skills(
        self,
        query: str,
        max_results: int = 5,
        min_similarity: float = 0.3,
        include_content: bool = True,
        tags: list[str] | None = None,
    ) -> tuple[list[SkillSearchResult], str]:
        """
        Search for relevant skills.

        Returns (results, query_id). The query_id is needed to access full content.
        """
        if not self.has_active_session:
            raise ValueError(
                "Active session required. This usually means the session was not properly started "
                "or has expired. Try running your task again."
            )

        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/skills/search",
            json={
                "query": query,
                "max_results": max_results,
                "min_similarity": min_similarity,
                "include_content": include_content,
                "tags": tags,
            },
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        results = []
        for item in data["results"]:
            results.append(SkillSearchResult(
                id=item["id"],
                name=item["name"],
                description=item["description"],
                tags=item["tags"],
                similarity_score=item["similarity_score"],
                success_rate=item["success_rate"],
                usage_count=item["usage_count"],
                content=item.get("content"),
            ))

            # Track retrieved items
            if self._session:
                self._session.items_retrieved.append(item["id"])

        return results, data["query_id"]

    async def search_tools(
        self,
        query: str,
        max_results: int = 5,
        min_similarity: float = 0.3,
        include_content: bool = True,
        tags: list[str] | None = None,
    ) -> tuple[list[ToolSearchResult], str]:
        """
        Search for relevant tools.

        Returns (results, query_id). The query_id is needed to access full content.
        """
        if not self.has_active_session:
            raise ValueError(
                "Active session required. This usually means the session was not properly started "
                "or has expired. Try running your task again."
            )

        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/tools/search",
            json={
                "query": query,
                "max_results": max_results,
                "min_similarity": min_similarity,
                "include_content": include_content,
                "tags": tags,
            },
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        results = []
        for item in data["results"]:
            results.append(ToolSearchResult(
                id=item["id"],
                name=item["name"],
                description=item["description"],
                function_name=item["function_name"],
                parameters=item["parameters"],
                dependencies=item.get("dependencies", []),
                tags=item["tags"],
                similarity_score=item["similarity_score"],
                success_rate=item["success_rate"],
                usage_count=item["usage_count"],
                code=item.get("code"),
            ))

            # Track retrieved items
            if self._session:
                self._session.items_retrieved.append(item["id"])

        return results, data["query_id"]

    # ============================================
    # ITEM ACCESS
    # ============================================

    async def get_skill(self, skill_id: str, query_id: str) -> Skill:
        """Get a skill by ID. Requires query_id from a recent search."""
        headers = await self._headers()
        headers["X-Query-ID"] = query_id

        response = await self._client.get(
            f"{self.api_url}/skills/{skill_id}",
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        return Skill.from_dict(data)

    async def get_tool(self, tool_id: str, query_id: str) -> Tool:
        """Get a tool by ID. Requires query_id from a recent search."""
        headers = await self._headers()
        headers["X-Query-ID"] = query_id

        response = await self._client.get(
            f"{self.api_url}/tools/{tool_id}",
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        return Tool.from_dict(data)

    # ============================================
    # CONTRIBUTION
    # ============================================

    async def contribute_skill(
        self,
        skill: Skill,
        source_task: str | None = None,
        execution_success: bool = True,
    ) -> ContributionResult:
        """Submit a skill as a contribution."""
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/skills/contribute",
            json={
                "name": skill.name,
                "description": skill.description,
                "content": skill.content,
                "tags": skill.tags,
                "source_task": source_task,
                "execution_success": execution_success,
            },
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        return ContributionResult(
            status=data["status"],
            item_id=data.get("item_id"),
            quality_score=data.get("quality_score"),
            message=data.get("message", ""),
            merged_with=data.get("merged_with"),
        )

    async def contribute_tool(
        self,
        tool: Tool,
        source_task: str | None = None,
        execution_success: bool = True,
    ) -> ContributionResult:
        """Submit a tool as a contribution."""
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/tools/contribute",
            json={
                "name": tool.name,
                "description": tool.description,
                "code": tool.code,
                "function_name": tool.function_name,
                "parameters": tool.parameters,
                "dependencies": tool.dependencies,
                "tags": tool.tags,
                "source_task": source_task,
                "execution_success": execution_success,
            },
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()

        return ContributionResult(
            status=data["status"],
            item_id=data.get("item_id"),
            quality_score=data.get("quality_score"),
            message=data.get("message", ""),
            merged_with=data.get("merged_with"),
        )

    # ============================================
    # WEB SEARCH
    # ============================================

    async def web_search(
        self,
        query: str,
        max_results: int = 5,
        search_type: str = "general",
    ) -> dict:
        """
        Search the web using server-side Tavily.

        Args:
            query: Search query
            max_results: Maximum results to return (1-10)
            search_type: 'general' or 'news'

        Returns:
            dict with success, query, results, and optional error
        """
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/web/search",
            json={
                "query": query,
                "max_results": max_results,
                "search_type": search_type,
            },
            headers=headers,
        )
        response.raise_for_status()
        return response.json()

    # ============================================
    # FEEDBACK & TELEMETRY
    # ============================================

    async def send_feedback(self, feedback_text: str) -> dict:
        """
        Send user feedback to the API server.

        Args:
            feedback_text: Arbitrary feedback text from the user

        Returns:
            dict with status and message
        """
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/feedback",
            json={"feedback": feedback_text},
            headers=headers,
        )
        response.raise_for_status()
        return response.json()

    async def send_session_log(
        self,
        task: str,
        success: bool,
        execution_time: float,
        steps_executed: int,
        self_heal_attempts: int = 0,
        error_message: str | None = None,
        tools_used: list[str] | None = None,
        skills_used: list[str] | None = None,
        log_type: str = "all",  # "all" or "failure"
    ) -> dict:
        """
        Send session log data to help improve Promethian.

        This is opt-in telemetry - users must enable it in settings.

        Args:
            task: The task that was executed
            success: Whether the task succeeded
            execution_time: Time taken in seconds
            steps_executed: Number of steps executed
            self_heal_attempts: Number of self-healing attempts
            error_message: Error message if failed
            tools_used: List of tool names used
            skills_used: List of skill names used
            log_type: Type of log ("all" or "failure")

        Returns:
            dict with status
        """
        headers = await self._headers()
        response = await self._client.post(
            f"{self.api_url}/telemetry/session-log",
            json={
                "task": task,
                "success": success,
                "execution_time": execution_time,
                "steps_executed": steps_executed,
                "self_heal_attempts": self_heal_attempts,
                "error_message": error_message,
                "tools_used": tools_used or [],
                "skills_used": skills_used or [],
                "log_type": log_type,
            },
            headers=headers,
        )
        response.raise_for_status()
        return response.json()

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

    async def health_check(self) -> dict:
        """Check API health status."""
        response = await self._client.get(f"{self.api_url}/health")
        response.raise_for_status()
        return response.json()


# ============================================
# STANDALONE AUTH FUNCTIONS (no token required)
# ============================================

async def request_verification_code(email: str, api_url: str = "https://promethian-api.fly.dev") -> bool:
    """
    Request a verification code to be sent to email.

    Returns True if code was sent successfully.
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{api_url}/auth/request-code",
            json={"email": email},
        )
        if response.status_code == 200:
            return True
        elif response.status_code == 429:
            raise ValueError("Too many requests. Please wait a minute and try again.")
        else:
            error = response.json().get("detail", "Failed to send code")
            raise ValueError(error)


async def verify_code_and_get_token(email: str, code: str, api_url: str = "https://promethian-api.fly.dev") -> str:
    """
    Verify the code and get an auth token.

    Returns the auth token (pmt_...) on success.
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{api_url}/auth/verify-code",
            json={"email": email, "code": code},
        )
        if response.status_code == 200:
            data = response.json()
            return data["token"]
        elif response.status_code == 400:
            raise ValueError("Invalid or expired code. Please request a new one.")
        else:
            error = response.json().get("detail", "Verification failed")
            raise ValueError(error)
