import logging
import json
from webcandy import util

from flask import (
    g, Blueprint, render_template, jsonify, request, url_for
)
from werkzeug.exceptions import NotFound
from typing import Optional

from webcandy.definitions import ROOT_DIR, DATA_DIR
from .models import User
from .extensions import auth, db
from .server import proxy_server, clients

views = Blueprint('views', __name__, static_folder=f'{ROOT_DIR}/static/dist',
                  template_folder=f'{ROOT_DIR}/static')
api = Blueprint('api', __name__)


# -------------------------------
# Login methods
# -------------------------------


@auth.verify_token
def verify_auth_token(token: str) -> bool:
    """
    Verify an authentication token and set ``g.user`` to the user associated
    with the token if it is valid.

    :param token: the token to verify
    :return: ``True`` if a valid token was provided; ``False`` otherwise
    """
    user = User.get_user(token)
    if user:
        g.user = user
        return True
    return False


# -------------------------------
# React routes
# -------------------------------
# TODO: Allow loading of favicon.ico


@views.route('/', defaults={'path': ''}, methods=['GET'])
@views.route('/<path:path>')
def index(path: str):
    # catch-all to route any non-API calls to React, which then does its own
    # routing to display the correct page
    del path  # just to get rid of IDE warnings
    return render_template('index.html')


# -------------------------------
# API routes
# -------------------------------


@api.route('/', defaults={'path': ''}, methods=['GET'])
@api.route('/<path:path>')
def api_catch_all(path: str):
    del path
    # this will only be reached if a user tries to get /api/<non-existing path>
    # we want to generate a JSON 404 response rather than the React one that
    # would be generated by the index catch-al if this method did not exist
    return not_found(NotFound())


@api.route('/token', methods=['POST'])
def get_auth_token():
    req_json = request.get_json()

    user = User.query.filter_by(username=req_json["username"]).first()
    if not user or not user.check_password(req_json["password"]):
        description = 'Invalid username and password combination'
        return jsonify(util.format_error(401, description)), 401

    g.user = user
    token = g.user.generate_auth_token()
    return jsonify({'token': token.decode('ascii')})


@api.route('/users/new', methods=['POST'])
def new_user():
    username = request.json.get('username')
    email = request.json.get('email')  # optional
    password = request.json.get('password')

    if not (username and password):
        error_description = 'Missing username or password'
        logging.error(error_description)
        return jsonify(util.format_error(400, error_description)), 400
    if User.query.filter_by(username=username).first() is not None:
        error_description = f"User '{username}' already exists"
        logging.error(error_description)
        return jsonify(util.format_error(400, error_description)), 400

    if not email:
        user = User(username=username)
    else:
        user = User(username=username, email=email)
    user.set_password(password)

    # add new user to database
    db.session.add(user)
    db.session.commit()

    # create data file
    with open(f'{ROOT_DIR}/server/data/{user.user_id}.json', 'w+') as file:
        json.dump({'colors': dict(), 'color_lists': dict()}, file)

    return (
        jsonify({'username': user.username}),
        201,
        {'Location': url_for('views.index')}
    )


def get_user_from_request() -> Optional[User]:
    """
    Get the user referenced in a request.

    Query string parameters:
    - ``u``: username or ID to get data for (required)
    - ``type``: "username" or "id" to specify if ``u`` is a username or ID. If
                unspecified or some other value, ``u`` will first be interpreted
                as a username, and then an ID.

    :return: a ``User`` if one was found
    :raises ValueError: if no ``u`` query string parameter was provided
    """
    u = request.args.get('u')
    u_type = request.args.get('type')

    if not u:
        raise ValueError('Please provide a username or ID the u parameter')

    if u_type == 'username':
        user = User.query.filter_by(username=u).first()
    elif u_type == 'id':
        user = User.query.get(u)
    else:
        # first, query with u as useranme
        user = User.query.filter_by(username=u).first()
        if not user:
            # if that doesn't work, query with u as id
            user = User.query.get(u)

    return user  # possibly None


