# -*- coding: utf-8 -*-
#
# Copyright 2020 - Swiss Data Science Center (SDSC)
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility for working with HTTP session.

This module provides some wrapper functions around ``requests`` library. It sets a timeout and converts exception types
whenever needed. Use this module instead of ``requests``.
"""

import os
import tempfile
import urllib
from contextlib import contextmanager
from pathlib import Path
from typing import Union

import patoolib
import requests
from requests.adapters import HTTPAdapter, TimeoutSauce
from urllib3.util.retry import Retry

from renku.core import errors

_RENKU_REQUESTS_TIMEOUT_SECONDS = float(os.getenv("RENKU_REQUESTS_TIMEOUT_SECONDS", 1200))


class _CustomTimeout(TimeoutSauce):
    """CustomTimeout for all HTTP requests."""

    def __init__(self, *args, **kwargs):
        """Construct CustomTimeout."""
        if kwargs["connect"] is None:
            kwargs["connect"] = _RENKU_REQUESTS_TIMEOUT_SECONDS

        if kwargs["read"] is None:
            kwargs["read"] = _RENKU_REQUESTS_TIMEOUT_SECONDS

        super().__init__(*args, **kwargs)


requests.adapters.TimeoutSauce = _CustomTimeout


def delete(url, headers=None):
    """Send a DELETE request."""
    return _request("delete", url=url, headers=headers)


def get(url, headers=None):
    """Send a GET request."""
    return _request("get", url=url, headers=headers)


def head(url, *, allow_redirects=False, headers=None):
    """Send a HEAD request."""
    return _request("head", url=url, allow_redirects=allow_redirects, headers=headers)


def post(url, *, data=None, files=None, headers=None, json=None, params=None):
    """Send a POST request."""
    return _request("post", url=url, data=data, files=files, headers=headers, json=json, params=params)


def put(url, *, data=None, files=None, headers=None, params=None):
    """Send a PUT request."""
    return _request("put", url=url, data=data, files=files, headers=headers, params=params)


def _request(verb: str, url: str, *, allow_redirects=True, data=None, files=None, headers=None, json=None, params=None):
    try:
        with _retry() as session:
            return getattr(session, verb)(
                url=url,
                allow_redirects=allow_redirects,
                data=data,
                files=files,
                headers=headers,
                json=json,
                params=params,
            )
    except (ConnectionError, requests.RequestException, urllib.error.HTTPError) as e:
        raise errors.RequestError(f"{verb.upper()} request failed for {url}") from e


def get_redirect_url(url) -> str:
    """Return redirect URL if any; otherwise, return the original URL."""
    try:
        return head(url, allow_redirects=True).url
    except errors.RequestError:
        # NOTE: HEAD request failed, try with original url
        return url


def check_response(response):
    """Check for expected response status code."""
    if response.status_code not in [200, 201, 202]:
        if response.status_code == 401:
            raise errors.AuthenticationError("Access unauthorized - update access token.")

        if response.status_code == 400:
            err_response = response.json()
            messages = [
                '"{0}" failed with "{1}"'.format(err["field"], err["message"]) for err in err_response["errors"]
            ]

            raise errors.ExportError(
                "\n" + "\n".join(messages) + "\nSee `renku dataset edit -h` for details on how to edit" " metadata"
            )

        raise errors.ExportError(response.content)


def download_file(base_directory: Union[Path, str], url: str, filename, extract, chunk_size=16384):
    """Download a URL to a given location."""
    from renku.core.utils import communication

    def extract_dataset(filepath):
        """Extract downloaded file."""
        try:
            tmp = tempfile.mkdtemp()
            patoolib.extract_archive(str(filepath), outdir=tmp, verbosity=-1)
        except patoolib.util.PatoolError:
            return filepath.parent, [filepath]
        else:
            filepath.unlink()
            return Path(tmp), [p for p in Path(tmp).rglob("*")]

    tmp_root = Path(base_directory)
    tmp_root.mkdir(parents=True, exist_ok=True)
    tmp = tempfile.mkdtemp(dir=tmp_root)

    try:
        with requests.get(url, stream=True, allow_redirects=True) as response:
            response.raise_for_status()

            if not filename:
                filename = get_filename_from_headers(response)

            if not filename:
                u = urllib.parse.urlparse(url)
                filename = Path(u.path).name
                if not filename:
                    raise errors.ParameterError(f"URL Cannot find a file to download from {url}")

            download_to = Path(tmp) / filename
            with open(str(download_to), "wb") as file_:
                total_size = int(response.headers.get("content-length", 0))

                communication.start_progress(name=filename, total=total_size)
                try:
                    for chunk in response.iter_content(chunk_size=chunk_size):
                        if chunk:  # ignore keep-alive chunks
                            file_.write(chunk)
                            communication.update_progress(name=filename, amount=len(chunk))
                finally:
                    communication.finalize_progress(name=filename)

    except (requests.exceptions.HTTPError, urllib.error.HTTPError) as e:  # pragma nocover
        raise errors.RequestError(f"Cannot download from {url}") from e

    if extract:
        return extract_dataset(download_to)

    return download_to.parent, [download_to]


def get_filename_from_headers(response):
    """Extract filename from content-disposition headers if available."""
    content_disposition = response.headers.get("content-disposition", None)

    if not content_disposition:
        return None

    entries = content_disposition.split(";")
    name_entry = next((e.strip() for e in entries if e.strip().lower().startswith("filename*=")), None)

    if name_entry:
        name = name_entry.split("=", 1)[1].strip()
        encoding, _, name = name.split("'")
        return urllib.parse.unquote(name, encoding, errors="strict")

    name_entry = next((e.strip() for e in entries if e.strip().lower().startswith("filename=")), None)

    if not name_entry:
        return None

    filename = name_entry.split("=", 1)[1].strip()

    if filename.startswith('"'):
        filename = filename[1:-1]
    return filename


@contextmanager
def _retry(total_requests=5, backoff_factor=0.2, statuses=(500, 502, 503, 504, 429)):
    """Default HTTP session for requests."""
    _session = requests.Session()

    retries = Retry(total=total_requests, backoff_factor=backoff_factor, status_forcelist=list(statuses))

    _session.mount("http://", HTTPAdapter(max_retries=retries))
    _session.mount("https://", HTTPAdapter(max_retries=retries))

    try:
        yield _session
    except requests.RequestException as e:
        raise errors.RequestError("renku operation failed due to network connection failure") from e
