import random
from typing import Dict, Tuple, Any, Optional
from flask import Blueprint, request, Response, render_template, url_for

from bemani.backend.base import Base
from bemani.common import CardCipher, CardCipherException, GameConstants, RegionConstants, ValidatedDict
from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client
from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException
from bemani.frontend.app import adminrequired, jsonify, valid_email, valid_username, valid_pin, render_react
from bemani.frontend.gamesettings import get_game_settings
from bemani.frontend.iidx.iidx import IIDXFrontend
from bemani.frontend.jubeat.jubeat import JubeatFrontend
from bemani.frontend.popn.popn import PopnMusicFrontend
from bemani.frontend.templates import templates_location
from bemani.frontend.static import static_location
from bemani.frontend.types import g

admin_pages = Blueprint(
    'admin_pages',
    __name__,
    url_prefix='/admin',
    template_folder=templates_location,
    static_folder=static_location,
)


def format_arcade(arcade: Arcade) -> Dict[str, Any]:
    owners = []
    for owner in arcade.owners:
        user = g.data.local.user.get_user(owner)
        if user is not None:
            owners.append(user.username)
    return {
        'id': arcade.id,
        'name': arcade.name,
        'description': arcade.description,
        'region': arcade.region,
        'paseli_enabled': arcade.data.get_bool('paseli_enabled'),
        'paseli_infinite': arcade.data.get_bool('paseli_infinite'),
        'mask_services_url': arcade.data.get_bool('mask_services_url'),
        'owners': owners,
    }


def format_machine(machine: Machine) -> Dict[str, Any]:
    return {
        'id': machine.id,
        'pcbid': machine.pcbid,
        'name': machine.name,
        'description': machine.description,
        'arcade': machine.arcade,
        'port': machine.port,
        'game': machine.game.value if machine.game else 'any',
        'version': machine.version,
    }


def format_card(card: Tuple[str, Optional[UserID]]) -> Dict[str, Any]:
    owner = None
    if card[1] is not None:
        user = g.data.local.user.get_user(card[1])
        if user is not None:
            owner = user.username
    try:
        return {
            'number': CardCipher.encode(card[0]),
            'owner': owner,
            'id': card[1],
        }
    except CardCipherException:
        return {
            'number': '????????????????',
            'owner': owner,
            'id': card[1],
        }


def format_user(user: User) -> Dict[str, Any]:
    return {
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'admin': user.admin,
    }


def format_news(news: News) -> Dict[str, Any]:
    return {
        'id': news.id,
        'timestamp': news.timestamp,
        'title': news.title,
        'body': news.body,
    }


def format_event(event: Event) -> Dict[str, Any]:
    return {
        'id': event.id,
        'timestamp': event.timestamp,
        'userid': event.userid,
        'arcadeid': event.arcadeid,
        'type': event.type,
        'data': event.data,
    }


def format_client(client: Client) -> Dict[str, Any]:
    return {
        'id': client.id,
        'name': client.name,
        'token': client.token,
    }


def format_server(server: Server) -> Dict[str, Any]:
    return {
        'id': server.id,
        'uri': server.uri,
        'token': server.token,
        'allow_stats': server.allow_stats,
        'allow_scores': server.allow_scores,
    }


@admin_pages.route('/')
@adminrequired
def viewsettings() -> Response:
    return Response(render_template(
        'admin/settings.html',
        **{
            'title': 'Network Settings',
            'config': g.config,
            'region': RegionConstants.LUT,
        },
    ))


