import sqlite3
import os
import json
import shutil
import sys
import time
from collections import defaultdict
import threading
from datetime import datetime, timedelta

import botconfig
import src.settings as var
from src.utilities import irc_lower, break_long_message, role_order, singular

# increment this whenever making a schema change so that the schema upgrade functions run on start
# they do not run by default for performance reasons
SCHEMA_VERSION = 3

_ts = threading.local()

def init_vars():
    with var.GRAVEYARD_LOCK:
        conn = _conn()
        c = conn.cursor()
        c.execute("""SELECT
                       pl.account,
                       pl.hostmask,
                       pe.notice,
                       pe.simple,
                       pe.deadchat,
                       pe.pingif,
                       pe.stasis_amount,
                       pe.stasis_expires,
                       COALESCE(at.flags, a.flags)
                     FROM person pe
                     JOIN player pl
                       ON pl.person = pe.id
                     LEFT JOIN access a
                       ON a.person = pe.id
                     LEFT JOIN access_template at
                       ON at.id = a.template
                     WHERE pl.active = 1""")

        var.SIMPLE_NOTIFY = set()  # cloaks of people who !simple, who don't want detailed instructions
        var.SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence
        var.PREFER_NOTICE = set()  # cloaks of people who !notice, who want everything /notice'd
        var.PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence
        var.STASISED = defaultdict(int)
        var.STASISED_ACCS = defaultdict(int)
        var.PING_IF_PREFS = {}
        var.PING_IF_PREFS_ACCS = {}
        var.PING_IF_NUMS = defaultdict(set)
        var.PING_IF_NUMS_ACCS = defaultdict(set)
        var.DEADCHAT_PREFS = set()
        var.DEADCHAT_PREFS_ACCS = set()
        var.FLAGS = defaultdict(str)
        var.FLAGS_ACCS = defaultdict(str)
        var.DENY = defaultdict(set)
        var.DENY_ACCS = defaultdict(set)

        for acc, host, notice, simple, dc, pi, stasis, stasisexp, flags in c:
            if acc is not None:
                acc = irc_lower(acc)
                if simple == 1:
                    var.SIMPLE_NOTIFY_ACCS.add(acc)
                if notice == 1:
                    var.PREFER_NOTICE_ACCS.add(acc)
                if stasis > 0:
                    var.STASISED_ACCS[acc] = stasis
                if pi is not None and pi > 0:
                    var.PING_IF_PREFS_ACCS[acc] = pi
                    var.PING_IF_NUMS_ACCS[pi].add(acc)
                if dc == 1:
                    var.DEADCHAT_PREFS_ACCS.add(acc)
                if flags:
                    var.FLAGS_ACCS[acc] = flags
            elif host is not None:
                # nick!ident lowercased per irc conventions, host uses normal casing
                try:
                    hl, hr = host.split("@", 1)
                    host = irc_lower(hl) + "@" + hr.lower()
                except ValueError:
                    host = host.lower()
                if simple == 1:
                    var.SIMPLE_NOTIFY.add(host)
                if notice == 1:
                    var.PREFER_NOTICE.add(host)
                if stasis > 0:
                    var.STASISED[host] = stasis
                if pi is not None and pi > 0:
                    var.PING_IF_PREFS[host] = pi
                    var.PING_IF_NUMS[pi].add(host)
                if dc == 1:
                    var.DEADCHAT_PREFS.add(host)
                if flags:
                    var.FLAGS[host] = flags

        c.execute("""SELECT
                       pl.account,
                       pl.hostmask,
                       ws.data
                     FROM warning w
                     JOIN warning_sanction ws
                       ON ws.warning = w.id
                     JOIN person pe
                       ON pe.id = w.target
                     JOIN player pl
                       ON pl.person = pe.id
                     WHERE
                       ws.sanction = 'deny command'
                       AND w.deleted = 0
                       AND (
                         w.expires IS NULL
                         OR w.expires > datetime('now')
                       )""")
        for acc, host, command in c:
            if acc is not None:
                acc = irc_lower(acc)
                var.DENY_ACCS[acc].add(command)
            if host is not None:
                host = irc_lower(host)
                var.DENY[host].add(command)

