"""
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]) -> 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``.
        """
        resp = self._http.post("/v1/users", json={"topics": topics})
        resp.raise_for_status()
        return resp.json()

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

        Returns None when the server responds with 404, meaning there are no
        more challenges available right now (the user has exhausted today's
        pool or there are simply no challenges for the requested topics).
        """
        resp = self._http.get(
            "/v1/challenges/next",
            params={"topics": ",".join(topics)},
            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) -> 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").

        Returns None when the server responds with 404, meaning there are no
        more problems available for the requested topics and language.
        """
        resp = self._http.get(
            "/v1/coding-problems/next",
            params={"topics": ",".join(topics), "language": language},
            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 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 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()
