"""HTTP client for the Codeflash Platform API (non-AI endpoints)."""

from __future__ import annotations

import json
import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self

import attrs

from ._http import (
    _make_session,
    _resolve_api_key,
    _resolve_cfapi_base_url,
    _strip_trailing_slash,
)
from .exceptions import (
    AIServiceConnectionError,
    AIServiceError,
    InvalidAPIKeyError,
)

if TYPE_CHECKING:
    import requests as requests_types

    from ._model import FileDiffContent, PrComment

log = logging.getLogger(__name__)


def _json_default(obj: object) -> object:
    """JSON encoder fallback for attrs objects."""
    if attrs.has(type(obj)):
        return attrs.asdict(obj)  # type: ignore[arg-type]
    msg = f"Object of type {type(obj).__name__} is not JSON serializable"
    raise TypeError(msg)


def parse_repo_owner_and_name(remote_url: str) -> tuple[str, str]:
    """Parse repository owner and name from a git remote URL.

    Handles both HTTPS and SSH URL formats::

        https://github.com/owner/repo.git  -> ("owner", "repo")
        git@github.com:owner/repo.git      -> ("owner", "repo")
    """
    url = remote_url.removesuffix(".git").rstrip("/")
    parts = url.split("/")
    owner_part, repo_name = parts[-2], parts[-1]
    owner = owner_part.split(":")[1] if ":" in owner_part else owner_part
    return owner, repo_name