@admin_pages.route('/events')
@adminrequired
def viewevents() -> Response:
    iidx = IIDXFrontend(g.data, g.config, g.cache)
    jubeat = JubeatFrontend(g.data, g.config, g.cache)
    pnm = PopnMusicFrontend(g.data, g.config, g.cache)
    return render_react(
        'Events',
        'admin/events.react.js',
        {
            'events': [format_event(event) for event in g.data.local.network.get_events(limit=100)],
            'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
            'arcades': {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()},
            'iidxsongs': iidx.get_all_songs(),
            'jubeatsongs': jubeat.get_all_songs(),
            'pnmsongs': pnm.get_all_songs(),
            'iidxversions': {version: name for (game, version, name) in iidx.all_games()},
            'jubeatversions': {version: name for (game, version, name) in jubeat.all_games()},
            'pnmversions': {version: name for (game, version, name) in pnm.all_games()},
        },
        {
            'refresh': url_for('admin_pages.listevents', since=-1),
            'backfill': url_for('admin_pages.backfillevents', until=-1),
            'viewuser': url_for('admin_pages.viewuser', userid=-1),
            'jubeatsong': url_for('jubeat_pages.viewtopscores', musicid=-1) if GameConstants.JUBEAT in g.config.support else None,
            'iidxsong': url_for('iidx_pages.viewtopscores', musicid=-1) if GameConstants.IIDX in g.config.support else None,
            'pnmsong': url_for('popn_pages.viewtopscores', musicid=-1) if GameConstants.POPN_MUSIC in g.config.support else None,
        },
    )


@admin_pages.route('/events/backfill/<int:until>')
@jsonify
@adminrequired
def backfillevents(until: int) -> Dict[str, Any]:
    return {
        'events': [format_event(event) for event in g.data.local.network.get_events(until_id=until, limit=1000)],
    }


@admin_pages.route('/events/list/<int:since>')
@jsonify
@adminrequired
def listevents(since: int) -> Dict[str, Any]:
    return {
        'events': [format_event(event) for event in g.data.local.network.get_events(since_id=since)],
        'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
        'arcades': {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()},
    }


@admin_pages.route('/api')
@adminrequired
def viewapi() -> Response:
    return render_react(
        'Data API',
        'admin/api.react.js',
        {
            'clients': [format_client(client) for client in g.data.local.api.get_all_clients()],
            'servers': [format_server(server) for server in g.data.local.api.get_all_servers()],
        },
        {
            'addclient': url_for('admin_pages.addclient'),
            'updateclient': url_for('admin_pages.updateclient'),
            'removeclient': url_for('admin_pages.removeclient'),
            'addserver': url_for('admin_pages.addserver'),
            'updateserver': url_for('admin_pages.updateserver'),
            'removeserver': url_for('admin_pages.removeserver'),
            'queryserver': url_for('admin_pages.queryserver', serverid=-1),
        },
    )


@admin_pages.route('/arcades')
@adminrequired
def viewarcades() -> Response:
    return render_react(
        'Arcades',
        'admin/arcades.react.js',
        {
            'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
            'regions': RegionConstants.LUT,
            'usernames': g.data.local.user.get_all_usernames(),
            'paseli_enabled': g.config.paseli.enabled,
            'paseli_infinite': g.config.paseli.infinite,
            'default_region': g.config.server.region,
            'mask_services_url': False,
        },
        {
            'addarcade': url_for('admin_pages.addarcade'),
            'updatearcade': url_for('admin_pages.updatearcade'),
            'removearcade': url_for('admin_pages.removearcade'),
        },
    )


@admin_pages.route('/pcbids')
@adminrequired
def viewmachines() -> Response:
    games: Dict[str, Dict[int, str]] = {}
    for (game, version, name) in Base.all_games():
        if game.value not in games:
            games[game.value] = {}
        games[game.value][version] = name

    return render_react(
        'Machines',
        'admin/machines.react.js',
        {
            'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
            'arcades': {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()},
            'series': {
                GameConstants.BISHI_BASHI.value: 'BishiBashi',
                GameConstants.DDR.value: 'DDR',
                GameConstants.IIDX.value: 'IIDX',
                GameConstants.JUBEAT.value: 'Jubeat',
                GameConstants.MGA.value: 'Metal Gear Arcade',
                GameConstants.MUSECA.value: 'MÚSECA',
                GameConstants.POPN_MUSIC.value: 'Pop\'n Music',
                GameConstants.REFLEC_BEAT.value: 'Reflec Beat',
                GameConstants.SDVX.value: 'SDVX',
            },
            'games': games,
            'enforcing': g.config.server.enforce_pcbid,
        },
        {
            'refresh': url_for('admin_pages.listmachines'),
            'generatepcbid': url_for('admin_pages.generatepcbid'),
            'addpcbid': url_for('admin_pages.addpcbid'),
            'updatepcbid': url_for('admin_pages.updatepcbid'),
            'removepcbid': url_for('admin_pages.removepcbid'),
        },
    )


