import logging
from requests import Response
from requests.models import RequestsJSONDecodeError
from typing import Any, NamedTuple, Optional, Self, Tuple


class ResponseParseException(Exception):
    """Generic exception parsing response."""


class SyncResult(NamedTuple):

    success: bool
    status_code: Optional[int] = None
    data: Optional[Any] = None

    @staticmethod
    def from_requests_response(response: Optional[Response]) -> Self:
        """Create SyncResult from requests response.

        :param Optional[Response] response: The response
        :return: Object containing sync results
        :rtype: Self
        """
        try:
            success, response_json = parse_response(response)
        except ResponseParseException:
            success = False
            return SyncResult(success)

        return SyncResult(success, response.status_code, response_json)


class ContentPushSyncResult(NamedTuple):
    success: bool
    status_code: int
    data: Any
    sent_content: Optional[str]

    @staticmethod
    def from_requests_response(response: Optional[Response], sent_content: Optional[str]) -> Self:
        """Create ContentPushSyncResult from requests response and sent content.

        The sent content is included here so it can be used in retaining a hash for failed pushes
        (which assist with false sync conflicts on high packet loss connections).

        :param Optional[Response] response: The response
        :param Optional[str] sent_content: The content as sent in the request
        :return: Object containing sync results and sent content
        :rtype: Self
        """
        try:
            success, response_json = parse_response(response)
        except ResponseParseException:
            status_code = None if not response else response.status_code
            return ContentPushSyncResult(False, status_code, None, sent_content)

        return ContentPushSyncResult(success, response.status_code, response_json, sent_content)


class FailedPush(NamedTuple):
    hash: str
    length: int
    timestamp: int


@staticmethod
def parse_response(response: Optional[Response]) -> Tuple[bool, dict]:
    """Parse requests response.

    :param Optional[Response] response: The response
    :return: Whether the request was a success and the parsed response JSON
    :rtype: Tuple[bool, dict]
    :raises ResponseParseException: If the response could not be parsed
    """
    if response is None:
        logging.debug("parse_response received null response")
        raise ResponseParseException("No response")

    try:
        response_json = response.json()
    except RequestsJSONDecodeError as e:
        logging.warning("Failed to decode server JSON response: %s", e)
        raise ResponseParseException("Failed to decode JSON")

    success = response.status_code == 200

    return (success, response_json)