@attrs.define
class PlatformClient:
    """HTTP client for the Codeflash Platform API.

    Handles non-AI interactions with the Codeflash backend:
    blocklists, PR management, completion notifications, etc.
    """

    _base_url: str = attrs.field(
        alias="base_url",
        default=attrs.Factory(_resolve_cfapi_base_url),
        converter=_strip_trailing_slash,
    )
    _api_key: str = attrs.field(
        alias="api_key",
        default=attrs.Factory(_resolve_api_key),
        repr=False,
    )
    _timeout: float = attrs.field(
        alias="timeout",
        default=60.0,
    )
    _session: requests_types.Session = attrs.field(
        init=False,
        factory=_make_session,
    )

    def __attrs_post_init__(self) -> None:
        """Set the Authorization header on the session."""
        if self._api_key:
            self._session.headers["Authorization"] = f"Bearer {self._api_key}"

    def __enter__(self) -> Self:
        """Enter the context manager."""
        return self

    def __exit__(self, *exc_info: object) -> None:
        """Exit the context manager and close the session."""
        self.close()

    def close(self) -> None:
        """Close the underlying HTTP session."""
        self._session.close()

    # ------------------------------------------------------------------
    # User identity
    # ------------------------------------------------------------------

    def get_user_id(self) -> str | None:
        """Fetch the current user's ID from the Codeflash API."""
        import requests  # noqa: PLC0415

        try:
            resp = self._session.get(
                f"{self._base_url}/cfapi/cli-get-user",
                timeout=self._timeout,
            )
        except requests.RequestException:
            return None

        if not resp.ok:
            return None

        try:
            data = resp.json()
            return data.get("userId")  # type: ignore[no-any-return]
        except (ValueError, KeyError):
            # Older API returns plain-text user ID.
            return resp.text or None

    def validate_api_key(self) -> str:
        """Validate the API key and return the user ID.

        Raises :class:`InvalidAPIKeyError` if the key is rejected
        (HTTP 403) or missing.  Returns the user ID string on
        success.  Network errors are re-raised as
        :class:`AIServiceConnectionError`.
        """
        import requests  # noqa: PLC0415

        try:
            resp = self._session.get(
                f"{self._base_url}/cfapi/cli-get-user",
                timeout=self._timeout,
            )
        except requests.RequestException as exc:
            raise AIServiceConnectionError(str(exc)) from exc

        if resp.status_code == 403:  # noqa: PLR2004
            msg = (
                "Invalid Codeflash API key."
                " Generate a new one at"
                " https://app.codeflash.ai/app/apikeys"
            )
            raise InvalidAPIKeyError(msg)

        if not resp.ok:
            raise AIServiceError(resp.status_code, resp.text)

        try:
            data = resp.json()
            user_id: str | None = data.get("userId")
        except (ValueError, KeyError):
            user_id = resp.text or None

        if not user_id:
            msg = "Could not retrieve user ID from the API."
            raise AIServiceError(0, msg)

        return user_id

    # ------------------------------------------------------------------
    # Internal HTTP helper
    # ------------------------------------------------------------------

    def _request(
        self,
        endpoint: str,
        method: str = "GET",
        payload: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
        *,
        suppress_errors: bool = False,
    ) -> requests_types.Response:
        """Make an HTTP request to the platform API."""
        import requests  # noqa: PLC0415

        url = f"{self._base_url}/cfapi{endpoint}"
        try:
            if method.upper() == "POST":
                data = json.dumps(payload, default=_json_default)
                response = self._session.post(
                    url,
                    data=data,
                    headers={"Content-Type": "application/json"},
                    timeout=self._timeout,
                )
            else:
                response = self._session.get(
                    url,
                    params=params,
                    timeout=self._timeout,
                )
            response.raise_for_status()
        except requests.HTTPError:
            if not suppress_errors:
                error_message = ""
                try:
                    json_resp = response.json()
                    if "error" in json_resp:
                        error_message = json_resp["error"]
                    elif "message" in json_resp:
                        error_message = json_resp["message"]
                except (ValueError, TypeError):
                    error_message = response.text
                log.warning(
                    "Platform API error (url=%s, method=%s, status=%s): %s",
                    url,
                    method,
                    response.status_code,
                    error_message,
                )
            return response
        else:
            return response

    # ------------------------------------------------------------------
    # Blocklist & re-optimization checks
    # ------------------------------------------------------------------

    def get_blocklisted_functions(
        self,
        owner: str,
        repo: str,
        pr_number: int,
    ) -> dict[str, set[str]]:
        """Retrieve blocklisted functions for a pull request."""
        try:
            resp = self._request(
                "/verify-existing-optimizations",
                "POST",
                {
                    "pr_number": pr_number,
                    "repo_owner": owner,
                    "repo_name": repo,
                },
                suppress_errors=True,
            )
            if resp.status_code >= 500:  # noqa: PLR2004
                log.error(
                    "Server error getting blocklisted functions: %s",
                    resp.status_code,
                )
                import sentry_sdk  # noqa: PLC0415

                sentry_sdk.capture_message(
                    f"Server error in verify-existing-optimizations: {resp.status_code}",
                )
                return {}
            if not resp.ok:
                return {}

            content: dict[str, list[str]] = resp.json()
            if "error" in content:
                return {}

            return {
                Path(k).name: {v.replace("()", "") for v in values}
                for k, values in content.items()
            }
        except Exception as exc:
            log.exception("Error getting blocklisted functions")
            import sentry_sdk  # noqa: PLC0415

            sentry_sdk.capture_exception(exc)
            return {}

    def is_function_being_optimized_again(
        self,
        owner: str,
        repo: str,
        pr_number: int,
        code_contexts: list[dict[str, str]],
    ) -> Any:
        """Check if a function is being re-optimized."""
        resp = self._request(
            "/is-already-optimized",
            "POST",
            {
                "owner": owner,
                "repo": repo,
                "pr_number": pr_number,
                "code_contexts": code_contexts,
            },
        )
        resp.raise_for_status()
        return resp.json()

    def add_code_context_hash(
        self,
        owner: str,
        repo: str,
        pr_number: int,
        code_hash: str,
    ) -> None:
        """Add a code context hash to the platform cache."""
        self._request(
            "/add-code-hash",
            "POST",
            {
                "owner": owner,
                "repo": repo,
                "pr_number": pr_number,
                "code_hash": code_hash,
            },
        )

    # ------------------------------------------------------------------
    # Optimization lifecycle
    # ------------------------------------------------------------------

    def mark_optimization_success(
        self,
        trace_id: str,
        *,
        is_optimization_found: bool,
    ) -> None:
        """Mark an optimization trace as successful or not."""
        self._request(
            "/mark-as-success",
            "POST",
            {
                "trace_id": trace_id,
                "is_optimization_found": is_optimization_found,
            },
        )

    def send_completion_email(self, owner: str, repo: str) -> None:
        """Send a completion notification email."""
        self._request(
            "/send-completion-email",
            "POST",
            {"owner": owner, "repo": repo},
        )

    # ------------------------------------------------------------------
    # GitHub App
    # ------------------------------------------------------------------

    def is_github_app_installed(
        self,
        owner: str,
        repo: str,
    ) -> bool:
        """Check if the Codeflash GitHub App is installed on a repo."""
        resp = self._request(
            f"/is-github-app-installed?repo={repo}&owner={owner}",
            suppress_errors=True,
        )
        return resp.ok and resp.text == "true"

    # ------------------------------------------------------------------
    # PR management
    # ------------------------------------------------------------------

    @staticmethod
    def _serialize_file_changes(
        file_changes: dict[str, FileDiffContent],
    ) -> dict[str, dict[str, str]]:
        """Convert FileDiffContent objects to API-expected format."""
        return {
            k: {
                "oldContent": v.old_content,
                "newContent": v.new_content,
            }
            for k, v in file_changes.items()
        }

    def suggest_changes(  # noqa: PLR0913
        self,
        owner: str,
        repo: str,
        pr_number: int,
        file_changes: dict[str, FileDiffContent],
        pr_comment: PrComment,
        existing_tests: str,
        generated_tests: str,
        trace_id: str,
        coverage_message: str,
        replay_tests: str = "",
        concolic_tests: str = "",
        optimization_review: str = "",
        original_line_profiler: str | None = None,
        optimized_line_profiler: str | None = None,
    ) -> requests_types.Response:
        """Suggest changes to an existing pull request."""
        payload: dict[str, Any] = {
            "owner": owner,
            "repo": repo,
            "pullNumber": pr_number,
            "diffContents": self._serialize_file_changes(file_changes),
            "prCommentFields": pr_comment.to_json(),
            "existingTests": existing_tests,
            "generatedTests": generated_tests,
            "traceId": trace_id,
            "coverage_message": coverage_message,
            "replayTests": replay_tests,
            "concolicTests": concolic_tests,
            "optimizationReview": optimization_review,
            "originalLineProfiler": original_line_profiler,
            "optimizedLineProfiler": optimized_line_profiler,
        }
        return self._request("/suggest-pr-changes", "POST", payload)

    def create_pr(  # noqa: PLR0913
        self,
        owner: str,
        repo: str,
        base_branch: str,
        file_changes: dict[str, FileDiffContent],
        pr_comment: PrComment,
        existing_tests: str,
        generated_tests: str,
        trace_id: str,
        coverage_message: str,
        replay_tests: str = "",
        concolic_tests: str = "",
        optimization_review: str = "",
        original_line_profiler: str | None = None,
        optimized_line_profiler: str | None = None,
    ) -> requests_types.Response:
        """Create a new pull request with optimized code."""
        payload: dict[str, Any] = {
            "owner": owner,
            "repo": repo,
            "baseBranch": base_branch,
            "diffContents": self._serialize_file_changes(file_changes),
            "prCommentFields": pr_comment.to_json(),
            "existingTests": existing_tests,
            "generatedTests": generated_tests,
            "traceId": trace_id,
            "coverage_message": coverage_message,
            "replayTests": replay_tests,
            "concolicTests": concolic_tests,
            "optimizationReview": optimization_review,
            "originalLineProfiler": original_line_profiler,
            "optimizedLineProfiler": optimized_line_profiler,
        }
        return self._request("/create-pr", "POST", payload)

    def create_staging(  # noqa: PLR0913
        self,
        base_branch: str,
        file_changes: dict[str, FileDiffContent],
        pr_comment: PrComment,
        existing_tests: str,
        generated_tests: str,
        trace_id: str,
        coverage_message: str,
        replay_tests: str = "",
        concolic_tests: str = "",
        optimization_review: str = "",
        original_line_profiler: str | None = None,
        optimized_line_profiler: str | None = None,
    ) -> requests_types.Response:
        """Create a staging pull request."""
        payload: dict[str, Any] = {
            "baseBranch": base_branch,
            "diffContents": self._serialize_file_changes(file_changes),
            "prCommentFields": pr_comment.to_json(),
            "existingTests": existing_tests,
            "generatedTests": generated_tests,
            "traceId": trace_id,
            "coverage_message": coverage_message,
            "replayTests": replay_tests,
            "concolicTests": concolic_tests,
            "optimizationReview": optimization_review,
            "originalLineProfiler": original_line_profiler,
            "optimizedLineProfiler": optimized_line_profiler,
        }
        return self._request("/create-staging", "POST", payload)

    def setup_github_actions(
        self,
        owner: str,
        repo: str,
        base_branch: str,
        workflow_content: str,
    ) -> requests_types.Response:
        """Set up GitHub Actions by creating a PR with a workflow file."""
        return self._request(
            "/setup-github-actions",
            "POST",
            {
                "owner": owner,
                "repo": repo,
                "baseBranch": base_branch,
                "workflowContent": workflow_content,
            },
        )
