from datetime import datetime, timedelta

from flask import current_app
from notifications_utils.timezones import convert_utc_to_bst
from sqlalchemy import asc, desc, func

from app import db
from app.constants import SMS_TYPE
from app.dao.dao_utils import autocommit
from app.models import (
    FactBilling,
    ProviderDetails,
    ProviderDetailsHistory,
    User,
)


def get_provider_details_by_id(provider_details_id):
    return ProviderDetails.query.get(provider_details_id)


def get_provider_details_by_identifier(identifier):
    return ProviderDetails.query.filter_by(identifier=identifier).one()


def get_alternative_sms_provider(identifier):
    if identifier == "firetext":
        return "mmg"
    elif identifier == "mmg":
        return "firetext"
    raise ValueError("Unrecognised sms provider {}".format(identifier))


def dao_get_provider_versions(provider_id):
    return (
        ProviderDetailsHistory.query.filter_by(id=provider_id)
        .order_by(desc(ProviderDetailsHistory.version))
        .limit(100)  # limit results instead of adding pagination
        .all()
    )


def _adjust_provider_priority(provider, new_priority):
    current_app.logger.info(
        "Adjusting provider priority - %s going from %s to %s", provider.identifier, provider.priority, new_priority
    )
    provider.priority = new_priority

    # Automatic update so set as notify user
    provider.created_by_id = current_app.config["NOTIFY_USER_ID"]

    # update without commit so that both rows can be changed without ending the transaction
    # and releasing the for_update lock
    _update_provider_details_without_commit(provider)


def _get_sms_providers_for_update(time_threshold):
    """
    Returns a list of providers, while holding a for_update lock on the provider details table, guaranteeing that those
    providers won't change (but can still be read) until you've committed/rolled back your current transaction.

    if any of the providers have been changed recently, it returns an empty list - it's still your responsiblity to
    release the transaction in that case
    """
    # get current priority of both providers
    q = (
        ProviderDetails.query.filter(ProviderDetails.notification_type == "sms", ProviderDetails.active)
        .with_for_update()
        .all()
    )

    # if something updated recently, don't update again. If the updated_at is null, treat it as min time
    if any((provider.updated_at or datetime.min) > datetime.utcnow() - time_threshold for provider in q):
        current_app.logger.info("Not adjusting providers, providers updated less than %s ago.", time_threshold)
        return []

    return q


@autocommit
def dao_reduce_sms_provider_priority(identifier, *, time_threshold):
    """
    Will reduce a chosen sms provider's priority, and increase the other provider's priority by 10 points each.
    If either provider has been updated in the last `time_threshold`, then it won't take any action.
    """
    amount_to_reduce_by = 10
    providers_list = _get_sms_providers_for_update(time_threshold)

    if len(providers_list) < 2:
        current_app.logger.info("Not adjusting providers, number of active providers is less than 2.")
        return

    providers = {provider.identifier: provider for provider in providers_list}
    other_identifier = get_alternative_sms_provider(identifier)

    reduced_provider = providers[identifier]
    increased_provider = providers[other_identifier]

    # always keep values between 0 and 100
    reduced_provider_priority = max(0, reduced_provider.priority - amount_to_reduce_by)
    increased_provider_priority = min(100, increased_provider.priority + amount_to_reduce_by)

    _adjust_provider_priority(reduced_provider, reduced_provider_priority)
    _adjust_provider_priority(increased_provider, increased_provider_priority)


@autocommit
def dao_adjust_provider_priority_back_to_resting_points():
    """
    Provided that neither SMS provider has been modified in the last hour, move both providers by 10 percentage points
    each towards their defined resting points (set in SMS_PROVIDER_RESTING_POINTS in config.py).
    """
    amount_to_reduce_by = 10
    time_threshold = timedelta(hours=1)

    providers = _get_sms_providers_for_update(time_threshold)

    for provider in providers:
        target = current_app.config["SMS_PROVIDER_RESTING_POINTS"][provider.identifier]
        current = provider.priority

        if current != target:
            if current > target:
                new_priority = max(target, provider.priority - amount_to_reduce_by)
            else:
                new_priority = min(target, provider.priority + amount_to_reduce_by)

            _adjust_provider_priority(provider, new_priority)


def get_provider_details_by_notification_type(notification_type, supports_international=False):

    filters = [ProviderDetails.notification_type == notification_type]

    if supports_international:
        filters.append(ProviderDetails.supports_international == supports_international)

    return ProviderDetails.query.filter(*filters).order_by(asc(ProviderDetails.priority)).all()


@autocommit
def dao_update_provider_details(provider_details):
    _update_provider_details_without_commit(provider_details)


def _update_provider_details_without_commit(provider_details):
    """
    Doesn't commit, for when you need to control the database transaction manually
    """
    provider_details.version += 1
    provider_details.updated_at = datetime.utcnow()
    history = ProviderDetailsHistory.from_original(provider_details)
    db.session.add(provider_details)
    db.session.add(history)


def dao_get_provider_stats():
    # this query does not include the current day since the task to populate ft_billing runs overnight

    current_bst_datetime = convert_utc_to_bst(datetime.utcnow())
    first_day_of_the_month = current_bst_datetime.date().replace(day=1)

    subquery = (
        db.session.query(
            FactBilling.provider,
            func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label("current_month_billable_sms"),
        )
        .filter(FactBilling.notification_type == SMS_TYPE, FactBilling.bst_date >= first_day_of_the_month)
        .group_by(FactBilling.provider)
        .subquery()
    )

    result = (
        db.session.query(
            ProviderDetails.id,
            ProviderDetails.display_name,
            ProviderDetails.identifier,
            ProviderDetails.priority,
            ProviderDetails.notification_type,
            ProviderDetails.active,
            ProviderDetails.updated_at,
            ProviderDetails.supports_international,
            User.name.label("created_by_name"),
            func.coalesce(subquery.c.current_month_billable_sms, 0).label("current_month_billable_sms"),
        )
        .outerjoin(subquery, ProviderDetails.identifier == subquery.c.provider)
        .outerjoin(User, ProviderDetails.created_by_id == User.id)
        .order_by(
            ProviderDetails.notification_type,
            ProviderDetails.priority,
        )
        .all()
    )

    return result