def decrement_stasis(acc=None, hostmask=None):
    peid, plid = _get_ids(acc, hostmask)
    if (acc is not None or hostmask is not None) and peid is None:
        return
    sql = "UPDATE person SET stasis_amount = MAX(0, stasis_amount - 1)"
    params = ()
    if peid is not None:
        sql += " WHERE id = ?"
        params = (peid,)

    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute(sql, params)

def set_stasis(newamt, acc=None, hostmask=None, relative=False):
    peid, plid = _get_ids(acc, hostmask, add=True)
    _set_stasis(int(newamt), peid, relative)

def _set_stasis(newamt, peid, relative=False):
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("SELECT stasis_amount, stasis_expires FROM person WHERE id = ?", (peid,))
        oldamt, expiry = c.fetchone()
        if relative:
            newamt = oldamt + newamt
        if newamt < 0:
            newamt = 0
        if newamt > oldamt:
            delta = newamt - oldamt
            # increasing stasis, so need to update expiry
            c.execute("""UPDATE person
                         SET
                           stasis_amount = ?,
                           stasis_expires = datetime(CASE WHEN stasis_expires IS NULL
                                                            OR stasis_expires <= datetime('now')
                                                          THEN 'now'
                                                          ELSE stasis_expires END,
                                                     '+{0} hours')
                         WHERE id = ?""".format(int(delta)), (newamt, peid))
        else:
            # decreasing stasis, don't touch expiry
            c.execute("""UPDATE person
                         SET stasis_amount = ?
                         WHERE id = ?""", (newamt, peid))

def expire_stasis():
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("""UPDATE person
                     SET
                       stasis_amount = 0,
                       stasis_expires = NULL
                     WHERE
                       stasis_expires IS NOT NULL
                       AND stasis_expires <= datetime('now')""")

def get_template(name):
    conn = _conn()
    c = conn.cursor()
    c.execute("SELECT id, flags FROM access_template WHERE name = ?", (name,))
    row = c.fetchone()
    if row is None:
        return (None, set())
    return (row[0], row[1])

def get_templates():
    conn = _conn()
    c = conn.cursor()
    c.execute("SELECT name, flags FROM access_template ORDER BY name ASC")
    tpls = []
    for name, flags in c:
        tpls.append((name, flags))
    return tpls

def update_template(name, flags):
    conn = _conn()
    with conn:
        tid, _ = get_template(name)
        c = conn.cursor()
        if tid is None:
            c.execute("INSERT INTO access_template (name, flags) VALUES (?, ?)", (name, flags))
        else:
            c.execute("UPDATE access_template SET flags = ? WHERE id = ?", (flags, tid))

def delete_template(name):
    conn = _conn()
    with conn:
        tid, _ = get_template(name)
        if tid is not None:
            c = conn.cursor()
            c.execute("DELETE FROM access WHERE template = ?", (tid,))
            c.execute("DELETE FROM template WHERE id = ?", (tid,))

def set_access(acc, hostmask, flags=None, tid=None):
    peid, plid = _get_ids(acc, hostmask)
    if peid is None:
        return
    conn = _conn()
    with conn:
        c = conn.cursor()
        if flags is None and tid is None:
            c.execute("DELETE FROM access WHERE person = ?", (peid,))
        elif tid is not None:
            c.execute("""INSERT OR REPLACE INTO access
                         (person, template, flags)
                         VALUES (?, ?, NULL)""", (peid, tid))
        else:
            c.execute("""INSERT OR REPLACE INTO access
                         (person, template, flags)
                         VALUES (?, NULL, ?)""", (peid, flags))

def toggle_simple(acc, hostmask):
    _toggle_thing("simple", acc, hostmask)

def toggle_notice(acc, hostmask):
    _toggle_thing("notice", acc, hostmask)

def toggle_deadchat(acc, hostmask):
    _toggle_thing("deadchat", acc, hostmask)

def set_pingif(val, acc, hostmask):
    _set_thing("pingif", val, acc, hostmask, raw=False)