@admin_pages.route('/cards')
@adminrequired
def viewcards() -> Response:
    return render_react(
        'Cards',
        'admin/cards.react.js',
        {
            'cards': [format_card(card) for card in g.data.local.user.get_all_cards()],
            'usernames': g.data.local.user.get_all_usernames(),
        },
        {
            'addcard': url_for('admin_pages.addcard'),
            'removecard': url_for('admin_pages.removecard'),
            'viewuser': url_for('admin_pages.viewuser', userid=-1),
        },
    )


@admin_pages.route('/users')
@adminrequired
def viewusers() -> Response:
    return render_react(
        'Users',
        'admin/users.react.js',
        {
            'users': [format_user(user) for user in g.data.local.user.get_all_users()],
        },
        {
            'searchusers': url_for('admin_pages.searchusers'),
            'viewuser': url_for('admin_pages.viewuser', userid=-1),
        },
    )


@admin_pages.route('/news')
@adminrequired
def viewnews() -> Response:
    return render_react(
        'News',
        'admin/news.react.js',
        {
            'news': [format_news(news) for news in g.data.local.network.get_all_news()],
        },
        {
            'removenews': url_for('admin_pages.removenews'),
            'addnews': url_for('admin_pages.addnews'),
            'updatenews': url_for('admin_pages.updatenews'),
        },
    )


@admin_pages.route('/gamesettings')
@adminrequired
def viewgamesettings() -> Response:
    return render_react(
        'Game Settings',
        'admin/gamesettings.react.js',
        {
            'game_settings': get_game_settings(g.data, g.data.local.machine.DEFAULT_SETTINGS_ARCADE),
        },
        {
            'update_settings': url_for('admin_pages.updatesettings'),
        },
    )


@admin_pages.route('/users/<int:userid>')
@adminrequired
def viewuser(userid: int) -> Response:
    # Cast the userID.
    userid = UserID(userid)
    user = g.data.local.user.get_user(userid)

    def __format_card(card: str) -> str:
        try:
            return CardCipher.encode(card)
        except CardCipherException:
            return '????????????????'

    cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)]
    arcades = g.data.local.machine.get_all_arcades()
    return render_react(
        'User',
        'admin/user.react.js',
        {
            'user': {
                'email': user.email,
                'username': user.username,
            },
            'cards': cards,
            'arcades': {arcade.id: arcade.name for arcade in arcades},
            'balances': {arcade.id: g.data.local.user.get_balance(userid, arcade.id) for arcade in arcades},
            'events': [format_event(event) for event in g.data.local.network.get_events(userid=userid, event='paseli_transaction')],
        },
        {
            'refresh': url_for('admin_pages.listuser', userid=userid),
            'removeusercard': url_for('admin_pages.removeusercard', userid=userid),
            'addusercard': url_for('admin_pages.addusercard', userid=userid),
            'updatebalance': url_for('admin_pages.updatebalance', userid=userid),
            'updateusername': url_for('admin_pages.updateusername', userid=userid),
            'updateemail': url_for('admin_pages.updateemail', userid=userid),
            'updatepin': url_for('admin_pages.updatepin', userid=userid),
            'updatepassword': url_for('admin_pages.updatepassword', userid=userid),
        },
    )


@admin_pages.route('/users/<int:userid>/list')
@jsonify
@adminrequired
def listuser(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    def __format_card(card: str) -> str:
        try:
            return CardCipher.encode(card)
        except CardCipherException:
            return '????????????????'

    cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)]
    arcades = g.data.local.machine.get_all_arcades()
    return {
        'cards': cards,
        'arcades': {arcade.id: arcade.name for arcade in arcades},
        'balances': {arcade.id: g.data.local.user.get_balance(userid, arcade.id) for arcade in arcades},
        'events': [format_event(event) for event in g.data.local.network.get_events(userid=userid, event='paseli_transaction')],
    }


