# --------------------------------------------------------------------------
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# --------------------------------------------------------------------------
import copy
import codecs
import email.message
from json import dumps
from typing import (
    Optional,
    Union,
    Mapping,
    Sequence,
    Tuple,
    IO,
    Any,
    Iterable,
    MutableMapping,
    AsyncIterable,
    cast,
    Dict,
)
import xml.etree.ElementTree as ET
from urllib.parse import urlparse
from azure.core.serialization import AzureJSONEncoder
from ..utils._pipeline_transport_rest_shared import (
    _format_parameters_helper,
    _pad_attr_name,
    _prepare_multipart_body_helper,
    _serialize_request,
    _format_data_helper,
)

################################### TYPES SECTION #########################

binary_type = str
PrimitiveData = Optional[Union[str, int, float, bool]]

ParamsType = Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]]

FileContent = Union[str, bytes, IO[str], IO[bytes]]
FileType = Tuple[Optional[str], FileContent]

FilesType = Union[Mapping[str, FileType], Sequence[Tuple[str, FileType]]]

ContentTypeBase = Union[str, bytes, Iterable[bytes]]
ContentType = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]

DataType = Optional[Union[bytes, Dict[str, Union[str, int]]]]

########################### HELPER SECTION #################################


def _verify_data_object(name, value):
    if not isinstance(name, str):
        raise TypeError("Invalid type for data name. Expected str, got {}: {}".format(type(name), name))
    if value is not None and not isinstance(value, (str, bytes, int, float)):
        raise TypeError("Invalid type for data value. Expected primitive type, got {}: {}".format(type(name), name))


def set_urlencoded_body(data, has_files):
    body = {}
    default_headers = {}
    for f, d in data.items():
        if not d:
            continue
        if isinstance(d, list):
            for item in d:
                _verify_data_object(f, item)
        else:
            _verify_data_object(f, d)
        body[f] = d
    if not has_files:
        # little hacky, but for files we don't send a content type with
        # boundary so requests / aiohttp etc deal with it
        default_headers["Content-Type"] = "application/x-www-form-urlencoded"
    return default_headers, body


def set_multipart_body(files):
    formatted_files = {f: _format_data_helper(d) for f, d in files.items() if d is not None}
    return {}, formatted_files


def set_xml_body(content):
    headers = {}
    bytes_content = ET.tostring(content, encoding="utf8")
    body = bytes_content.replace(b"encoding='utf8'", b"encoding='utf-8'")
    if body:
        headers["Content-Length"] = str(len(body))
    return headers, body


def set_content_body(
    content: Any,
) -> Tuple[MutableMapping[str, str], Optional[ContentTypeBase]]:
    headers: MutableMapping[str, str] = {}

    if isinstance(content, ET.Element):
        # XML body
        return set_xml_body(content)
    if isinstance(content, (str, bytes)):
        headers = {}
        body = content
        if isinstance(content, str):
            headers["Content-Type"] = "text/plain"
        if body:
            headers["Content-Length"] = str(len(body))
        return headers, body
    if any(hasattr(content, attr) for attr in ["read", "__iter__", "__aiter__"]):
        return headers, content
    raise TypeError(
        "Unexpected type for 'content': '{}'. ".format(type(content))
        + "We expect 'content' to either be str, bytes, a open file-like object or an iterable/asynciterable."
    )


def set_json_body(json: Any) -> Tuple[Dict[str, str], Any]:
    headers = {"Content-Type": "application/json"}
    if hasattr(json, "read"):
        content_headers, body = set_content_body(json)
        headers.update(content_headers)
    else:
        body = dumps(json, cls=AzureJSONEncoder)
        headers.update({"Content-Length": str(len(body))})
    return headers, body


def lookup_encoding(encoding: str) -> bool:
    # including check for whether encoding is known taken from httpx
    try:
        codecs.lookup(encoding)
        return True
    except LookupError:
        return False


def get_charset_encoding(response) -> Optional[str]:
    content_type = response.headers.get("Content-Type")

    if not content_type:
        return None
    # https://peps.python.org/pep-0594/#cgi
    m = email.message.Message()
    m["content-type"] = content_type
    encoding = cast(str, m.get_param("charset"))  # -> utf-8
    if encoding is None or not lookup_encoding(encoding):
        return None
    return encoding


def decode_to_text(encoding: Optional[str], content: bytes) -> str:
    if not content:
        return ""
    if encoding == "utf-8":
        encoding = "utf-8-sig"
    if encoding:
        return content.decode(encoding)
    return codecs.getincrementaldecoder("utf-8-sig")(errors="replace").decode(content)