def add_game(mode, size, started, finished, winner, players, options):
    """ Adds a game record to the database.

    mode: Game mode (string)
    size: Game size on start (int)
    started: Time when game started (timestamp)
    finished: Time when game ended (timestamp)
    winner: Winning team (string)
    players: List of players (sequence of dict, described below)
    options: Game options (role reveal, stats type, etc., freeform dict)

    Players dict format:
    {
        nick: "Nickname"
        account: "Account name" (or None, "*" is converted to None)
        ident: "Ident"
        host: "Host"
        role: "role name"
        templates: ["template names", ...]
        special: ["special qualities", ... (lover, entranced, etc.)]
        won: True/False
        iwon: True/False
        dced: True/False
    }
    """

    if mode == "roles":
        # Do not record stats for games with custom roles
        return

    # Normalize players dict
    conn = _conn()
    for p in players:
        if p["account"] == "*":
            p["account"] = None
        p["hostmask"] = "{0}!{1}@{2}".format(p["nick"], p["ident"], p["host"])
        c = conn.cursor()
        p["personid"], p["playerid"] = _get_ids(p["account"], p["hostmask"], add=True)
    with conn:
        c = conn.cursor()
        c.execute("""INSERT INTO game (gamemode, options, started, finished, gamesize, winner)
                     VALUES (?, ?, ?, ?, ?, ?)""", (mode, json.dumps(options), started, finished, size, winner))
        gameid = c.lastrowid
        for p in players:
            c.execute("""INSERT INTO game_player (game, player, team_win, indiv_win, dced)
                         VALUES (?, ?, ?, ?, ?)""", (gameid, p["playerid"], p["won"], p["iwon"], p["dced"]))
            gpid = c.lastrowid
            c.execute("""INSERT INTO game_player_role (game_player, role, special)
                         VALUES (?, ?, 0)""", (gpid, p["role"]))
            for tpl in p["templates"]:
                c.execute("""INSERT INTO game_player_role (game_player, role, special)
                             VALUES (?, ?, 0)""", (gpid, tpl))
            for sq in p["special"]:
                c.execute("""INSERT INTO game_player_role (game_player, role, special)
                             VALUES (?, ?, 1)""", (gpid, sq))

def get_player_stats(acc, hostmask, role):
    peid, plid = _get_ids(acc, hostmask)
    if not _total_games(peid):
        return "\u0002{0}\u0002 has not played any games.".format(acc if acc and acc != "*" else hostmask)
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT
                   gpr.role AS role,
                   SUM(gp.team_win) AS team,
                   SUM(gp.indiv_win) AS indiv,
                   SUM(gp.team_win OR gp.indiv_win) AS overall,
                   COUNT(1) AS total
                 FROM person pe
                 JOIN player pl
                   ON pl.person = pe.id
                 JOIN game_player gp
                   ON gp.player = pl.id
                 JOIN game_player_role gpr
                   ON gpr.game_player = gp.id
                   AND gpr.role = ?
                 WHERE pe.id = ?
                 GROUP BY role""", (role, peid))
    row = c.fetchone()
    name = _get_display_name(peid)
    if row:
        return ("\u0002{0}\u0002 as \u0002{1[0]}\u0002 | Team wins: {1[1]} ({2:.0%}), "
                "Individual wins: {1[2]} ({3:.0%}), Overall wins: {1[3]} ({4:.0%}), Total games: {1[4]}.").format(name, row, row[1]/row[4], row[2]/row[4], row[3]/row[4])
    return "No stats for \u0002{0}\u0002 as \u0002{1}\u0002.".format(name, role)

def get_player_totals(acc, hostmask):
    peid, plid = _get_ids(acc, hostmask)
    total_games = _total_games(peid)
    if not total_games:
        return "\u0002{0}\u0002 has not played any games.".format(acc if acc and acc != "*" else hostmask)
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT
                   gpr.role AS role,
                   COUNT(1) AS total
                 FROM person pe
                 JOIN player pl
                   ON pl.person = pe.id
                 JOIN game_player gp
                   ON gp.player = pl.id
                 JOIN game_player_role gpr
                   ON gpr.game_player = gp.id
                 WHERE pe.id = ?
                 GROUP BY role""", (peid,))
    tmp = {}
    totals = []
    for row in c:
        tmp[row[0]] = row[1]
    order = role_order()
    name = _get_display_name(peid)
    #ordered role stats
    totals = ["\u0002{0}\u0002: {1}".format(r, tmp[r]) for r in order if r in tmp]
    #lover or any other special stats
    totals += ["\u0002{0}\u0002: {1}".format(r, t) for r, t in tmp.items() if r not in order]
    return "\u0002{0}\u0002's totals | \u0002{1}\u0002 games | {2}".format(name, total_games, break_long_message(totals, ", "))

