"""
SkilarkClient: thin httpx wrapper for the Skilark API.

Every method maps 1-to-1 to a REST endpoint.  The client is synchronous
because the CLI is a short-lived interactive process — async overhead is
not warranted here.

The `transport` constructor argument exists solely for testing: pass an
httpx.MockTransport to intercept calls without hitting the network.
"""

import httpx


class SkilarkClient:
    """HTTP client for the Skilark API.

    Args:
        base_url:  Root URL of the API (e.g. "https://api.skilark.com").
        user_id:   UUID for the authenticated user.  When set, every request
                   carries an ``X-Skilark-User`` header so the server can
                   associate actions with the right account.
        transport: Optional httpx transport override, used in tests.
    """

    def __init__(
        self,
        base_url: str,
        user_id: str | None = None,
        transport: httpx.BaseTransport | None = None,
    ):
        kwargs: dict = {
            "base_url": base_url,
            "timeout": httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0),
        }
        if transport is not None:
            kwargs["transport"] = transport
        self._http = httpx.Client(**kwargs)
        self._user_id = user_id

    @property
    def user_id(self) -> str | None:
        """The authenticated user's UUID, or None if not yet set."""
        return self._user_id

    def _headers(self) -> dict:
        """Return request headers that identify the current user, if known."""
        h: dict = {}
        if self._user_id:
            h["X-Skilark-User"] = self._user_id
        return h

    def create_user(
        self,
        topics: list[str],
        current_role: str | None = None,
        primary_language: str | None = None,
        career_direction: str | None = None,
        target_role: str | None = None,
    ) -> dict:
        """Register a new CLI user and return the created user object.

        Called once during ``skilark setup``.  The returned dict contains
        at minimum ``id`` (UUID) and ``topics``.
        """
        body: dict = {"topics": topics}
        if current_role:
            body["current_role"] = current_role
        if primary_language:
            body["primary_language"] = primary_language
        if career_direction:
            body["career_direction"] = career_direction
        if target_role:
            body["target_role"] = target_role
        resp = self._http.post("/v1/users", json=body)
        resp.raise_for_status()
        return resp.json()

    def get_profile(self) -> dict | None:
        """Fetch the current user's profile. Returns None on 404."""
        resp = self._http.get("/v1/users/me/profile", headers=self._headers())
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return resp.json()

    def update_profile(
        self,
        current_role: str | None = None,
        primary_language: str | None = None,
        career_direction: str | None = None,
        target_role: str | None = None,
    ) -> dict:
        """Upsert profile fields for the current user."""
        body: dict = {}
        if current_role is not None:
            body["current_role"] = current_role
        if primary_language is not None:
            body["primary_language"] = primary_language
        if career_direction is not None:
            body["career_direction"] = career_direction
        if target_role is not None:
            body["target_role"] = target_role
        resp = self._http.put("/v1/users/me/profile", json=body, headers=self._headers())
        resp.raise_for_status()
        return resp.json()

    def update_topics(self, topics: list[str]) -> None:
        """Update the user's topic list."""
        resp = self._http.patch(
            "/v1/users/me/topics",
            json={"topics": topics},
            headers=self._headers(),
        )
        resp.raise_for_status()

    def get_next_challenge(
        self, topics: list[str], *, topic: str | None = None
    ) -> dict | None:
        """Fetch the next unseen challenge.

        If *topic* is given (single topic from browser), uses ``?topic=X``.
        Otherwise uses ``?topics=X,Y`` (legacy multi-topic).

        Returns None when the server responds with 404.
        """
        params = {"topic": topic} if topic else {"topics": ",".join(topics)}
        resp = self._http.get(
            "/v1/challenges/next",
            params=params,
            headers=self._headers(),
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return resp.json()

    def get_next_coding_problem(
        self,
        topics: list[str],
        language: str,
        *,
        category: str | None = None,
    ) -> dict | None:
        """Fetch the next unsolved coding problem, or None if exhausted.

        Args:
            topics:   List of topic slugs to filter by (e.g. ["dsa", "python"]).
            language: The preferred implementation language (e.g. "python", "java").
            category: Optional category filter (e.g. "arrays").

        Returns None when the server responds with 404.
        """
        params: dict[str, str] = {
            "topics": ",".join(topics),
            "language": language,
        }
        if category is not None:
            params["category"] = category
        resp = self._http.get(
            "/v1/coding-problems/next",
            params=params,
            headers=self._headers(),
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return resp.json()

    def submit_coding_problem(
        self,
        problem_id: str,
        *,
        language: str,
        all_passed: bool,
        passed_count: int,
        total_count: int,
    ) -> None:
        """Record a coding problem submission.

        Args:
            problem_id:   The coding problem UUID returned by get_next_coding_problem.
            language:     The language the user wrote the solution in.
            all_passed:   True if every test case passed.
            passed_count: Number of test cases that passed.
            total_count:  Total number of test cases evaluated.
        """
        resp = self._http.post(
            f"/v1/coding-problems/{problem_id}/submit",
            json={
                "language": language,
                "all_passed": all_passed,
                "passed_count": passed_count,
                "total_count": total_count,
            },
            headers=self._headers(),
        )
        resp.raise_for_status()

    def complete_challenge(
        self,
        challenge_id: str,
        answer: str,
        correct: bool,
        self_corrected: bool,
    ) -> None:
        """Record the user's attempt for a challenge.

        Args:
            challenge_id:   The challenge UUID returned by get_next_challenge.
            answer:         The raw text the user typed.
            correct:        Whether the system judged the answer correct.
            self_corrected: Whether the user self-assessed after seeing the
                            expected answer (used for partial-credit scoring).
        """
        resp = self._http.post(
            f"/v1/challenges/{challenge_id}/complete",
            json={
                "answer": answer,
                "correct": correct,
                "self_corrected": self_corrected,
            },
            headers=self._headers(),
        )
        resp.raise_for_status()

    def get_stats(self) -> dict:
        """Return practice statistics for the current user.

        The returned dict contains at minimum ``streak`` and
        ``total_completed``.
        """
        resp = self._http.get("/v1/users/me/stats", headers=self._headers())
        resp.raise_for_status()
        return resp.json()

    def get_challenge_topics(self) -> list[dict]:
        """GET /v1/challenges/topics — quiz topics with per-user progress."""
        resp = self._http.get("/v1/challenges/topics", headers=self._headers())
        resp.raise_for_status()
        return resp.json()

    def get_coding_categories(self) -> list[dict]:
        """GET /v1/coding-problems/categories — coding categories with progress."""
        resp = self._http.get(
            "/v1/coding-problems/categories", headers=self._headers()
        )
        resp.raise_for_status()
        return resp.json()

    def get_coding_problems_by_category(self, category: str) -> list[dict]:
        """GET /v1/coding-problems?category={category} — problem list for a category."""
        resp = self._http.get(
            "/v1/coding-problems",
            params={"category": category},
            headers=self._headers(),
        )
        resp.raise_for_status()
        return resp.json()

    def get_coding_problem_by_id(self, problem_id: str) -> dict | None:
        """GET /v1/coding-problems/{problem_id} — fetch a specific coding problem."""
        resp = self._http.get(
            f"/v1/coding-problems/{problem_id}", headers=self._headers()
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return resp.json()

    def create_link_code(self) -> dict:
        """Generate a 6-char link code for cross-channel linking.

        The returned dict contains at minimum ``code`` (the 6-character
        alphanumeric code) and ``expires_at`` (ISO-8601 timestamp).  The
        caller should display the code to the user so they can paste it into
        another Skilark channel (e.g. the Telegram bot).
        """
        resp = self._http.post("/v1/users/link-code", headers=self._headers())
        resp.raise_for_status()
        return resp.json()

    def get_signals(self, limit: int = 5) -> dict:
        """Fetch this week's market signals.

        Args:
            limit: Maximum number of signals to return. Pass 0 for all signals.

        Returns a dict with ``week_of`` (ISO date string) and ``signals``
        (list of signal dicts).
        """
        resp = self._http.get("/v1/signals", params={"limit": limit})
        resp.raise_for_status()
        return resp.json()

    def find_my_fit(self, query: str) -> dict:
        """Semantic job search. POST /find-my-fit with JSON body.

        Args:
            query: Free-text description of the role or skills to match against
                   (e.g. "senior backend engineer distributed systems").

        Returns a dict with ``companies``, ``common_titles``, ``top_skills``,
        ``salary_range``, ``seniority``, ``remote_policy``, and
        ``role_categories``.
        """
        resp = self._http.post(
            "/find-my-fit",
            json={"query": query},
            timeout=httpx.Timeout(connect=5.0, read=45.0, write=10.0, pool=5.0),
        )
        resp.raise_for_status()
        return resp.json()

    def find_my_fit_resume(self, pdf_path: str) -> dict:
        """Resume upload and analysis. POST /find-my-fit/resume multipart.

        Reads the PDF at ``pdf_path``, uploads it as multipart/form-data, and
        returns the full FindMyFitResponse extended with ``profile_summary``
        (extracted skills, seniority, role category, years experience, summary)
        and ``skill_gap`` (missing skills, underrepresented areas,
        recommendation).

        Args:
            pdf_path: Absolute or relative path to a PDF resume file.

        The server enforces a 5MB size limit (413) and a daily budget cap (503).
        """
        with open(pdf_path, "rb") as f:
            resp = self._http.post(
                "/find-my-fit/resume",
                files={"resume": ("resume.pdf", f, "application/pdf")},
                timeout=httpx.Timeout(connect=5.0, read=45.0, write=10.0, pool=5.0),
            )
        resp.raise_for_status()
        return resp.json()

    def send_feedback(self, message: str, source: str = "cli") -> None:
        """Submit user feedback to the API.

        Args:
            message: The feedback message text (1-2000 chars).
            source:  Channel identifier for tracking (default "cli").
        """
        resp = self._http.post(
            "/feedback",
            json={"message": message, "source": source},
        )
        resp.raise_for_status()

    def redeem_link_code(self, code: str, channel_type: str, channel_id: str) -> dict:
        """Redeem a link code to attach a new channel to the current user.

        Args:
            code:         The 6-character code generated by another channel.
            channel_type: Identifier for the channel type (e.g. ``"cli"``).
            channel_id:   The channel-specific user identifier (e.g. a UUID
                          for CLI users or a Telegram user ID for bots).

        Returns the updated user object returned by the server.
        """
        resp = self._http.post(
            "/v1/users/link",
            json={"code": code, "channel_type": channel_type, "channel_id": channel_id},
        )
        resp.raise_for_status()
        return resp.json()
