import json
import os
from typing import List, Tuple, Union

import numpy as np

from worker.data.api import API
from worker.data.enums import EventTypes, Environment
from worker.exceptions import NotFound


def gather_data_for_period(asset_id: int, start: int, end: int, limit: int = 1800, collection: str = "wits") -> list:
    """
    Get the wits data from API for an asset over an interval
    :param asset_id: asset id
    :param start: start timestamp
    :param end: end timestamp
    :param limit: count of the data
    :param collection: any collection
    :return: a list of wits data
    """
    if start >= end:
        return []

    query = '{timestamp#gte#%s}AND{timestamp#lte#%s}' % (start, end)
    worker = API()
    wits_dataset = worker.get(
        path="/v1/data/corva", collection=collection, asset_id=asset_id, sort="{timestamp: 1}", limit=limit, query=query
    ).data

    if not wits_dataset:
        return []

    return wits_dataset


def get_one_data_record(asset_id: int, timestamp_sort: int = -1, collection: str = "wits") -> dict:
    """
    Get the first or last wits record of a given asset
    :param asset_id:
    :param timestamp_sort:
    :param collection:
    :return:
    """
    api_worker = API()
    data = api_worker.get(path='/v1/data/corva/', collection=collection, asset_id=asset_id,
                          sort="{timestamp:%s}" % timestamp_sort, limit=1).data
    if not data:
        return {}

    return data[0]


def delete_collection_data_of_asset_id(asset_id: int, collections: Union[str, list]):
    """
    Delete all the data of a collection for an asset id.
    :param asset_id:
    :param collections: a collection or a list of collections
    :return:
    """
    worker = API()

    if isinstance(collections, str):
        collections = [collections]

    for collection in collections:
        path = "/v1/data/corva/%s" % collection
        query = "{asset_id#eq#%s}" % asset_id
        worker.delete(path=path, query=query)


def point_main_envs(env: str):
    """
    The purpose of this function is to point main environment variables to
    the provided environment. This method updates the following environment
    variables:
    - API_ROOT_URL
    - API_KEY
    - CACHE_URL

    :param env: the environment to point to
    :return:
    """
    if not env:
        return

    # validating the environment
    Environment(env)

    api_url = os.getenv(f"API_ROOT_URL_{env}")
    api_key = os.getenv(f"API_KEY_{env}")
    cache_url = os.getenv(f"CACHE_URL_{env}")

    if not all([api_url, api_key, cache_url]):
        raise ValueError("Missing environment variables!")

    new_envs = {
        'API_ROOT_URL': api_url,
        'API_KEY': api_key,
        'CACHE_URL': cache_url
    }

    os.environ.update(new_envs)


def setup_api_worker(env: str, app_name: str):
    """
    Set up the Corva API worker
    :param env: environment ['local', 'qa', 'staging', 'production']
    :param app_name:
    :return: api worker
    """
    # validating the environment
    Environment(env)

    api_url = os.getenv(f"API_ROOT_URL_{env}")
    api_key = os.getenv(f"API_KEY_{env}")

    options = {
        "api_url": api_url,
        "api_key": api_key,
        "app_name": app_name
    }
    api_worker = API(**options)

    return api_worker


def setup_redis_worker(env: str) -> Tuple:
    """
    Set up the Redis worker
    :param env: environment ['local', 'qa', 'staging', 'production']
    :return: redis worker
    """
    # validating the environment
    Environment(env)

    cache_url = os.getenv(f"CACHE_URL_{env}")

    import redis
    redis_worker = redis.Redis.from_url(cache_url, decode_responses=True)

    return redis_worker


def get_drillstring_by_id(drillstring_id: str) -> Union[dict, None]:
    """
    Get the drillstring from API by providing mongodb _id
    :param drillstring_id: mongodb _id of the drillstring
    :return:
    """
    drillstring = None
    try:
        drillstring = API().get_by_id(
            path="/v1/data/corva/", collection='data.drillstring', id=drillstring_id
        ).data
    except NotFound:
        pass

    return drillstring


def is_number(data):
    """
    Check and return True if data is a number, else return False
    :param data: Input can be string, number or nan
    :return: True or False
    """
    try:
        data_cast = float(data)
        if data_cast >= 0 or data_cast <= 0:  # to make sure it is a valid number
            return True

        return False
    except ValueError:
        return False
    except TypeError:
        return False


