"""
Contains all functions that access an Endpoint object
"""
import datetime
from collections import defaultdict

from sqlalchemy import func, desc
from sqlalchemy.orm.exc import NoResultFound

from flask_monitoringdashboard.core.timezone import to_local_datetime
from flask_monitoringdashboard.database import Request, Endpoint


def get_num_requests(session, endpoint_id, start_date, end_date):
    """
    Returns a list with all dates on which an endpoint is accessed.
    :param session: session for the database
    :param endpoint_id: if None, the result is the sum of all endpoints
    :param start_date: datetime.date object
    :param end_date: datetime.date object
    :return list of dates
    """
    query = session.query(Request.time_requested)
    if endpoint_id:
        query = query.filter(Request.endpoint_id == endpoint_id)
    result = query.filter(
        Request.time_requested >= start_date, Request.time_requested <= end_date
    ).all()

    return group_request_times([r[0] for r in result])


def group_request_times(datetimes):
    """
    Returns a list of tuples containing the number of hits per hour
    :param datetimes: list of datetime objects
    :return list of tuples ('%Y-%m-%d %H:00:00', count)
    """
    hours_dict = defaultdict(int)
    for dt in datetimes:
        round_time = dt.strftime('%Y-%m-%d %H:00:00')
        hours_dict[round_time] += 1
    return hours_dict.items()


def get_users(session, endpoint_id, limit=None):
    """
    Returns a list with the distinct group-by from a specific endpoint. The limit is used to
    filter the most used distinct.
    :param session: session for the database
    :param endpoint_id: the id of the endpoint to filter on
    :param limit: the max number of results
    :return a list of tuples (group_by, hits)
    """
    query = (
        session.query(Request.group_by, func.count(Request.group_by))
        .filter(Request.endpoint_id == endpoint_id)
        .group_by(Request.group_by)
        .order_by(desc(func.count(Request.group_by)))
    )
    if limit:
        query = query.limit(limit)
    result = query.all()
    session.expunge_all()
    return result


def get_ips(session, endpoint_id, limit=None):
    """
    Returns a list with the distinct group-by from a specific endpoint. The limit is used to
    filter the most used distinct.
    :param session: session for the database
    :param endpoint_id: the endpoint_id to filter on
    :param limit: the number of
    :return a list with the group_by as strings.
    """
    query = (
        session.query(Request.ip, func.count(Request.ip))
        .filter(Request.endpoint_id == endpoint_id)
        .group_by(Request.ip)
        .order_by(desc(func.count(Request.ip)))
    )
    if limit:
        query = query.limit(limit)
    result = query.all()
    session.expunge_all()
    return result


def get_endpoint_by_name(session, endpoint_name):
    """
    Returns the Endpoint object from a given endpoint_name.
    If the result doesn't exist in the database, a new row is added.
    :param session: session for the database
    :param endpoint_name: string with the endpoint name
    :return Endpoint object
    """
    try:
        result = session.query(Endpoint).filter(Endpoint.name == endpoint_name).one()
        result.time_added = to_local_datetime(result.time_added)
        result.last_requested = to_local_datetime(result.last_requested)
    except NoResultFound:
        result = Endpoint(name=endpoint_name)
        session.add(result)
        session.flush()
    session.expunge(result)
    return result


def get_endpoint_by_id(session, endpoint_id):
    """
    Returns the Endpoint object from a given endpoint id.
    :param session: session for the database
    :param endpoint_id: id of the endpoint.
    :return Endpoint object
    """
    result = session.query(Endpoint).filter(Endpoint.id == endpoint_id).one()
    session.expunge(result)
    return result


def update_endpoint(session, endpoint_name, value):
    """
    Updates the value of a specific Endpoint.
    :param session: session for the database
    :param endpoint_name: name of the endpoint
    :param value: new monitor level
    """
    session.query(Endpoint).filter(Endpoint.name == endpoint_name).update(
        {Endpoint.monitor_level: value}
    )
    session.flush()


def get_last_requested(session):
    """
    Returns the accessed time of all endpoints.
    :param session: session for the database
    :return list of tuples with name of the endpoint and date it was last used
    """
    result = session.query(Endpoint.name, Endpoint.last_requested).all()
    session.expunge_all()
    return result


def update_last_requested(session, endpoint_name, timestamp=None):
    """
    Updates the timestamp of last access of the endpoint.
    :param session: session for the database
    :param endpoint_name: name of the endpoint
    :param timestamp: optional timestamp. If not given, timestamp is current time
    """
    ts = timestamp if timestamp else datetime.datetime.utcnow()
    session.query(Endpoint).filter(Endpoint.name == endpoint_name).update(
        {Endpoint.last_requested: ts}
    )


def get_endpoints(session):
    """
    Returns all Endpoint objects from the database.
    :param session: session for the database
    :return list of Endpoint objects, sorted on the number of requests (descending)
    """
    return (
        session.query(Endpoint)
        .outerjoin(Request)
        .group_by(Endpoint.id)
        .order_by(desc(func.count(Request.endpoint_id)))
    )


def get_endpoints_hits(session):
    """
    Returns all endpoint names and total hits from the database.
    :param session: session for the database
    :return list of (endpoint name, total hits) tuples
    """
    return (
        session.query(Endpoint.name, func.count(Request.endpoint_id))
        .join(Request)
        .group_by(Endpoint.name)
        .order_by(desc(func.count(Request.endpoint_id)))
        .all()
    )


def get_avg_duration(session, endpoint_id):
    """ Returns the average duration of all the requests of an endpoint. If there are no requests
        for that endpoint, it returns 0.
    :param session: session for the database
    :param endpoint_id: id of the endpoint
    :return average duration
    """
    result = (
        session.query(func.avg(Request.duration).label('average'))
        .filter(Request.endpoint_id == endpoint_id)
        .one()
    )
    if result[0]:
        return result[0]
    return 0


def get_endpoint_averages(session):
    """ Returns the average duration of all endpoints. If there are no requests for an endpoint,
        the average will be none.
    :param session: session for the database
    :return tuple of (endpoint_name, avg_duration)
    """
    result = (
        session.query(Endpoint.name, func.avg(Request.duration).label('average'))
        .outerjoin(Request)
        .group_by(Endpoint.name)
        .all()
    )
    return result