def get_game_stats(mode, size):
    conn = _conn()
    c = conn.cursor()
    c.execute("SELECT COUNT(1) FROM game WHERE gamemode = ? AND gamesize = ?", (mode, size))
    total_games = c.fetchone()[0]
    if not total_games:
        return "No stats for \u0002{0}\u0002 player games.".format(size)
    c.execute("""SELECT
                   winner AS team,
                   COUNT(1) AS games,
                   CASE winner
                     WHEN 'villagers' THEN 0
                     WHEN 'wolves' THEN 1
                     ELSE 2 END AS ord
                 FROM game
                 WHERE
                   gamemode = ?
                   AND gamesize = ?
                   AND winner IS NOT NULL
                 GROUP BY team
                 ORDER BY ord ASC, team ASC""", (mode, size))
    msg = "\u0002{0}\u0002 player games | {1}"
    bits = []
    for row in c:
        bits.append("%s wins: %d (%d%%)" % (singular(row[0]), row[1], round(row[1]/total_games * 100)))
    bits.append("total games: {0}".format(total_games))
    return msg.format(size, ", ".join(bits))

def get_game_totals(mode):
    conn = _conn()
    c = conn.cursor()
    c.execute("SELECT COUNT(1) FROM game WHERE gamemode = ?", (mode,))
    total_games = c.fetchone()[0]
    if not total_games:
        return "No games have been played in the {0} game mode.".format(mode)
    c.execute("""SELECT
                   gamesize,
                   COUNT(1) AS games
                 FROM game
                 WHERE gamemode = ?
                 GROUP BY gamesize
                 ORDER BY gamesize ASC""", (mode,))
    totals = []
    for row in c:
        totals.append("\u0002{0}p\u0002: {1}".format(*row))
    return "Total games ({0}) | {1}".format(total_games, ", ".join(totals))

def get_warning_points(acc, hostmask):
    peid, plid = _get_ids(acc, hostmask)
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT COALESCE(SUM(amount), 0)
                 FROM warning
                 WHERE
                   target = ?
                   AND deleted = 0
                   AND (
                     expires IS NULL
                     OR expires > datetime('now')
                   )""", (peid,))
    row = c.fetchone()
    return row[0]

def has_unacknowledged_warnings(acc, hostmask):
    peid, plid = _get_ids(acc, hostmask)
    if peid is None:
        return False
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT COALESCE(MIN(acknowledged), 1)
                 FROM warning
                 WHERE
                   target = ?
                   AND deleted = 0
                   AND (
                     expires IS NULL
                     OR expires > datetime('now')
                   )""", (peid,))
    row = c.fetchone()
    return not bool(row[0])

def list_all_warnings(list_all=False, skip=0, show=0):
    conn = _conn()
    c = conn.cursor()
    sql = """SELECT
               warning.id,
               COALESCE(plt.account, plt.hostmask) AS target,
               COALESCE(pls.account, pls.hostmask, ?) AS sender,
               warning.amount,
               warning.issued,
               warning.expires,
               CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now')
                    THEN 0 ELSE 1 END AS expired,
               CASE WHEN warning.deleted
                         OR (
                             warning.expires IS NOT NULL
                             AND warning.expires <= datetime('now')
                         )
                    THEN 1 ELSE warning.acknowledged END AS acknowledged,
               warning.deleted,
               warning.reason
             FROM warning
             JOIN person pet
               ON pet.id = warning.target
             JOIN player plt
               ON plt.id = pet.primary_player
             LEFT JOIN person pes
               ON pes.id = warning.sender
             LEFT JOIN player pls
               ON pls.id = pes.primary_player
             """
    if not list_all:
        sql += """WHERE
                    deleted = 0
                    AND (
                      expires IS NULL
                      OR expires > datetime('now')
                    )
                """
    sql += "ORDER BY warning.issued DESC\n"
    if show > 0:
        sql += "LIMIT {0} OFFSET {1}".format(show, skip)

    c.execute(sql, (botconfig.NICK,))
    warnings = []
    for row in c:
        warnings.append({"id": row[0],
                         "target": row[1],
                         "sender": row[2],
                         "amount": row[3],
                         "issued": row[4],
                         "expires": row[5],
                         "expired": row[6],
                         "ack": row[7],
                         "deleted": row[8],
                         "reason": row[9]})
    return warnings

