"""metrics"""
import itertools
from ..error import ForecastError


def get_reference(df, shows, clicks):
    """(DataFrame, int?, int?) -> DataFrame

    Return:
      DataFrame - index = ("PhraseWordsCnt", "QueryWordsCnt"); cols = Shows, Clicks, Cost
    """
    MIN_REF = 4
    MIN_SHOWS = 24
    MIN_CLICKS = 4
    if shows <= MIN_SHOWS or clicks <= MIN_CLICKS:
        raise ForecastError("there is not enough data to make a forecast")

    df_chart = (
        df[["Shows", "Clicks", "Cost", "PhraseWordsCnt", "QueryWordsCnt"]]
        .loc[df["Shows"] > df["Clicks"]]
        .groupby(["PhraseWordsCnt", "QueryWordsCnt"])
        .agg({
            "Shows": lambda x: x.sum(),
            "Clicks": lambda x: x.sum(),
            "Cost": lambda x: x.sum()
        }))

    # reference is filtered by MIN_SHOWS, MIN_CLICKS
    cond = (df_chart.Shows > shows) & (df_chart.Clicks > clicks)
    df_chart = df_chart[cond]
    if df_chart.index.size >= MIN_REF:
        return df_chart
    else:
        return get_reference(df,
                             max(shows * 0.8, MIN_SHOWS),
                             max(clicks * 0.8, MIN_CLICKS))


def make_cached_nearest_vals_from_reference():
    "() -> (DataFrame, int, int) -> dict<'Shows'|'Clicks'|'Cost', int|float>"

    def _euclidean_dist2d(p1, p2):
        """((number, number), (number, number)) -> float"""

        x1, y1 = p1
        x2, y2 = p2
        dx = x1 - float(x2)
        dy = y1 - float(y2)

        return (dx**2.0 + dy**2.0)**0.5

    def _nearest_vals_from_reference(reference, phrase_wc, query_wc):
        "(DataFrame, int, int) -> dict<'Shows'|'Clicks'|'Cost', int|float>"

        # [((PhraseWordsCnt::int, QueryWordsCnt::int), distance::float)]
        distances = sorted(
            [
                # x :: (int, int)
                (x, _euclidean_dist2d((phrase_wc, query_wc), x))
                for x in reference.index.tolist()
            ],
            key=lambda (_, dist): dist)
        # group by distance; it can contain several idx from reference
        _, first_group = itertools.groupby(
            distances, key=lambda (_, dist): dist).next()

        nearest_indexes = [idx for idx, _ in first_group]  # noqa: F812

        ref = {
            "Shows": float(reference.loc[nearest_indexes]["Shows"].sum()),
            "Clicks": float(reference.loc[nearest_indexes]["Clicks"].sum()),
            "Cost": float(reference.loc[nearest_indexes]["Cost"].sum())
        }

        return ref

    memo = {}

    def wrapped(reference, phrase_wc, query_wc):
        key = phrase_wc, query_wc
        if key in memo:
            return memo[key]
        else:
            val = _nearest_vals_from_reference(reference, phrase_wc, query_wc)
            memo[key] = val
            return val

    return wrapped