class HttpRequestBackcompatMixin:
    def __getattr__(self, attr: str):
        backcompat_attrs = [
            "files",
            "data",
            "multipart_mixed_info",
            "query",
            "body",
            "format_parameters",
            "set_streamed_data_body",
            "set_text_body",
            "set_xml_body",
            "set_json_body",
            "set_formdata_body",
            "set_bytes_body",
            "set_multipart_mixed",
            "prepare_multipart_body",
            "serialize",
        ]
        attr = _pad_attr_name(attr, backcompat_attrs)
        return self.__getattribute__(attr)

    def __setattr__(self, attr, value):
        backcompat_attrs = [
            "multipart_mixed_info",
            "files",
            "data",
            "body",
        ]
        attr = _pad_attr_name(attr, backcompat_attrs)
        super(HttpRequestBackcompatMixin, self).__setattr__(attr, value)

    @property
    def _multipart_mixed_info(self):
        """DEPRECATED: Information used to make multipart mixed requests.
        This is deprecated and will be removed in a later release.

        :rtype: tuple
        :return: (requests, policies, boundary, kwargs)
        """
        try:
            return self._multipart_mixed_info_val
        except AttributeError:
            return None

    @_multipart_mixed_info.setter
    def _multipart_mixed_info(self, val):
        """DEPRECATED: Set information to make multipart mixed requests.
        This is deprecated and will be removed in a later release.

        :param tuple val: (requests, policies, boundary, kwargs)
        """
        self._multipart_mixed_info_val = val

    @property
    def _query(self):
        """DEPRECATED: Query parameters passed in by user
        This is deprecated and will be removed in a later release.

        :rtype: dict
        :return: Query parameters
        """
        query = urlparse(self.url).query
        if query:
            return {p[0]: p[-1] for p in [p.partition("=") for p in query.split("&")]}
        return {}

    @property
    def _body(self) -> DataType:
        """DEPRECATED: Body of the request. You should use the `content` property instead
        This is deprecated and will be removed in a later release.

        :rtype: bytes
        :return: Body of the request
        """
        return self._data

    @_body.setter
    def _body(self, val: DataType):
        """DEPRECATED: Set the body of the request
        This is deprecated and will be removed in a later release.

        :param bytes val: Body of the request
        """
        self._data = val

    def _format_parameters(self, params):
        """DEPRECATED: Format the query parameters
        This is deprecated and will be removed in a later release.
        You should pass the query parameters through the kwarg `params`
        instead.

        :param dict params: Query parameters
        """
        return _format_parameters_helper(self, params)

    def _set_streamed_data_body(self, data):
        """DEPRECATED: Set the streamed request body.
        This is deprecated and will be removed in a later release.
        You should pass your stream content through the `content` kwarg instead

        :param data: Streamed data
        :type data: bytes or iterable
        """
        if not isinstance(data, binary_type) and not any(
            hasattr(data, attr) for attr in ["read", "__iter__", "__aiter__"]
        ):
            raise TypeError("A streamable data source must be an open file-like object or iterable.")
        headers = self._set_body(content=data)
        self._files = None
        self.headers.update(headers)

    def _set_text_body(self, data):
        """DEPRECATED: Set the text body
        This is deprecated and will be removed in a later release.
        You should pass your text content through the `content` kwarg instead

        :param str data: Text data
        """
        headers = self._set_body(content=data)
        self.headers.update(headers)
        self._files = None

    def _set_xml_body(self, data):
        """DEPRECATED: Set the xml body.
        This is deprecated and will be removed in a later release.
        You should pass your xml content through the `content` kwarg instead

        :param data: XML data
        :type data: xml.etree.ElementTree.Element
        """
        headers = self._set_body(content=data)
        self.headers.update(headers)
        self._files = None

    def _set_json_body(self, data):
        """DEPRECATED: Set the json request body.
        This is deprecated and will be removed in a later release.
        You should pass your json content through the `json` kwarg instead

        :param data: JSON data
        :type data: dict
        """
        headers = self._set_body(json=data)
        self.headers.update(headers)
        self._files = None

    def _set_formdata_body(self, data=None):
        """DEPRECATED: Set the formrequest body.
        This is deprecated and will be removed in a later release.
        You should pass your stream content through the `files` kwarg instead

        :param data: Form data
        :type data: dict
        """
        if data is None:
            data = {}
        content_type = self.headers.pop("Content-Type", None) if self.headers else None

        if content_type and content_type.lower() == "application/x-www-form-urlencoded":
            headers = self._set_body(data=data)
            self._files = None
        else:  # Assume "multipart/form-data"
            headers = self._set_body(files=data)
            self._data = None
        self.headers.update(headers)

    def _set_bytes_body(self, data):
        """DEPRECATED: Set the bytes request body.
        This is deprecated and will be removed in a later release.
        You should pass your bytes content through the `content` kwarg instead

        :param bytes data: Bytes data
        """
        headers = self._set_body(content=data)
        # we don't want default Content-Type
        # in 2.7, byte strings are still strings, so they get set with text/plain content type

        headers.pop("Content-Type", None)
        self.headers.update(headers)
        self._files = None

    def _set_multipart_mixed(self, *requests, **kwargs):
        """DEPRECATED: Set the multipart mixed info.
        This is deprecated and will be removed in a later release.

        :param requests: Requests to be sent in the multipart request
        :type requests: list[HttpRequest]
        """
        self.multipart_mixed_info = (
            requests,
            kwargs.pop("policies", []),
            kwargs.pop("boundary", None),
            kwargs,
        )

    def _prepare_multipart_body(self, content_index=0):
        """DEPRECATED: Prepare your request body for multipart requests.
        This is deprecated and will be removed in a later release.

        :param int content_index: The index of the request to be sent in the multipart request
        :returns: The updated index after all parts in this request have been added.
        :rtype: int
        """
        return _prepare_multipart_body_helper(self, content_index)

    def _serialize(self):
        """DEPRECATED: Serialize this request using application/http spec.
        This is deprecated and will be removed in a later release.

        :rtype: bytes
        :return: The serialized request
        """
        return _serialize_request(self)

    def _add_backcompat_properties(self, request, memo):
        """While deepcopying, we also need to add the private backcompat attrs.

        :param HttpRequest request: The request to copy from
        :param dict memo: The memo dict used by deepcopy
        """
        request._multipart_mixed_info = copy.deepcopy(  # pylint: disable=protected-access
            self._multipart_mixed_info, memo
        )