def list_warnings(acc, hostmask, expired=False, deleted=False, skip=0, show=0):
    peid, plid = _get_ids(acc, hostmask)
    conn = _conn()
    c = conn.cursor()
    sql = """SELECT
               warning.id,
               COALESCE(plt.account, plt.hostmask) AS target,
               COALESCE(pls.account, pls.hostmask, ?) AS sender,
               warning.amount,
               warning.issued,
               warning.expires,
               CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now')
                    THEN 0 ELSE 1 END AS expired,
               CASE WHEN warning.deleted
                         OR (
                             warning.expires IS NOT NULL
                             AND warning.expires <= datetime('now')
                         )
                    THEN 1 ELSE warning.acknowledged END AS acknowledged,
               warning.deleted,
               warning.reason
             FROM warning
             JOIN person pet
               ON pet.id = warning.target
             JOIN player plt
               ON plt.id = pet.primary_player
             LEFT JOIN person pes
               ON pes.id = warning.sender
             LEFT JOIN player pls
               ON pls.id = pes.primary_player
             WHERE
               warning.target = ?
             """
    if not deleted:
        sql += " AND deleted = 0"
    if not expired:
        sql += """ AND (
                      expires IS NULL
                      OR expires > datetime('now')
                    )"""
    sql += " ORDER BY warning.issued DESC"
    if show > 0:
        sql += " LIMIT {0} OFFSET {1}".format(show, skip)

    c.execute(sql, (botconfig.NICK, peid))
    warnings = []
    for row in c:
        warnings.append({"id": row[0],
                         "target": row[1],
                         "sender": row[2],
                         "amount": row[3],
                         "issued": row[4],
                         "expires": row[5],
                         "expired": row[6],
                         "ack": row[7],
                         "deleted": row[8],
                         "reason": row[9]})
    return warnings

def get_warning(warn_id, acc=None, hm=None):
    peid, plid = _get_ids(acc, hm)
    conn = _conn()
    c = conn.cursor()
    sql = """SELECT
               warning.id,
               COALESCE(plt.account, plt.hostmask) AS target,
               COALESCE(pls.account, pls.hostmask, ?) AS sender,
               warning.amount,
               warning.issued,
               warning.expires,
               CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now')
                    THEN 0 ELSE 1 END AS expired,
               warning.acknowledged,
               warning.deleted,
               warning.reason,
               warning.notes,
               COALESCE(pld.account, pld.hostmask) AS deleted_by,
               warning.deleted_on
             FROM warning
             JOIN person pet
               ON pet.id = warning.target
             JOIN player plt
               ON plt.id = pet.primary_player
             LEFT JOIN person pes
               ON pes.id = warning.sender
             LEFT JOIN player pls
               ON pls.id = pes.primary_player
             LEFT JOIN person ped
               ON ped.id = warning.deleted_by
             LEFT JOIN player pld
               ON pld.id = ped.primary_player
             WHERE
               warning.id = ?
             """
    params = (botconfig.NICK, warn_id)
    if acc is not None and hm is not None:
        sql += """  AND warning.target = ?
                    AND warning.deleted = 0"""
        params = (botconfig.NICK, warn_id, peid)

    c.execute(sql, params)
    row = c.fetchone()
    if not row:
        return None

    return {"id": row[0],
            "target": row[1],
            "sender": row[2],
            "amount": row[3],
            "issued": row[4],
            "expires": row[5],
            "expired": row[6],
            "ack": row[7],
            "deleted": row[8],
            "reason": row[9],
            "notes": row[10],
            "deleted_by": row[11],
            "deleted_on": row[12],
            "sanctions": get_warning_sanctions(warn_id)}

