#!/usr/bin/env python3
# -*-coding: utf-8 -*-


import json
import logging
from enum import IntEnum
from http import HTTPStatus
from pathlib import Path
from time import sleep
from urllib.error import HTTPError
from urllib.parse import parse_qs
from urllib.parse import urlencode
from urllib.parse import urlparse
from urllib.parse import urlunparse
from urllib.request import Request
from urllib.request import urlopen


from .tool import parse_sentinel2_name


QUERY_URL_BASE = "https://finder.creodias.eu/resto/api/collections/{:s}/search.json?sortParams=startDate&sortOrder=ascending"
QUERY_MAX_RECORDS = 100
CATALOG_EPSG = 4326

HTTP_MAX_TRIES = 10
HTTP_SLEEP_TIME = 5


class CatalogStatus(IntEnum):
    """The status of the catalog item.

    An excerpt from `EOData Finder API <https://creodias.eu/eo-data-finder-api-manual>`_.

        status:

        * *all* shows all products regardless of processing,
        * *31* means that product is orderable and waiting for download to our cache,
        * *32* means that product is ordered and processing is in progress,
        * *34* means that product is downloaded in cache,
        * *37* means that product is processed by our platform,
        * *0* means that already processed product is waiting in our platform

    The description of statuses has been considered ambiguous.
    Therefore, the statuses were more carefully examined with the help of creodias support,
    see Ticket 315632: Order orderable scenes.
    More reasonable description has been deduced:

      * 31, the product exists in a foreign archive and its acquisition may be ordered by a user;

      * 32, the product, formerly 31, has been ordered and the acquisition is in progress;

      * 34, the acquisition of the product, formerly 32, has been finished
            and the product is available in the eodata archive;

      * 37, formerly nonexistent product has been generated by creodias platform itself
            and the product is now available in the eodata archive;
            generation of a product may be ordered by a user;

      * 0, product is a native member of the eodata archive;

    Products having statuses 34 or 37 are labeled as _cached_ in the eodata archive.
    It means that such items are provided with limited lifetime.
    However, at the time of writing this comment, the limit had not been declared so far,
    so that the lifetime was unlimited.
    """
    ORDERABLE = 31
    ORDERED = 32
    DOWNLOADED = 34
    PROCESSED = 37
    NATIVE = 0

AVAILABLE_STATUSES = set([CatalogStatus.DOWNLOADED, CatalogStatus.PROCESSED, CatalogStatus.NATIVE])


log = logging.getLogger(__name__)