@api.route('/user_data', methods=['GET', 'PUT'])
@auth.login_required
def user_data():
    """
    Get saved user data for the current user.
    """
    if request.method == 'GET':
        # TODO: Return patterns with user data?
        return jsonify(util.load_user_data(g.user.user_id))
    else:
        # PUT request
        retval = {
            'colors': {
                'added': dict(),
                'modified': dict()
            },
            'color_lists': {
                'added': dict(),
                'modified': dict()
            }
        }

        with open(f'{DATA_DIR}/{g.user.user_id}.json') as data_file:
            json_data = json.load(data_file)

        for section, data in request.get_json().items():

            # ensure section is valid
            if section == 'colors':

                for name, color in data.items():
                    if util.is_color(color):
                        if name in json_data['colors']:
                            retval['colors']['modified'][name] = color
                        else:
                            retval['colors']['added'][name] = color
                        json_data['colors'][name] = color

            elif section == 'color_lists':

                for name, color_list in data.items():
                    if all([util.is_color(color) for color in color_list]):
                        if name in json_data['color_lists']:
                            retval['color_lists']['modified'][name] = color_list
                        else:
                            retval['color_lists']['added'][name] = color_list
                        json_data['color_lists'][name] = color_list

        # re-open to overwrite rather than append to using r+
        with open(f'{DATA_DIR}/{g.user.user_id}.json', 'w') as data_file:
            json.dump(json_data, data_file, indent=4)

        return jsonify(retval)


@api.route('/user_info', methods=['GET'])
@auth.login_required
def user_info():
    """
    Get account information for the current user.
    """
    return jsonify({'user_id': g.user.user_id, 'username': g.user.username,
                    'email': g.user.email})


@api.route('/clients', methods=['GET'])
@auth.login_required
def get_clients():
    """
    Determine if the current user has a connected client.
    """
    return jsonify(g.user.user_id in clients)


@api.route('/patterns', methods=['GET'])
@auth.login_required
def patterns():
    """
    Get a list of valid lighting pattern names.
    """
    if g.user.user_id not in clients:
        return util.format_error(400, 'No connected clients for current user')
    return jsonify(clients[g.user.user_id].patterns)


@api.route('/colors', methods=['GET', 'PUT'])
@auth.login_required
def colors():
    """
    Operations on the ``colors`` attribute of the logged in user.

    GET: Get a mapping from name to hex value of saved colors
    PUT: Add a new saved color
    """
    if request.method == 'GET':
        return jsonify(g.user.get_colors())
    else:
        # PUT request
        retval = {
            'added': dict(),
            'modified': dict(),
        }

        with open(f'{DATA_DIR}/{g.user.user_id}.json') as data_file:
            json_data = json.load(data_file)

        for name, color in request.get_json().items():
            if util.is_color(color):
                if name in json_data['colors']:
                    retval['modified'][name] = color
                else:
                    retval['added'][name] = color
                json_data['colors'][name] = color

        # re-open to overwrite rather than append to using r+
        with open(f'{DATA_DIR}/{g.user.user_id}.json', 'w') as data_file:
            json.dump(json_data, data_file, indent=4)

        return jsonify(retval)


@api.route('/color_lists', methods=['GET', 'PUT'])
@auth.login_required
def color_lists():
    """
    Operations on the ``color_lists`` attribute of the logged in user.

    GET: Get a mapping from name to list of hex value of saved colors lists
    PUT: Add a new saved color list
    """
    if request.method == 'GET':
        return jsonify(util.load_user_data(g.user.user_id)['color_lists'])
    else:
        # PUT request
        retval = {
            'added': dict(),
            'modified': dict(),
        }

        with open(f'{DATA_DIR}/{g.user.user_id}.json') as data_file:
            json_data = json.load(data_file)

        for name, color_list in request.get_json().items():
            if all([util.is_color(color) for color in color_list]):
                if name in json_data['color_lists']:
                    retval['modified'][name] = color_list
                else:
                    retval['added'][name] = color_list
                json_data['color_lists'][name] = color_list

        # re-open to overwrite rather than append to using r+
        with open(f'{DATA_DIR}/{g.user.user_id}.json', 'w') as data_file:
            json.dump(json_data, data_file, indent=4)

        return jsonify(retval)


@api.route('/submit', methods=['POST'])
@auth.login_required
def submit():
    """
    Handle the submission of a lighting configuration to run.

    POST JSON fields:
    - "pattern": the name of the pattern to run (required)
    - "strobe": whether to add a strobe effect
    - "color": the color to use, if applicable
    - "color_list": the color list to use, if applicable

    :return: JSON indicating if running was successful
    """
    return jsonify(
        success=proxy_server.send(g.user.user_id, request.get_data()))


# -------------------------------
# Error handlers
# -------------------------------


def not_found(error):
    return jsonify(util.format_error(404, error.description)), 404


def internal_server_error(_):
    return (
        jsonify(
            util.format_error(500, 'The server encountered an internal error '
                                   'and was unable to complete your request.')),
        500
    )