def get_warning_sanctions(warn_id):
    conn = _conn()
    c = conn.cursor()
    c.execute("SELECT sanction, data FROM warning_sanction WHERE warning=?", (warn_id,))
    sanctions = {}
    for sanc, data in c:
        if sanc == "stasis":
            sanctions["stasis"] = int(data)
        elif sanc == "deny command":
            if "deny" not in sanctions:
                sanctions["deny"] = set()
            sanctions["deny"].add(data)

    return sanctions

def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires):
    teid, tlid = _get_ids(tacc, thm, add=True)
    seid, slid = _get_ids(sacc, shm)
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("""INSERT INTO warning
                     (
                     target, sender, amount,
                     issued, expires,
                     reason, notes,
                     acknowledged
                     )
                     VALUES
                     (
                       ?, ?, ?,
                       datetime('now'), ?,
                       ?, ?,
                       0
                     )""", (teid, seid, amount, expires, reason, notes))
    return c.lastrowid

def add_warning_sanction(warning, sanction, data):
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("""INSERT INTO warning_sanction
                     (warning, sanction, data)
                     VALUES
                     (?, ?, ?)""", (warning, sanction, data))

        if sanction == "stasis":
            c.execute("SELECT target FROM warning WHERE id = ?", (warning,))
            peid = c.fetchone()[0]
            _set_stasis(int(data), peid, relative=True)
        elif sanction == "tempban":
            # we want to return a list of all banned accounts/hostmasks
            idlist = set()
            acclist = set()
            hmlist = set()
            c.execute("SELECT target FROM warning WHERE id = ?", (warning,))
            peid = c.fetchone()[0]
            c.execute("SELECT id, account, hostmask FROM player WHERE person = ? AND active = 1", (peid,))
            if isinstance(data, datetime):
                sql = "INSERT INTO bantrack (player, expires) values (?, ?)"
            else:
                sql = "INSERT INTO bantrack (player, warning_amount) values (?, ?)"
            for row in c:
                idlist.add(row[0])
                if row[1] is None:
                    hmlist.add(row[2])
                else:
                    acclist.add(row[1])
            for plid in idlist:
                c.execute(sql, (plid, data))
            return (acclist, hmlist)

def del_warning(warning, acc, hm):
    peid, plid = _get_ids(acc, hm)
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("""UPDATE warning
                     SET
                       acknowledged = 1,
                       deleted = 1,
                       deleted_on = datetime('now'),
                       deleted_by = ?
                     WHERE
                       id = ?
                       AND deleted = 0""", (peid, warning))

def set_warning(warning, expires, reason, notes):
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("""UPDATE warning
                     SET reason = ?, notes = ?, expires = ?
                     WHERE id = ?""", (reason, notes, expires, warning))

def acknowledge_warning(warning):
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,))

def expire_tempbans():
    conn = _conn()
    with conn:
        idlist = set()
        acclist = set()
        hmlist = set()
        c = conn.cursor()
        c.execute("""SELECT
                       bt.player,
                       pl.account,
                       pl.hostmask
                     FROM bantrack bt
                     JOIN player pl
                       ON pl.id = bt.player
                     WHERE
                       (bt.expires IS NOT NULL AND bt.expires < datetime('now'))
                       OR (
                         warning_amount IS NOT NULL
                         AND warning_amount <= (
                           SELECT COALESCE(SUM(amount), 0)
                           FROM warning
                           WHERE
                             target = pl.person
                             AND deleted = 0
                             AND (
                               expires IS NULL
                               OR expires > datetime('now')
                             )
                         )
                       )""")
        for row in c:
            idlist.add(row[0])
            if row[1] is None:
                hmlist.add(row[2])
            else:
                acclist.add(row[1])
        for plid in idlist:
            c.execute("DELETE FROM bantrack WHERE player = ?", (plid,))
        return (acclist, hmlist)