@admin_pages.route('/arcades/list')
@jsonify
@adminrequired
def listmachines() -> Dict[str, Any]:
    return {
        'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
        'arcades': {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()},
    }


@admin_pages.route('/arcades/update', methods=['POST'])
@jsonify
@adminrequired
def updatearcade() -> Dict[str, Any]:
    # Attempt to look this arcade up
    new_values = request.get_json()['arcade']
    arcade = g.data.local.machine.get_arcade(new_values['id'])
    if arcade is None:
        raise Exception('Unable to find arcade to update!')

    arcade.name = new_values['name']
    arcade.description = new_values['description']
    arcade.region = new_values['region']
    arcade.data.replace_bool('paseli_enabled', new_values['paseli_enabled'])
    arcade.data.replace_bool('paseli_infinite', new_values['paseli_infinite'])
    arcade.data.replace_bool('mask_services_url', new_values['mask_services_url'])
    owners = []
    for owner in new_values['owners']:
        ownerid = g.data.local.user.from_username(owner)
        if ownerid is not None:
            owners.append(ownerid)
    owners = list(set(owners))
    arcade.owners = owners
    g.data.local.machine.put_arcade(arcade)

    # Just return all arcades for ease of updating
    return {
        'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
    }


@admin_pages.route('/arcades/add', methods=['POST'])
@jsonify
@adminrequired
def addarcade() -> Dict[str, Any]:
    # Attempt to look this arcade up
    new_values = request.get_json()['arcade']

    if len(new_values['name']) == 0:
        raise Exception('Please name your new arcade!')
    if len(new_values['description']) == 0:
        raise Exception('Please describe your new arcade!')
    owners = []
    for owner in new_values['owners']:
        ownerid = g.data.local.user.from_username(owner)
        if ownerid is not None:
            owners.append(ownerid)
    owners = list(set(owners))

    g.data.local.machine.create_arcade(
        new_values['name'],
        new_values['description'],
        new_values['region'],
        {
            'paseli_enabled': new_values['paseli_enabled'],
            'paseli_infinite': new_values['paseli_infinite'],
            'mask_services_url': new_values['mask_services_url'],
        },
        owners,
    )

    # Just return all arcades for ease of updating
    return {
        'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
    }


@admin_pages.route('/arcades/remove', methods=['POST'])
@jsonify
@adminrequired
def removearcade() -> Dict[str, Any]:
    # Attempt to look this arcade up
    arcadeid = request.get_json()['arcadeid']
    arcade = g.data.local.machine.get_arcade(arcadeid)
    if arcade is None:
        raise Exception('Unable to find arcade to delete!')
    g.data.local.machine.destroy_arcade(arcadeid)

    # Just return all arcades for ease of updating
    return {
        'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
    }


@admin_pages.route('/clients/update', methods=['POST'])
@jsonify
@adminrequired
def updateclient() -> Dict[str, Any]:
    # Attempt to look this client up
    new_values = request.get_json()['client']
    client = g.data.local.api.get_client(new_values['id'])
    if client is None:
        raise Exception('Unable to find client to update!')

    if len(new_values['name']) == 0:
        raise Exception('Client names must be at least one character long!')

    client.name = new_values['name']
    g.data.local.api.put_client(client)

    # Just return all clients for ease of updating
    return {
        'clients': [format_client(client) for client in g.data.local.api.get_all_clients()],
    }


@admin_pages.route('/clients/add', methods=['POST'])
@jsonify
@adminrequired
def addclient() -> Dict[str, Any]:
    # Attempt to look this client up
    new_values = request.get_json()['client']

    if len(new_values['name']) == 0:
        raise Exception('Please name your new client!')

    g.data.local.api.create_client(
        new_values['name'],
    )

    # Just return all clientss for ease of updating
    return {
        'clients': [format_client(client) for client in g.data.local.api.get_all_clients()],
    }


