"""Ethiack API client."""

from __future__ import annotations

from typing import Any

import httpx

from . import __version__
from .config import Settings
from .exceptions import BeaconAPIError, BeaconAuthError, BeaconNotFoundError


class BeaconAPI:
    def __init__(self, settings: Settings, timeout: float = 30.0):
        self._settings = settings
        self._org_id: str = settings.org_id

        self._client = httpx.Client(
            base_url=settings.api_url,
            auth=(settings.api_key, settings.api_secret),
            timeout=timeout,
            headers={"User-Agent": f"ethiack-beacon-cli/{__version__}"},
        )

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> "BeaconAPI":
        return self

    def __exit__(self, *_: Any) -> None:
        self.close()

    # Organizations

    def get_organizations(self) -> list[dict[str, Any]]:
        """GET /v1/organizations/ - returns list of {id, name}."""
        data = self._get("/v1/organizations/")
        return data.get("organizations", [])

    @property
    def _beacons_base(self) -> str:
        return f"/v1/organizations/{self._org_id}/beacons"

    # Beacon CRUD

    def create(
        self,
        name: str,
        allowed_cidrs: list[str],
    ) -> dict[str, Any]:
        """
        POST /v1/organizations/{org_id}/beacons/

        Returns beacon record including:
          - id, name, status, allowed_cidrs
          - wireguard_config  (full wg config text, ready to write to disk)
          - beacon_token
          - rathole_server, rathole_control_port, rathole_public_port
        """
        payload: dict[str, Any] = {
            "name": name,
            "allowed_cidrs": allowed_cidrs,
        }
        return self._post(f"{self._beacons_base}/", json=payload)

    def list(
        self,
        page: int = 1,
        per_page: int = 20,
        status: str | None = None,
    ) -> dict[str, Any]:
        """GET /v1/organizations/{org_id}/beacons/ - returns {results: [...], count, next, previous}."""
        params: dict[str, Any] = {"page": page, "per_page": per_page}
        if status:
            params["status"] = status
        return self._get(f"{self._beacons_base}/", params=params)

    def get(self, beacon_id: str) -> dict[str, Any]:
        """GET /v1/organizations/{org_id}/beacons/{id}"""
        return self._get(f"{self._beacons_base}/{beacon_id}")

    def update(self, beacon_id: str, **kwargs: Any) -> dict[str, Any]:
        """PATCH /v1/organizations/{org_id}/beacons/{id}"""
        return self._patch(f"{self._beacons_base}/{beacon_id}", json=kwargs)

    def delete(self, beacon_id: str) -> None:
        """DELETE /v1/organizations/{org_id}/beacons/{id}"""
        self._delete(f"{self._beacons_base}/{beacon_id}")

    # Health

    def health(
        self,
        beacon_id: str,
        status: str,
        beacon_token: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        """POST /v1/beacons/health - authenticated via X-Beacon-Token, no org scope needed."""
        payload: dict[str, Any] = {"beacon_id": beacon_id, "status": status}
        if details:
            payload["details"] = details
        self._client.post(
            "/v1/beacons/health",
            json=payload,
            headers={"X-Beacon-Token": beacon_token},
        )

    # Test connection

    def test_connection(self, beacon_id: str, target: str) -> dict[str, Any]:
        """POST /v1/organizations/{org_id}/beacons/{id}/test - ask the API to reach target from the beacon."""
        return self._post(f"{self._beacons_base}/{beacon_id}/test", json={"target": target})

    # Events (pentests)

    def get_events(self, organization_id: str | None = None) -> list[dict[str, Any]]:
        """GET /v1/events/ - list events (pentests) for the organization."""
        params: dict[str, Any] = {"per_page": 50}
        if organization_id:
            params["organization_id"] = organization_id
        data = self._get("/v1/events/", params=params)
        return data.get("events", data.get("results", []))

    def create_pentest(
        self,
        organization_id: str,
        name: str,
        add_assets: bool = False,
        launch_recon: bool = False,
    ) -> dict[str, Any]:
        """POST /v1/events/ - create a new pentest."""
        payload: dict[str, Any] = {
            "organization_id": int(organization_id),
            "name": name,
            "add_assets": add_assets,
            "launch_recon": launch_recon,
        }
        return self._post("/v1/events/", json=payload)

    # Assets

    def create_asset(
        self,
        organization_id: str,
        asset_value: str,
        asset_type: str,
        beacon_id: int | str,
    ) -> dict[str, Any]:
        """POST /v1/assets/ - create a new asset."""
        payload: dict[str, Any] = {
            "organization_id": int(organization_id),
            "asset_value": asset_value,
            "asset_type": asset_type,
            "beacon_id": int(beacon_id),
        }
        return self._post("/v1/assets/", json=payload)

    def add_asset_to_event(
        self,
        pentest_slug: str,
        asset_id: int,
        in_scope: bool = True,
    ) -> dict[str, Any]:
        """POST /v1/events/{pentest_slug}/scope/assets/ - add an asset to an event's scope."""
        payload: dict[str, Any] = {"asset_id": asset_id, "in_scope": in_scope}
        return self._post(f"/v1/events/{pentest_slug}/scope/assets/", json=payload)

    def start_pentest(self, pentest_slug: str) -> dict[str, Any]:
        """POST /v1/events/{pentest_slug}/start - start the pentest."""
        return self._post(f"/v1/events/{pentest_slug}/start")

    # Low-level helpers

    def _get(self, path: str, **kwargs: Any) -> dict[str, Any]:
        return self._request("GET", path, **kwargs)

    def _post(self, path: str, **kwargs: Any) -> dict[str, Any]:
        return self._request("POST", path, **kwargs)

    def _patch(self, path: str, **kwargs: Any) -> dict[str, Any]:
        return self._request("PATCH", path, **kwargs)

    def _delete(self, path: str, **kwargs: Any) -> None:
        self._request("DELETE", path, expected_status=204, **kwargs)

    def _request(
        self,
        method: str,
        path: str,
        expected_status: int | None = None,
        **kwargs: Any,
    ) -> dict[str, Any]:
        try:
            resp = self._client.request(method, path, **kwargs)
        except httpx.ConnectError as exc:
            raise BeaconAPIError(f"Could not connect to {self._settings.api_url}: {exc}") from exc
        except httpx.TimeoutException as exc:
            raise BeaconAPIError(f"Request timed out: {exc}") from exc

        if resp.status_code in (401, 403):
            raise BeaconAuthError(
                "Authentication failed - check ETHIACK_API_KEY and ETHIACK_API_SECRET.",
                status_code=resp.status_code,
            )
        if resp.status_code == 404:
            raise BeaconNotFoundError(
                f"Beacon not found.", status_code=404
            )

        target_status = expected_status or 200
        # Accept 200 and 201 for POST/PATCH
        ok_statuses = {200, 201} if method in ("POST", "PATCH") else {target_status}
        if resp.status_code not in ok_statuses and resp.status_code != (expected_status or 200):
            _raise_for_status(resp)

        if resp.status_code == 204 or not resp.content:
            return {}

        try:
            return resp.json()
        except Exception:
            return {}


def _raise_for_status(resp: httpx.Response) -> None:
    try:
        detail = resp.json().get("detail") or resp.json().get("error") or resp.text
    except Exception:
        detail = resp.text or f"HTTP {resp.status_code}"
    raise BeaconAPIError(detail, status_code=resp.status_code)


def report_health(
    api_url: str,
    beacon_id: str,
    status: str,
    beacon_token: str,
    details: dict[str, Any] | None = None,
) -> None:
    """POST health status using beacon token only - no API key/secret required.

    This is a module-level function so it can be called without a full BeaconAPI
    instance (i.e. without API key/secret), which is the normal runtime case after
    the beacon has been created and runtime credentials are stored in local state.
    """
    payload: dict[str, Any] = {"beacon_id": beacon_id, "status": status}
    if details:
        payload["details"] = details
    try:
        with httpx.Client(
            base_url=api_url,
            timeout=10.0,
            headers={"User-Agent": f"ethiack-beacon-cli/{__version__}"},
        ) as client:
            client.post(
                "/v1/beacons/health",
                json=payload,
                headers={"X-Beacon-Token": beacon_token},
            )
    except httpx.ConnectError as exc:
        raise BeaconAPIError(f"Could not connect to {api_url}: {exc}") from exc
    except httpx.TimeoutException as exc:
        raise BeaconAPIError(f"Health report timed out: {exc}") from exc