def get_pre_restart_state():
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("SELECT players FROM pre_restart_state")
        players = c.fetchone()
        if players is None:
            # missing state row
            c.execute("INSERT INTO pre_restart_state (players) VALUES (NULL)")
            players = []
        else:
            c.execute("UPDATE pre_restart_state SET players=NULL")
            players = players[0]
            if players is not None:
                players = players.split()
    return players

def set_pre_restart_state(players):
    if not players:
        return
    conn = _conn()
    with conn:
        c = conn.cursor()
        c.execute("UPDATE pre_restart_state SET players = ?", (" ".join(players),))

def _upgrade(oldversion):
    # try to make a backup copy of the database
    print ("Performing schema upgrades, this may take a while.", file=sys.stderr)
    have_backup = False
    try:
        print ("Creating database backup...", file=sys.stderr)
        shutil.copyfile("data.sqlite3", "data.sqlite3.bak")
        have_backup = True
        print ("Database backup created at data.sqlite3.bak...", file=sys.stderr)
    except OSError:
        print ("Database backup failed! Hit Ctrl+C to abort, otherwise upgrade will continue in 5 seconds...", file=sys.stderr)
        time.sleep(5)

    dn = os.path.dirname(__file__)
    conn = _conn()
    try:
        with conn:
            c = conn.cursor()
            if oldversion < 2:
                print ("Upgrade from version 1 to 2...", file=sys.stderr)
                # Update FKs to be deferrable, update collations to nocase where it makes sense,
                # and clean up how fool wins are tracked (giving fools team wins instead of saving the winner's
                # player id as a string). When nocasing players, this may cause some records to be merged.
                with open(os.path.join(dn, "db", "upgrade2.sql"), "rt") as f:
                    c.executescript(f.read())
            if oldversion < 3:
                print ("Upgrade from version 2 to 3...", file=sys.stderr)
                with open(os.path.join(dn, "db", "upgrade3.sql"), "rt") as f:
                    c.executescript(f.read())

            c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))
            print ("Upgrades complete!", file=sys.stderr)
    except sqlite3.Error:
        print ("An error has occurred while upgrading the database schema.",
               "Please report this issue to ##werewolf-dev on irc.freenode.net.",
               "Include all of the following details in your report:",
               sep="\n", file=sys.stderr)
        if have_backup:
            try:
                shutil.copyfile("data.sqlite3.bak", "data.sqlite3")
            except OSError:
                print ("An error has occurred while restoring your database backup.",
                       "You can manually move data.sqlite3.bak to data.sqlite3 to restore the original database.",
                       sep="\n", file=sys.stderr)
        raise

def _migrate():
    # try to make a backup copy of the database
    import shutil
    try:
        shutil.copyfile("data.sqlite3", "data.sqlite3.bak")
    except OSError:
        pass
    dn = os.path.dirname(__file__)
    conn = _conn()
    with conn, open(os.path.join(dn, "db", "db.sql"), "rt") as f1, open(os.path.join(dn, "db", "migrate.sql"), "rt") as f2:
        c = conn.cursor()
        #######################################################
        # Step 1: install the new schema (from db.sql script) #
        #######################################################
        c.executescript(f1.read())

        ################################################################
        # Step 2: migrate relevant info from the old schema to the new #
        ################################################################
        c.executescript(f2.read())

        ######################################################################
        # Step 3: Indicate we have updated the schema to the current version #
        ######################################################################
        c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))

def _install():
    dn = os.path.dirname(__file__)
    conn = _conn()
    with conn, open(os.path.join(dn, "db", "db.sql"), "rt") as f1:
        c = conn.cursor()
        c.executescript(f1.read())
        c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))