def query_catalog(collection, query_args, max_records=QUERY_MAX_RECORDS):
    parsed_url = urlparse(QUERY_URL_BASE.format(collection))
    base_url_query_args = parse_qs(parsed_url.query)
    base_url_query_args = {k: v[0] for k, v in base_url_query_args.items()}
    final_query_args = {"index": 1, "maxRecords": max_records}
    final_query_args.update(base_url_query_args)
    final_query_args.update(query_args)
    start_item = int(final_query_args["index"])
    items_per_page = int(final_query_args["maxRecords"])
    while True:
        final_query_args["index"] = start_item
        query_part = urlencode(final_query_args)
        parsed_url = parsed_url._replace(query=query_part)
        url = urlunparse(parsed_url)
        log.debug("Sending query to creodias finder: {:s}".format(url))

        # FIXME:
        # Creodias catalog responds with HTTP 429 Too Many Requests from time to time.
        # This code part implements simple fix handling such HTTP 429.
        # This part should be extracted into standalone function
        # so that it may be exploited by any other http call.
        # Orelse, instead of using plain urllib.request.urlopen()
        # it should be utilized some powerful library, which can handle such HTTP 429.
        response_ok = False
        try_count = 0
        while not response_ok:
            try_count += 1
            try:
                with urlopen(url) as f:
                    response_data = json.load(f)
                    response_ok = True
            except json.decoder.JSONDecodeError as ex1:
                if try_count >= HTTP_MAX_TRIES:
                    log.error("Reached maximum {:d} http tries for the url {:s}."
                              .format(try_count, url))
                    raise
                log.debug("Got JSONdecodeError, sleeping ordered {:d} seconds."
                                  .format(HTTP_SLEEP_TIME))
                sleep(HTTP_SLEEP_TIME)
            except HTTPError as ex:
                if try_count >= HTTP_MAX_TRIES:
                    log.error("Reached maximum {:d} http tries for the url {:s}."
                              .format(try_count, url))
                    raise
                if ex.code == HTTPStatus.TOO_MANY_REQUESTS:
                    sleep_time = None
                    if "Retry-After" in ex.headers:
                        # FIXME:
                        # Retry-After header value may be specified not only by delay-seconds,
                        # however also by HTTP-date.
                        # This simple fix implements delay-seconds only.
                        try:
                            sleep_time = int(ex.headers["Retry-After"])
                        except ValueError:
                            log.warning("Unhandled Retry-After header value {:s}."
                                        .format(repr(ex.headers["Retry-After"])))
                    if sleep_time is None:
                        sleep_time = HTTP_SLEEP_TIME
                        log.debug("Got HTTP Too Many Requests, sleeping predefined {:d} seconds."
                                  .format(sleep_time))
                    else:
                        log.debug("Got HTTP Too Many Requests, sleeping ordered {:d} seconds."
                                  .format(sleep_time))
                    sleep(sleep_time)
                else:
                    raise

        log.debug("Got {:d}/{:d} features in one page for the query: {:s}"
                  .format(len(response_data["features"]), max_records, url))
        start_item += len(response_data["features"])
        for feature in response_data["features"]:
            yield feature
        if len(response_data["features"]) < items_per_page:
            break


def prepare_sentinel2_scene_infos(catalog_items):
    scene_infos = []
    for catalog_item in catalog_items:
        scene_info = parse_sentinel2_name(catalog_item["properties"]["title"])
        scene_info["cloud_cover"] = catalog_item["properties"]["cloudCover"]
        scene_info["status"] = catalog_item["properties"]["status"]
        scene_info["path"] = Path(catalog_item["properties"]["productIdentifier"])
        scene_infos.append(scene_info)
    return scene_infos


#
# Utility functions for managing orders.
#

def get_token(username, password):
    URL = "https://auth.creodias.eu/auth/realms/DIAS/protocol/openid-connect/token"
    params = {"client_id": "CLOUDFERRO_PUBLIC",
              "grant_type": "password",
              "username": username,
              "password": password}
    data = urlencode(params)
    data = data.encode()
    with urlopen(Request(URL, data=data, method="POST")) as f:
        resp_json = json.load(f)
    return resp_json["access_token"]


def submit_order(token, order_name, item_names, processor="sen2cor"):
    URL = "https://finder.creodias.eu/api/order/"
    headers = {"Content-Type": "application/json",
               "Keycloak-Token": token}
    params = {"processor": processor,
              "prority": 1,
              "order_name": order_name,
              "identifier_list": item_names}
    data = json.dumps(params)
    data = data.encode()
    with urlopen(Request(URL, data=data, headers=headers, method="POST")) as f:
        resp_json = json.load(f)
    return resp_json["id"]


def get_order_status(token, order_ident):
    URL = "https://finder.creodias.eu/api/order/{:d}/".format(order_ident)
    headers = {"Content-Type": "application/json",
               "Keycloak-Token": token}
    log.debug("Sending query: {:s}".format(URL))
    with urlopen(Request(URL, headers=headers, method="GET")) as f:
        resp_json = json.load(f)
    return resp_json["status"]


def get_order_items(token, order_ident):
    url = "https://finder.creodias.eu/api/order/{:d}/order_items/?page=1".format(order_ident)
    headers = {"Content-Type": "application/json",
               "Keycloak-Token": token}
    order_items = []
    while True:
        log.debug("Sending query: {:s}".format(url))
        with urlopen(Request(url, headers=headers, method="GET")) as f:
            resp_json = json.load(f)
        order_items.extend(resp_json["results"])
        if resp_json["next"] is None:
            break
        url = resp_json["next"]
    return order_items