@admin_pages.route('/clients/remove', methods=['POST'])
@jsonify
@adminrequired
def removeclient() -> Dict[str, Any]:
    # Attempt to look this client up
    clientid = request.get_json()['clientid']
    client = g.data.local.api.get_client(clientid)
    if client is None:
        raise Exception('Unable to find client to delete!')
    g.data.local.api.destroy_client(clientid)

    # Just return all clients for ease of updating
    return {
        'clients': [format_client(client) for client in g.data.local.api.get_all_clients()],
    }


@admin_pages.route('/server/<int:serverid>/info')
@jsonify
@adminrequired
def queryserver(serverid: int) -> Dict[str, Any]:
    # Attempt to look this server up
    server = g.data.local.api.get_server(serverid)
    if server is None:
        raise Exception('Unable to find server to query!')

    client = APIClient(server.uri, server.token, False, False)
    try:
        serverinfo = client.get_server_info()
        info = {
            'name': serverinfo['name'],
            'email': serverinfo['email'],
        }
        info['status'] = 'ok' if APIClient.API_VERSION in serverinfo['versions'] else 'badversion'
    except NotAuthorizedAPIException:
        info = {
            'name': 'unknown',
            'email': 'unknown',
            'status': 'badauth',
        }
    except APIException:
        info = {
            'name': 'unknown',
            'email': 'unknown',
            'status': 'error',
        }

    return info


@admin_pages.route('/servers/update', methods=['POST'])
@jsonify
@adminrequired
def updateserver() -> Dict[str, Any]:
    # Attempt to look this server up
    new_values = request.get_json()['server']
    server = g.data.local.api.get_server(new_values['id'])
    if server is None:
        raise Exception('Unable to find server to update!')

    if len(new_values['uri']) == 0:
        raise Exception('Please provide a valid connection URI for this server!')
    if len(new_values['token']) == 0 or len(new_values['token']) > 64:
        raise Exception('Please provide a valid connection token for this server!')

    server.uri = new_values['uri']
    server.token = new_values['token']
    server.allow_stats = new_values['allow_stats']
    server.allow_scores = new_values['allow_scores']
    g.data.local.api.put_server(server)

    # Just return all servers for ease of updating
    return {
        'servers': [format_server(server) for server in g.data.local.api.get_all_servers()],
    }


@admin_pages.route('/servers/add', methods=['POST'])
@jsonify
@adminrequired
def addserver() -> Dict[str, Any]:
    # Attempt to look this server up
    new_values = request.get_json()['server']

    if len(new_values['uri']) == 0:
        raise Exception('Please provide a connection URI for the new server!')
    if len(new_values['token']) == 0 or len(new_values['token']) > 64:
        raise Exception('Please provide a valid connection token for the new server!')

    g.data.local.api.create_server(
        new_values['uri'],
        new_values['token'],
    )

    # Just return all serverss for ease of updating
    return {
        'servers': [format_server(server) for server in g.data.local.api.get_all_servers()],
    }


@admin_pages.route('/servers/remove', methods=['POST'])
@jsonify
@adminrequired
def removeserver() -> Dict[str, Any]:
    # Attempt to look this server up
    serverid = request.get_json()['serverid']
    server = g.data.local.api.get_server(serverid)
    if server is None:
        raise Exception('Unable to find server to delete!')
    g.data.local.api.destroy_server(serverid)

    # Just return all servers for ease of updating
    return {
        'servers': [format_server(server) for server in g.data.local.api.get_all_servers()],
    }


@admin_pages.route('/pcbids/generate', methods=['POST'])
@jsonify
@adminrequired
def generatepcbid() -> Dict[str, Any]:
    # Attempt to look this arcade up
    new_pcbid = request.get_json()['machine']
    if new_pcbid['arcade'] is not None:
        arcade = g.data.local.machine.get_arcade(new_pcbid['arcade'])
        if arcade is None:
            raise Exception('Unable to find arcade to link PCBID to!')

    # Will be set by the game on boot.
    name: str = 'なし'
    pcbid: Optional[str] = None
    while pcbid is None:
        # Generate a new PCBID, check for uniqueness
        potential_pcbid = "01201000000000" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)])
        if g.data.local.machine.get_machine(potential_pcbid) is None:
            pcbid = potential_pcbid

    g.data.local.machine.create_machine(pcbid, name, new_pcbid['description'], new_pcbid['arcade'])

    # Just return all machines for ease of updating
    return {
        'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
    }


