from collections import defaultdict
from datetime import datetime, timedelta
from functools import wraps

import little_data.database as db

datecols = ('dat', 'date', 'start', 'end', 'strt_date', 'stop_date')
dates = (
    {'date': datetime(2014,  9, 15), 'name': 'Co-op 1'},
    {'date': datetime(2015,  3, 30), 'name': 'Spring\nQuarter'},
    {'date': datetime(2015,  6, 13), 'name': 'Summer\nBreak'},
    {'date': datetime(2015,  6, 22), 'name': 'Summer\nQuarter'},
    {'date': datetime(2015,  9,  6), 'name': 'Fall\nBreak'},
    {'date': datetime(2015,  9, 21), 'name': 'Co-op 2'},
    {'date': datetime(2016,  3, 28), 'name': 'Spring\nQuarter'},
    {'date': datetime(2016,  6, 11), 'name': 'Summer\nBreak'},
    {'date': datetime(2016,  6, 20), 'name': 'Summer\nQuarter'},
    {'date': datetime(2016,  9,  4), 'name': 'Fall\nBreak'},
    {'date': datetime(2016,  9, 19), 'name': 'Fall\nQuarter'},
    {'date': datetime(2016, 12, 11), 'name': 'Winter\nBreak'},
    {'date': datetime(2017,  1,  9), 'name': 'Winter\nQuarter'},
    {'date': datetime(2017,  3, 26), 'name': 'Spring\nBreak'},
    {'date': datetime(2017,  4,  3), 'name': 'Spring\nQuarter'},
    {'date': datetime(2017,  6, 17), 'name': 'Summer\nBreak'},
    {'date': datetime(2017,  6, 26), 'name': 'Summer\nQuarter'},
    {'date': datetime(2017,  9, 10), 'name': 'Fall\nBreak'},
    {'date': datetime(2017,  9, 25), 'name': 'Fall\nQuarter'},
    {'date': datetime(2017, 12, 17), 'name': 'Winter\nBreak'},
    {'date': datetime(2018,  1,  8), 'name': 'Winter\nQuarter'},
    {'date': datetime(2018,  3, 25), 'name': 'Spring\nBreak'},
    {'date': datetime(2018,  4,  2), 'name': 'Spring\nQuarter'},
    {'date': datetime(2018,  6, 16), 'name': 'Graduation'},
)


utilities = ('Electricity', 'Gas', 'Internet', 'Utilities')

datestr = "%Y-%m-%d"


def date2str(date):
    """Standardized date string representation"""
    return date.strftime(datestr)


def str2date(date):
    return datetime.strptime(date, datestr)


def primitivized(func):
    """Casts database data into usable python primitive format (primarily for later casting into JSON)"""
    # Two functions so I can use a generator but return a list
    def primitivizer(*args, **kwargs):
        for row in func(*args, **kwargs):
            if not isinstance(row, dict):
                d = row._asdict()
            else:
                d = dict(row)
            for col in datecols:
                if col in d and d[col]:
                    d[col] = date2str(d[col])
            if 'amount' in d:
                d['amount'] = round(d['amount'], 2)
            yield d

    @wraps(func)
    def wrapper(*args, **kwargs):
        return list(primitivizer(*args, **kwargs))
    return wrapper


def day_filled(func):
    """Fills date gaps in input stream with 0s"""
    # Two functions so I can use a generator but return a list
    def filler(*args, **kwargs):
        last = None
        for row in func(*args, **kwargs):
            date = str2date(row['date'])
            if last is not None:
                diff = date - last
                if diff > timedelta(days=1):
                    for i in range(diff.days-1):
                        yield {"date": date2str(last + timedelta(i+1)), "amount": 0}
            last = date
            yield row

    @wraps(func)
    def wrapper(*args, **kwargs):
        return list(filler(*args, **kwargs))
    return wrapper


@primitivized
def transactions(*filters):
    """List of all transactions, newest first"""
    return db.transactions().filter(*filters)


@primitivized
def projections(*filters):
    """List of all transactions, newest first"""
    return db.projections().filter(*filters)


def period(transaction):
    for n in range(int((transaction.stop_date - transaction.strt_date).days) + 1):
        yield transaction.strt_date + timedelta(n)


@day_filled
@primitivized
def averaged_daily(*filters):
    filters = filters or (db.category.notin_(('Transfer', 'Adjustment')),)
    ranged_totals = defaultdict(lambda: 0)
    for t in db.ranged_transactions().filter(*filters):
        t_dates = list(period(t))
        avg_val = t.amount / len(t_dates)
        for t_date in t_dates:
            ranged_totals[t_date] += avg_val
    for t in db.rangeless_transactions().filter(*filters):
        ranged_totals[t.date] += t.amount
    for day in sorted(ranged_totals):
        if day < datetime.today().date():
            yield {'date': day, 'amount': ranged_totals[day]}


def averaged_daily_cat(*cats):
    return list(reversed(list(averaged_daily(db.category.in_(cats)))))


@primitivized
def net_worth():
    """Net worth by day, oldest first"""
    total = 0
    for day in db.daily():
        total += day.amount
        yield {'date': day.date, 'amount': total}


@day_filled
@primitivized
def raw_projected(end=None):
    """Net worth by day, oldest first"""
    end = end or datetime.today().replace(hour=0, minute=0, second=0, microsecond=0).date()
    last = end
    # Initial for date filling
    yield {'date': last, 'amount': 0}
    for day in db.daily_projected().filter(db.Projected.date > end):
        last = day.date
        yield day
    # Final for date filling
    yield {'date': max(last, end+timedelta(28*24)), 'amount': 0}


def projection(averaged, end=None):
    total = None
    for d in net_worth():
        total = d['amount']
        yield d
    avg = db.average(28*6).filter(db.category.in_(averaged)).first().amount
    print(avg)
    for day in raw_projected(end):
        total = total + avg + day['amount']
        yield {'date': day['date'], 'amount': total}


@primitivized
def notable_dates():
    """Notable events with dates for annotations"""
    return dates


@primitivized
def past_dates():
    """Notable events with dates for annotations, excluding future dates"""
    return [date for date in dates if date['date'] <= datetime.today()]
