import os
import platform
from collections.abc import Iterable
from datetime import datetime
from typing import NoReturn

import httpx

from .constants import ENDPOINTS, AbuseCategory
from .exceptions import (
    InvalidParameterError,
    MissingAPIKeyError,
    PaymentRequiredError,
    TooManyRequestsError,
    UnconventionalAPIError,
    UnprocessableContentError,
)
from .responses import (
    BlacklistPlaintextResponse,
    BlacklistResponse,
    BulkReportResponse,
    CheckBlockResponse,
    CheckResponse,
    ClearAddressResponse,
    ReportResponse,
    ReportsPaginatedResponse,
)

DEFAULT_BASE_URL = "https://api.abuseipdb.com/api/v2"
DEFAULT_TIMEOUT = 10.0


class AbuseIPDB:
    """Synchronous client for AbuseIPDB API v2."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float | httpx.Timeout = DEFAULT_TIMEOUT,
        *,
        transport: httpx.BaseTransport | None = None,
    ) -> None:
        from . import __version__

        self._api_key = api_key if api_key is not None else os.getenv("ABUSEIPDB_API_KEY")
        resolved_base_url = (
            base_url if base_url is not None else (os.getenv("ABUSEIPDB_URL") or DEFAULT_BASE_URL)
        )
        headers = {
            "Accept": "application/json",
            "X-Request-Source": (
                f"Python_{platform.python_version()};AbuseIPDB-Python_{__version__};"
            ),
        }
        self._client = httpx.Client(
            base_url=resolved_base_url.rstrip("/") + "/",
            timeout=timeout,
            headers=headers,
            transport=transport,
        )

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

    def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
        self.close()

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

    def _request(
        self,
        endpoint: str,
        parameters: dict[str, object],
        *,
        accept: str = "application/json",
        file_contents: bytes | None = None,
    ) -> httpx.Response:
        if not self._api_key or not self._api_key.strip():
            raise MissingAPIKeyError(
                "ABUSEIPDB_API_KEY must be set or an AbuseIPDB API key passed explicitly."
            )

        method = ENDPOINTS[endpoint]
        kwargs: dict[str, object] = {"headers": {"Key": self._api_key, "Accept": accept}}
        if file_contents is not None:
            kwargs["files"] = {"csv": ("report.csv", file_contents, "text/csv")}
        elif method == "GET":
            kwargs["params"] = parameters
        else:
            kwargs["data"] = parameters

        response = self._client.request(method, endpoint, **kwargs)  # type: ignore[arg-type]
        if response.status_code == 200:
            return response
        self._raise_api_error(response)

    @staticmethod
    def _raise_api_error(response: httpx.Response) -> NoReturn:
        detail: str | None = None
        try:
            payload = response.json()
            candidate = payload["errors"][0]["detail"]
            if isinstance(candidate, str):
                detail = candidate
        except (ValueError, KeyError, IndexError, TypeError):
            pass

        message = f"AbuseIPDB: An error occurred. Status code: {response.status_code}."
        if detail is not None:
            message += f" Detail: {detail}"
        error_type = {
            402: PaymentRequiredError,
            422: UnprocessableContentError,
            429: TooManyRequestsError,
        }.get(response.status_code, UnconventionalAPIError)
        raise error_type(message, response=response, detail=detail)

    @staticmethod
    def _validate_max_age(max_age_in_days: int) -> None:
        if max_age_in_days < 1 or max_age_in_days > 365:
            raise InvalidParameterError("max_age_in_days must be between 1 and 365.")

    def check(
        self, ip_address: str, max_age_in_days: int = 30, verbose: bool = False
    ) -> CheckResponse:
        self._validate_max_age(max_age_in_days)
        parameters: dict[str, object] = {
            "ipAddress": ip_address,
            "maxAgeInDays": max_age_in_days,
        }
        if verbose:
            parameters["verbose"] = "true"
        return CheckResponse.from_response(self._request("check", parameters))

    def report(
        self,
        ip: str,
        categories: int | AbuseCategory | Iterable[int | AbuseCategory],
        comment: str | None = None,
        timestamp: datetime | None = None,
    ) -> ReportResponse:
        if isinstance(categories, int):
            values = [int(categories)]
        else:
            values = [int(category) for category in categories]
        if not values or any(category not in range(1, 24) for category in values):
            raise InvalidParameterError("Individual category must be a valid category.")
        if timestamp is not None and not isinstance(timestamp, datetime):
            raise InvalidParameterError("timestamp must be a datetime.")
        parameters: dict[str, object] = {"ip": ip, "categories": ",".join(map(str, values))}
        if comment is not None:
            parameters["comment"] = comment
        if timestamp is not None:
            parameters["timestamp"] = timestamp.isoformat()
        return ReportResponse.from_response(self._request("report", parameters))

    def reports(
        self,
        ip_address: str,
        max_age_in_days: int = 30,
        page: int = 1,
        per_page: int = 25,
    ) -> ReportsPaginatedResponse:
        self._validate_max_age(max_age_in_days)
        if page < 1:
            raise InvalidParameterError("page must be at least 1.")
        if per_page < 1 or per_page > 100:
            raise InvalidParameterError("per_page must be between 1 and 100.")
        return ReportsPaginatedResponse.from_response(
            self._request(
                "reports",
                {
                    "ipAddress": ip_address,
                    "maxAgeInDays": max_age_in_days,
                    "page": page,
                    "perPage": per_page,
                },
            )
        )

    def blacklist(
        self,
        confidence_minimum: int = 100,
        limit: int = 10000,
        plaintext: bool = False,
        only_countries: Iterable[str] | None = None,
        except_countries: Iterable[str] | None = None,
        ip_version: int | None = None,
    ) -> BlacklistResponse | BlacklistPlaintextResponse:
        if confidence_minimum < 25 or confidence_minimum > 100:
            raise InvalidParameterError("confidence_minimum must be between 25 and 100.")
        only = tuple(only_countries or ())
        excluded = tuple(except_countries or ())
        if any(len(code) != 2 for code in (*only, *excluded)):
            raise InvalidParameterError("Country codes must be 2 characters long.")
        if ip_version is not None and ip_version not in (4, 6):
            raise InvalidParameterError("ip_version must be 4 or 6.")
        parameters: dict[str, object] = {
            "confidenceMinimum": confidence_minimum,
            "limit": limit,
        }
        if ip_version is not None:
            parameters["ipVersion"] = ip_version
        if only:
            parameters["onlyCountries"] = ",".join(only)
        if excluded:
            parameters["exceptCountries"] = ",".join(excluded)
        if plaintext:
            parameters["plaintext"] = "true"
            return BlacklistPlaintextResponse.from_response(
                self._request("blacklist", parameters, accept="text/plain")
            )
        return BlacklistResponse.from_response(self._request("blacklist", parameters))

    def check_block(self, network: str, max_age_in_days: int = 30) -> CheckBlockResponse:
        self._validate_max_age(max_age_in_days)
        return CheckBlockResponse.from_response(
            self._request("check-block", {"network": network, "maxAgeInDays": max_age_in_days})
        )

    def bulk_report(self, csv_file_contents: str | bytes) -> BulkReportResponse:
        contents = (
            csv_file_contents.encode("utf-8")
            if isinstance(csv_file_contents, str)
            else csv_file_contents
        )
        return BulkReportResponse.from_response(
            self._request("bulk-report", {}, file_contents=contents)
        )

    def clear_address(self, ip_address: str) -> ClearAddressResponse:
        return ClearAddressResponse.from_response(
            self._request("clear-address", {"ipAddress": ip_address})
        )