@admin_pages.route('/pcbids/add', methods=['POST'])
@jsonify
@adminrequired
def addpcbid() -> Dict[str, Any]:
    # Attempt to look this arcade up
    new_pcbid = request.get_json()['machine']
    if new_pcbid['arcade'] is not None:
        arcade = g.data.local.machine.get_arcade(new_pcbid['arcade'])
        if arcade is None:
            raise Exception('Unable to find arcade to link PCBID to!')

    # Verify that the PCBID is valid
    potential_pcbid = "".join([c for c in new_pcbid['pcbid'].upper() if c in "0123456789ABCDEF"])
    if len(potential_pcbid) != len(new_pcbid['pcbid']):
        raise Exception("Invalid characters in PCBID!")
    if len(potential_pcbid) != 20:
        raise Exception("PCBID has invalid length!")

    if g.data.local.machine.get_machine(potential_pcbid) is not None:
        raise Exception('PCBID already exists!')

    # Will be set by the game on boot.
    name = 'なし'
    g.data.local.machine.create_machine(potential_pcbid, name, new_pcbid['description'], new_pcbid['arcade'])

    # Just return all machines for ease of updating
    return {
        'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
    }


@admin_pages.route('/pcbids/update', methods=['POST'])
@jsonify
@adminrequired
def updatepcbid() -> Dict[str, Any]:
    # Attempt to look this machine up
    machine = request.get_json()['machine']
    if machine['arcade'] is not None:
        arcade = g.data.local.machine.get_arcade(machine['arcade'])
        if arcade is None:
            raise Exception('Unable to find arcade to link PCBID to!')

    # Make sure we don't duplicate port assignments
    other_pcbid = g.data.local.machine.from_port(machine['port'])
    if other_pcbid is not None and other_pcbid != machine['pcbid']:
        raise Exception(f'The specified port is already in use by \'{other_pcbid}\'!')

    if machine['port'] < 1 or machine['port'] > 65535:
        raise Exception('The specified port is out of range!')

    current_machine = g.data.local.machine.get_machine(machine['pcbid'])
    current_machine.description = machine['description']
    current_machine.arcade = machine['arcade']
    current_machine.port = machine['port']
    current_machine.game = None if machine['game'] == 'any' else GameConstants(machine['game'])
    current_machine.version = None if machine['game'] == 'any' else machine['version']
    g.data.local.machine.put_machine(current_machine)

    # Just return all machines for ease of updating
    return {
        'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
    }


@admin_pages.route('/pcbids/remove', methods=['POST'])
@jsonify
@adminrequired
def removepcbid() -> Dict[str, Any]:
    # Attempt to look this machine up
    pcbid = request.get_json()['pcbid']
    if g.data.local.machine.get_machine(pcbid) is None:
        raise Exception('Unable to find PCBID to delete!')

    g.data.local.machine.destroy_machine(pcbid)

    # Just return all machines for ease of updating
    return {
        'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
    }


@admin_pages.route('/cards/remove', methods=['POST'])
@jsonify
@adminrequired
def removecard() -> Dict[str, Any]:
    # Grab card, convert it
    card = request.get_json()['card']
    try:
        cardid = CardCipher.decode(card)
    except CardCipherException:
        raise Exception('Invalid card number!')

    # Make sure it is our card
    userid = g.data.local.user.from_cardid(cardid)

    # Remove it from the user's account
    g.data.local.user.destroy_card(userid, cardid)

    # Return new card list
    return {
        'cards': [format_card(card) for card in g.data.local.user.get_all_cards()],
    }


@admin_pages.route('/cards/add', methods=['POST'])
@jsonify
@adminrequired
def addcard() -> Dict[str, Any]:
    # Grab card, convert it
    card = request.get_json()['card']
    try:
        cardid = CardCipher.decode(card['number'])
    except CardCipherException:
        raise Exception('Invalid card number!')

    # Make sure it is our card
    userid = g.data.local.user.from_username(card['owner'])
    if userid is None:
        raise Exception('Cannot find user to add card to!')

    # See if it is already claimed
    curuserid = g.data.local.user.from_cardid(cardid)
    if curuserid is not None:
        raise Exception('This card is already in use!')

    # Add it to the user's account
    g.data.local.user.add_card(userid, cardid)

    # Return new card list
    return {
        'cards': [format_card(card) for card in g.data.local.user.get_all_cards()],
    }


