import asyncio
import io
import json
import socket
import urllib.parse
import urllib.request
from functools import partial
from logging import getLogger
from pathlib import Path

from defence360agent.contracts.config import ANTIVIRUS_MODE

logger = getLogger(__name__)


class ZendeskAPIError(Exception):
    def __init__(self, error, description, details):
        self.error = error
        self.description = description
        self.details = details
        super().__init__(description)


_API_URL_TMPL = "https://cloudlinux.zendesk.com/api/v2/{}"
_HC_URL_TMPL = "https://cloudlinux.zendesk.com/hc/requests/{}"

# Identifiers for custom fields in API
_PRODUCT_ID = 33267569
_DOCTOR_ID = 43297669
_CLN_ID = 43148369
_PRIVACY_POLICY_ID = 12355021509788


async def send_request(
    sender_email,
    subject,
    description,
    doctor_key=None,
    cln=None,
    attachments=None,
):
    """
    Send request to support of Imunify360 via Zendesk API
    """
    # Uploading attachments to Zendesk
    upload_token = await _upload_attachments(attachments)

    # Creating comment object: setting description and attaching
    # uploads token
    comment = dict(body=description)
    if upload_token is not None:
        comment["uploads"] = [upload_token]

    # Author of request
    requester = dict(name=sender_email, email=sender_email)

    # Custom fields for support convenience
    custom_fields = [
        {
            "id": _PRODUCT_ID,
            "value": "pr_imunify_av" if ANTIVIRUS_MODE else "pr_im360",
        },
        {"id": _PRIVACY_POLICY_ID, "value": True},
    ]

    if doctor_key:
        custom_fields.append({"id": _DOCTOR_ID, "value": doctor_key})

    if cln:
        custom_fields.append({"id": _CLN_ID, "value": cln})

    # Ready request
    request = dict(
        requester=requester,
        subject=subject,
        comment=comment,
        custom_fields=custom_fields,
    )

    return await _post_support_request(request)


def decode_as_json(response):
    return json.load(
        io.TextIOWrapper(
            response,
            encoding=response.headers.get_content_charset("utf-8"),
        )
    )


def parse_params(params, url):
    p = urllib.parse.urlparse(url)
    query = p.query
    if query:
        query += "&"
    query += urllib.parse.urlencode(params)
    url = urllib.parse.urlunparse(
        (p.scheme, p.netloc, p.path, p.params, query, p.fragment)
    )
    return url


def _post_data(url, data: bytes, headers, *, params=None, timeout=None):
    """HTTP POST *data* to *url* with given *headers*.

    Add query *params* to the *url* if given.

    Return (http_status, decoded_json_response) tuple.
    """
    if params:  # add params to the url
        url = parse_params(params, url)

    try:
        with urllib.request.urlopen(
            urllib.request.Request(url, data=data, headers=headers),
            timeout=timeout,
        ) as response:
            return response.code, decode_as_json(response)  # http status
    except socket.timeout:
        raise TimeoutError
    except OSError as e:
        if not hasattr(e, "code"):
            raise
        # HTTPError
        return e.code, (decode_as_json(e) if e.fp is not None else {})


async def _post_support_request(request):
    """Return url of the support request or None if request is suspended,
    because of we not able to obtain the id of the ticket if it suspended.
    """
    url = _API_URL_TMPL.format("requests.json")
    headers = {"Content-Type": "application/json"}
    data = json.dumps(dict(request=request), sort_keys=True).encode("ascii")
    loop = asyncio.get_event_loop()
    status, result = await loop.run_in_executor(
        None, _post_data, url, data, headers
    )
    if status == 201:
        request_data = result.get("request")
        if request_data:
            return _HC_URL_TMPL.format(request_data["id"])
        elif "suspended_ticket" in result.keys():
            return None
        else:
            raise ZendeskAPIError(
                "Response error", "UNKNOWN ERROR", "{!r}".format(result)
            )
    else:
        raise ZendeskAPIError(
            result.get("error", "UNKNOWN ERROR"),
            result.get("description"),
            result.get("details", {}),
        )


async def _upload_attachments(attachments):
    # Uploading attachments to Zendesk
    upload_token = None
    if attachments is None:
        return upload_token

    loop = asyncio.get_event_loop()
    for attachment in attachments:
        path = Path(attachment)
        params = {"filename": path.name}
        if upload_token is not None:
            params["token"] = upload_token
        status, result = await loop.run_in_executor(
            None,
            partial(
                _post_data,
                _API_URL_TMPL.format("uploads.json"),
                data=path.read_bytes(),
                headers={"Content-Type": "application/binary"},
                params=params,
            ),
        )
        if status != 201:
            logger.warning(
                "Failed to upload file %s to Zendesk: %s",
                attachment,
                result["error"],
            )
            continue

        if upload_token is None:
            upload_token = result["upload"]["token"]

    return upload_token