def is_finite(data):
    """
    Check if the given data is a finite number
    Note that the string representation of a number is not finite
    :param data:
    :return: True or False
    """
    try:
        return is_number(data) and np.isfinite(data)
    except (TypeError, ValueError):
        return False


def is_int(s: str) -> bool:
    """
    To check if the given string is an integer or not
    :param s:
    :return:
    """
    try:
        int(s)
        return True
    except ValueError:
        return False


def to_number(data):
    """
    Check and return if the data can be cast to a number, else return None
    :param data: Input can be string, number or nan
    :return: A numbers
    """
    if is_number(data):
        return float(data)

    return None


def none_to_nan(data):
    """
    If data is a list, return list with None replaced with nan.
    If data is None, return nan
    :param data:
    :return:
    """
    if isinstance(data, list):
        return [np.nan if e is None else e for e in data]

    if data is None:
        return np.nan

    return data


def get_data_by_path(data: dict, path: str, func=lambda x: x, **kwargs):
    """
    To find the path to a key in a nested dictionary.
    Note that none of the keys should end up in a list
    :param data:
    :param path: path to the final key; example of the paths are:
        'data.X.Y'
        'data.bit_depth'
    :param func: the type of the data (int, str, float, ...)
    :param kwargs: pass default value in case the path not found;
    note that None is an acceptable default
    :return:
    """
    has_default = 'default' in kwargs
    default = kwargs.pop('default', None)

    if not path:
        if has_default:
            return default

        raise KeyError("No key provided")

    keys = path.split('.')

    while keys:
        current_key = keys.pop(0)

        if current_key not in data:
            if has_default:
                return default

            raise KeyError("{0} not found in path".format(current_key))

        data = data.get(current_key)

    if data is None:
        return None

    return func(data)


def is_in_and_not_none(d: dict, key: str):
    """
    An structured way of getting data from a dict.
    :param d: the dictionary
    :param key:
    :return: True or False
    """
    if key in d.keys() and d[key] is not None:
        return True

    return False


def nanround(value, decimal_places=2):
    """
    Similar to python built-in round but considering None values as well
    :param value:
    :param decimal_places:
    :return:
    """
    if is_number(value):
        return round(value, decimal_places)

    return None


def merge_dicts(d1: dict, d2: dict) -> dict:
    """
    Merge two dictionaries
    Note: the 2nd item (d2) has a higher priority to write items with similar keys
    :param d1:
    :param d2:
    :return:
    """
    d = {**d1, **d2}
    return d


def equal(obj1: object, obj2: object, params: List[str]) -> bool:
    """
    To check if two objects are equal by comparing the given parameters.
    :param obj1:
    :param obj2:
    :param params:
    :return:
    """
    if type(obj1) is not type(obj2):
        return False

    return all(getattr(obj1, param) == getattr(obj2, param) for param in params)


def get_cleaned_event_and_type(event) -> Tuple[Union[list, dict], EventTypes]:
    """
    validate and flatten the events and organize the data into a desired format

    Task and generic events format is : dict => {}
    Scheduler events format is: list of list of dict => [[{}]]
    Kafka events format is: list of dict => [{}]
    The above formats can be used to determine the format

    :param event: a scheduler of kafka stream
    :return: event and event_type
    """

    if not event:
        raise ValueError("Empty events")

    if isinstance(event, (str, bytes, bytearray)):
        event = json.loads(event)

    if isinstance(event, dict):
        if "asset_id" in event:
            return event, EventTypes.GENERIC

        if "task_id" in event:
            return event, EventTypes.TASK

        raise TypeError("Missing task_id or asset_id keys in event")

    if not isinstance(event, list):
        raise TypeError("Event is not a list or a dict")

    first_event = event[0]
    if isinstance(first_event, list):
        if first_event[0] and "schedule_start" in first_event[0]:
            return event, EventTypes.SCHEDULER

        raise Exception("Missing scheduler_start key in scheduler event")

    elif isinstance(first_event, dict):
        # new kafka stream format: list of json objects, each with metadata and records
        # event = [{"metadata": { ... }, "records": [ ... ]}, {"metadata": { ... }, "records": [ ... ]}]
        return event, EventTypes.STREAM

    raise TypeError("Event is not either a scheduler or kafka consumer")