@admin_pages.route('/users/search', methods=['POST'])
@jsonify
@adminrequired
def searchusers() -> Dict[str, Any]:
    # Grab card, convert it
    searchdetails = request.get_json()['user_search']
    if len(searchdetails['card']) > 0:
        try:
            cardid = CardCipher.decode(searchdetails['card'])
            actual_userid = g.data.local.user.from_cardid(cardid)
            if actual_userid is None:
                # Force a non-match below
                actual_userid = UserID(-1)
        except CardCipherException:
            actual_userid = UserID(-1)
    else:
        actual_userid = None

    def match(user: User) -> bool:
        if actual_userid is not None:
            return user.id == actual_userid
        else:
            return True

    return {
        'users': [format_user(user) for user in g.data.local.user.get_all_users() if match(user)],
    }


@admin_pages.route('/users/<int:userid>/balance/update', methods=['POST'])
@jsonify
@adminrequired
def updatebalance(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    credits = request.get_json()['credits']
    user = g.data.local.user.get_user(userid)
    arcades = g.data.local.machine.get_all_arcades()

    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    # Update balances
    for arcadeid in credits:
        balance = g.data.local.user.update_balance(userid, arcadeid, credits[arcadeid])
        if balance is not None:
            g.data.local.network.put_event(
                'paseli_transaction',
                {
                    'delta': credits[arcadeid],
                    'balance': balance,
                    'reason': 'admin adjustment',
                },
                userid=userid,
                arcadeid=arcadeid,
            )

    return {
        'arcades': {arcade.id: arcade.name for arcade in arcades},
        'balances': {arcade.id: g.data.local.user.get_balance(userid, arcade.id) for arcade in arcades},
        'events': [format_event(event) for event in g.data.local.network.get_events(userid=userid, event='paseli_transaction')],
    }


@admin_pages.route('/users/<int:userid>/username/update', methods=['POST'])
@jsonify
@adminrequired
def updateusername(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    username = request.get_json()['username']
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    if not valid_username(username):
        raise Exception('Invalid username!')

    # Make sure this user ID isn't taken
    potential_userid = g.data.local.user.from_username(username)
    if potential_userid is not None and potential_userid != userid:
        raise Exception('That username is already taken!')

    # Update the user
    user.username = username
    g.data.local.user.put_user(user)

    return {
        'username': username,
    }


@admin_pages.route('/users/<int:userid>/email/update', methods=['POST'])
@jsonify
@adminrequired
def updateemail(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    email = request.get_json()['email']
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    if not valid_email(email):
        raise Exception('Invalid email!')

    # Update the user
    user.email = email
    g.data.local.user.put_user(user)

    return {
        'email': email,
    }


@admin_pages.route('/users/<int:userid>/pin/update', methods=['POST'])
@jsonify
@adminrequired
def updatepin(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    pin = request.get_json()['pin']
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    if not valid_pin(pin, 'card'):
        raise Exception('Invalid pin, must be exactly 4 digits!')

    # Update the user
    g.data.local.user.update_pin(userid, pin)

    return {}


@admin_pages.route('/users/<int:userid>/password/update', methods=['POST'])
@jsonify
@adminrequired
def updatepassword(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    new1 = request.get_json()['new1']
    new2 = request.get_json()['new2']
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    # Now, make sure that the passwords match
    if new1 != new2:
        raise Exception('Passwords do not match each other!')

    # Now, make sure passwords are long enough
    if len(new1) < 6:
        raise Exception('Password is not long enough!')

    # Update the user
    g.data.local.user.update_password(userid, new1)

    return {}


@admin_pages.route('/users/<int:userid>/cards/remove', methods=['POST'])
@jsonify
@adminrequired
def removeusercard(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    # Grab card, convert it
    card = request.get_json()['card']
    try:
        cardid = CardCipher.decode(card)
    except CardCipherException:
        raise Exception('Invalid card number!')
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    # Remove it from the user's account
    g.data.local.user.destroy_card(userid, cardid)

    # Return new card list
    return {
        'cards': [CardCipher.encode(card) for card in g.data.local.user.get_cards(userid)],
    }


@admin_pages.route('/users/<int:userid>/cards/add', methods=['POST'])
@jsonify
@adminrequired
def addusercard(userid: int) -> Dict[str, Any]:
    # Cast the userID.
    userid = UserID(userid)

    # Grab card, convert it
    card = request.get_json()['card']
    try:
        cardid = CardCipher.decode(card)
    except CardCipherException:
        raise Exception('Invalid card number!')
    user = g.data.local.user.get_user(userid)
    # Make sure the user ID is valid
    if user is None:
        raise Exception('Cannot find user to update!')

    # See if it is already claimed
    curuserid = g.data.local.user.from_cardid(cardid)
    if curuserid is not None:
        raise Exception('This card is already in use!')

    # Add it to the user's account
    g.data.local.user.add_card(userid, cardid)

    # Return new card list
    return {
        'cards': [CardCipher.encode(card) for card in g.data.local.user.get_cards(userid)],
    }


@admin_pages.route('/news/add', methods=['POST'])
@jsonify
@adminrequired
def addnews() -> Dict[str, Any]:
    news = request.get_json()['news']
    if len(news['title']) == 0:
        raise Exception('Please provide a title!')
    if len(news['body']) == 0:
        raise Exception('Please provide a body!')

    g.data.local.network.create_news(news['title'], news['body'])

    return {
        'news': [format_news(news) for news in g.data.local.network.get_all_news()],
    }


@admin_pages.route('/news/remove', methods=['POST'])
@jsonify
@adminrequired
def removenews() -> Dict[str, Any]:
    newsid = request.get_json()['newsid']
    if g.data.local.network.get_news(newsid) is None:
        raise Exception('Unable to find entry to delete!')

    g.data.local.network.destroy_news(newsid)

    return {
        'news': [format_news(news) for news in g.data.local.network.get_all_news()],
    }


@admin_pages.route('/news/update', methods=['POST'])
@jsonify
@adminrequired
def updatenews() -> Dict[str, Any]:
    new_news = request.get_json()['news']
    if g.data.local.network.get_news(new_news['id']) is None:
        raise Exception('Unable to find entry to update!')
    if len(new_news['title']) == 0:
        raise Exception('Please provide a title!')
    if len(new_news['body']) == 0:
        raise Exception('Please provide a body!')

    news = g.data.local.network.get_news(new_news['id'])
    news.title = new_news['title']
    news.body = new_news['body']
    g.data.local.network.put_news(news)

    return {
        'news': [format_news(news) for news in g.data.local.network.get_all_news()],
    }


@admin_pages.route('/gamesettings/update', methods=['POST'])
@jsonify
@adminrequired
def updatesettings() -> Dict[str, Any]:
    # Cast the ID for type safety.
    arcadeid = g.data.local.machine.DEFAULT_SETTINGS_ARCADE

    game = GameConstants(request.get_json()['game'])
    version = request.get_json()['version']

    for setting_type, update_function in [
        ('bools', 'replace_bool'),
        ('ints', 'replace_int'),
        ('strs', 'replace_str'),
        ('longstrs', 'replace_str'),
    ]:
        for game_setting in request.get_json()[setting_type]:
            # Grab the value to update
            category = game_setting['category']
            setting = game_setting['setting']
            new_value = game_setting['value']

            # Update the value
            current_settings = g.data.local.machine.get_settings(arcadeid, game, version, category)
            if current_settings is None:
                current_settings = ValidatedDict()

            getattr(current_settings, update_function)(setting, new_value)

            # Save it back
            g.data.local.machine.put_settings(arcadeid, game, version, category, current_settings)

    # Return the updated value
    return {
        'game_settings': [
            gs for gs in get_game_settings(g.data, arcadeid)
            if gs['game'] == game.value and gs['version'] == version
        ][0],
    }