def _get_ids(acc, hostmask, add=False):
    conn = _conn()
    c = conn.cursor()
    if acc == "*":
        acc = None
    if acc is None and hostmask is None:
        return (None, None)
    elif acc is None:
        c.execute("""SELECT pe.id, pl.id
                     FROM player pl
                     JOIN person pe
                       ON pe.id = pl.person
                     WHERE
                       pl.account IS NULL
                       AND pl.hostmask = ?
                       AND pl.active = 1""", (hostmask,))
    else:
        hostmask = None
        c.execute("""SELECT pe.id, pl.id
                     FROM player pl
                     JOIN person pe
                       ON pe.id = pl.person
                     WHERE
                       pl.account = ?
                       AND pl.hostmask IS NULL
                       AND pl.active = 1""", (acc,))
    row = c.fetchone()
    peid = None
    plid = None
    if row:
        peid, plid = row
    elif add:
        with conn:
            c.execute("INSERT INTO player (account, hostmask) VALUES (?, ?)", (acc, hostmask))
            plid = c.lastrowid
            c.execute("INSERT INTO person (primary_player) VALUES (?)", (plid,))
            peid = c.lastrowid
            c.execute("UPDATE player SET person=? WHERE id=?", (peid, plid))
    return (peid, plid)

def _get_display_name(peid):
    if peid is None:
        return None
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT COALESCE(pp.account, pp.hostmask)
                 FROM person pe
                 JOIN player pp
                   ON pp.id = pe.primary_player
                 WHERE pe.id = ?""", (peid,))
    return c.fetchone()[0]

def _total_games(peid):
    if peid is None:
        return 0
    conn = _conn()
    c = conn.cursor()
    c.execute("""SELECT COUNT(DISTINCT gp.game)
                 FROM person pe
                 JOIN player pl
                   ON pl.person = pe.id
                 JOIN game_player gp
                   ON gp.player = pl.id
                 WHERE
                   pe.id = ?""", (peid,))
    # aggregates without GROUP BY always have exactly one row,
    # so no need to check for None here
    return c.fetchone()[0]

def _set_thing(thing, val, acc, hostmask, raw=False):
    conn = _conn()
    with conn:
        c = conn.cursor()
        peid, plid = _get_ids(acc, hostmask, add=True)
        if raw:
            params = (peid,)
        else:
            params = (val, peid)
            val = "?"
        c.execute("""UPDATE person SET {0} = {1} WHERE id = ?""".format(thing, val), params)

def _toggle_thing(thing, acc, hostmask):
    _set_thing(thing, "CASE {0} WHEN 1 THEN 0 ELSE 1 END".format(thing), acc, hostmask, raw=True)

def _conn():
    try:
        return _ts.conn
    except AttributeError:
        _ts.conn = sqlite3.connect("data.sqlite3")
        with _ts.conn:
            c = _ts.conn.cursor()
            c.execute("PRAGMA foreign_keys = ON")
        # remap NOCASE to be IRC casing
        _ts.conn.create_collation("NOCASE", _collate_irc)
        return _ts.conn

def _collate_irc(s1, s2):
    # treat hostmasks specially, otherwise call irc_lower on stuff
    if "@" in s1:
        hl, hr = s1.split("@", 1)
        s1 = irc_lower(hl) + "@" + hr.lower()
    else:
        s1 = irc_lower(s1)

    if "@" in s2:
        hl, hr = s2.split("@", 1)
        l2 = irc_lower(hl) + "@" + hr.lower()
    else:
        s2 = irc_lower(s2)

    if s1 == s2:
        return 0
    elif s1 < s2:
        return -1
    else:
        return 1

need_install = not os.path.isfile("data.sqlite3")
conn = sqlite3.connect("data.sqlite3")
with conn:
    c = conn.cursor()
    c.execute("PRAGMA foreign_keys = ON")
    if need_install:
        _install()
    c.execute("PRAGMA user_version")
    row = c.fetchone()
    ver = row[0]
    c.close()

if ver == 0:
    # new schema does not exist yet, migrate from old schema
    # NOTE: game stats are NOT migrated to the new schema; the old gamestats table
    # will continue to exist to allow queries against it, however given how horribly
    # inaccurate the stats on it are, it would be a disservice to copy those inaccurate
    # statistics over to the new schema which has the capability of actually being accurate.
    _migrate()
elif ver < SCHEMA_VERSION:
    _upgrade(ver)

del need_install, conn, c, ver

# vim: set expandtab:sw=4:ts=4:
