#!/usr/bin/env python

from sys import exit, stdout
from collections import defaultdict
from datetime import datetime
import numpy
import argparse
import logging

from libBGG.BGGAPI import BGGAPI
from libBGG.BGGCache import BGGCache
from libBGG.Boardgame import Boardgame
from libBGG.User import User
from libBGG.Collection import Collection
from libBGG.Guild import Guild

log = logging.getLogger(__name__)


if __name__ == "__main__":
    desc = ('Show top ratings for a given guild.')
    ap = argparse.ArgumentParser(description=desc)
    ap.add_argument('-g', '--guild', dest='guild',
                    help='ID of guild. (bgg does not support guild by name search.')
    ap.add_argument('-n', '--number', dest='number', default=100, type=int,
                    help='How many games to show. (Default=100)')
    ap.add_argument('-C', '--cache', dest='cache', help='Path to cache. If given look in '
                    'cache first, then fetch from BGG. Otherwise always fetch from BGG.'
                    ' collection. If --forcefetch is given, force a fetch into the cache.')
    ap.add_argument('-f', '--forcefetch', dest='forcefetch', help='If given, force a refetch '
                    'of any data. This argument does nothing if a cache is not given.',
                    action='store_true')
    ap.add_argument('-H', '--htmlout', dest='htmlout', help='If given, write an HTML table of '
                    ' the data to the given file.')
    ap.add_argument('-w', '--wikiout', dest='wikiout', help='If given, write a wiki formatted '
                    ' version of the data to the given file.')
    ap.add_argument("-l", "--loglevel", dest="loglevel",
                    help="The level at which to log. Must be one of "
                    "none, debug, info, warning, error, or critical. Default is none. ("
                    "This is mostly used for debugging.)",
                    default='none', choices=['none', 'all', 'debug', 'info', 'warning',
                                             'error', 'critical'])
    args = ap.parse_args()

    if args.guild is None:
        ap.print_help()
        print('\nYou must specify a guild ID with -g (or --guild)')
        exit(1)
    else:
        try:
            int(args.guild)
        except TypeError:
            ap.print_help()
            print('\nGuild value must be a guild ID - an integer.')
            exit(2)

    logLevels = {
        'none': 100,
        'all': 0,
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'critical': logging.CRITICAL
    }
    log_format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
    log_datefmt = '%m-%d %H:%M:%S'
    logging.basicConfig(format=log_format, datefmt=log_datefmt,
                        level=logLevels[args.loglevel])

    # now do some real work.
    if args.cache is not None:
        api = BGGAPI(BGGCache(args.cache))
    else:
        api = BGGAPI()

    guild = api.fetch_guild(args.guild, forcefetch=args.forcefetch)
    if guild is None:
        print('Error fetching guild info.')
        exit(2)

    print('Fetched member information for guild "%s"' % guild.name)

    collections = defaultdict(str)
    print('Fetching %d member collections.' % len(guild.members), end='')
    stdout.flush()
    game_num = 0
    rating_num = 0
    for member in guild.members:
        collections[member] = api.fetch_collection(member, forcefetch=args.forcefetch)
        if collections[member] is not None:
            print('.', end='')
            stdout.flush()
            game_num += len(collections[member].games)
            rating_num += len(collections[member].rating)
        else:
            print('Error fetching collection for %s' % member)

    print('Fetched %d collections totalling %d games and %d ratings.' % (
        len(collections), game_num, rating_num))

    print('Computing ratings...')
    rating_map = defaultdict(list)
    bgid_map = defaultdict(list)
    for member, collection in collections.items():
        if not collection:
            log.warn('Error getting collection for %s. Ignoring the collection!' % member)
            continue
        for game_name, rating in collection.rating.items():
            if rating.userrating is not None:
                bg = api.fetch_boardgame(name=None, bgid=rating.bgid)
                if not bg:
                    log.info('Ignoring game %s. It has no bgid')
                    continue
                if getattr(bg, 'categories'):
                    if not 'Expansion for Base-game' in bg.categories:
                        rating_map[rating.name].append(rating.userrating)
                        bgid_map[rating.name] = rating.bgid
                    else:
                        log.info('excluding expansion %s (id:%s)' % (rating.name, rating.bgid))
                else:
                    log.info('Ignoring %s - it has no categories' % bg.name)

    # filter out games that have not been rated by at least %5 of the guild
    min_rating = int(len(guild.members) * 0.05)
    rating_map = {k: v for k, v in rating_map.items() if len(rating_map[k]) > min_rating}

    # games to ignore for some reason, by bgg id.
    # 40834 --> Dominion Intrgue (really just Dominion)
    ignored_games = ['40834']
    rating_map = {k: v for k, v in rating_map.items() if not bgid_map[k] in ignored_games}

    stats = defaultdict(lambda: defaultdict(float))
    for name, data in rating_map.items():
        a = numpy.array(data, dtype='float64')   # convert to float
        stats[name]['mean'] = a.mean()
        stats[name]['stddev'] = a.std()
        stats[name]['n'] = a.size
    
    print()

    # top is list of game names sorted by mean.
    top = sorted(stats, key=lambda x: stats[x]['mean'], reverse=True)

    title = 'Ratings for guild %s:' % guild.name
    print('%s\n%s' % (title, '=' * len(title)))
    print('Rank Rating Rated Stddev Name\n---- ------ ----- ------ ----')
    i = 1
    for name in top[:args.number]:
        print('%3d. %6.2f %5d %6.2f %s' % (
            i, stats[name]['mean'], stats[name]['n'], stats[name]['stddev'], name))
        i = i + 1

    #print('Computed at: %s' % datetime.ctime(datetime.now()))
    #print('Using %d ratings from %d guild members.' % (rating_num, len(guild.members)))

    if args.htmlout:
        with open(args.htmlout, 'w') as fd:
            i = 1
            fd.write('<table><thead>\n<tr>\n<th align="right"></th>\n<th align="left">Game</th>\n<th align="right">Rating</th>\n<th align="left"></th>\n</tr>\n</thead><tbody>\n')
            for name in top[:args.number]:
                fd.write('<tr>\n')
                fd.write('<td align="right">%d</td>\n' % i)
                fd.write('<td align="left"><a href="http://boardgamegeek.com/boardgame/%s">%s</a></td>\n' % (bgid_map[name], stats[name]))
                fd.write('<td align="right">%6.2s</td>\n' % stats[name]['mean'])
                fd.write('<td align="left"><strong>▲X</strong></td>\n')
                fd.write('</tr>\n')
                i = i + 1
            fd.write('</tbody></table>\n')
            fd.write('Computed at: %s ' % datetime.now().isoformat())
            fd.write('using %d ratings from %d guild members.\n' % (rating_num, len(guild.members)))

    if args.wikiout:
        with open(args.wikiout, 'w') as fd:
            i = 1
            fd.write('|Rank|Game|Rating|+/-|Raters\n|--:|:-----------|---------------:|--:|-----:|\n')
            for name in top[:args.number]:
                fd.write('|%d|[%s](http://boardgamegeek.com/boardgame/%s)|%4.2f|%s|%d\n' % (
                    i, name, bgid_map[name], stats[name]['mean'], ' ', stats[name]['n']))
                i = i + 1
            fd.write('Computed at: %s ' % datetime.now().isoformat())
            fd.write('using %d ratings from %d guild members.\n' % (rating_num, len(guild.members)))

    exit(0)
