# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

import logging
from requests import Response
from requests.exceptions import JSONDecodeError
from typing import Any, NamedTuple, Optional


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


class SyncResult(NamedTuple):

    success: bool
    status_code: Optional[int] = None
    data: Optional[list[dict]] = None

    @staticmethod
    def from_requests_response(response: Optional[Response]) -> SyncResult:
        """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)
        if not isinstance(response_json, list):
            logging.warning(f"Data from server unexpected type {type(response_json)}")
            success = False
            return SyncResult(success)

        return SyncResult(success, response.status_code, response_json)


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

    @staticmethod
    def from_requests_response(
        response: Optional[Response], sent_content: Optional[str]
    ) -> ContentPushSyncResult:
        """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)

        assert response is not None  # mypy

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


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


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 JSONDecodeError as e:
        logging.warning(f"Failed to decode server JSON response: {e}")
        raise ResponseParseException("Failed to decode JSON")

    success = response.status_code == 200

    return (success, response_json)
