import enum
import json
import datetime
import os, os.path
from celery  import Celery
from functools import wraps
from pyfcm import FCMNotification
from flask_sqlalchemy import SQLAlchemy
from flask_httpauth import HTTPBasicAuth
from passlib.apps import custom_app_context as pwd_context
from itsdangerous import (URLSafeSerializer as Serializer, BadSignature, SignatureExpired)
from flask import Flask, request, jsonify, g, url_for, send_file, render_template, abort, Response

import tools.settings as settings
import tools.waifu as wt
import tools.database_tools as dbt
import tools.image_scraping as ist

from tools.slack_posting import postMsg

################################################################################
# SETUP

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/moe.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = settings.getValue('moe', 'key')

API_BASE = '/api/waifus'

db = SQLAlchemy(app)
auth = HTTPBasicAuth()

app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'

celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)

push_service = FCMNotification(api_key=settings.getFCMKey())

################################################################################
# DECORATORS

def limit_access(min_auth_level):
    def funcWrapper(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            if (g.user.auth_level.value < min_auth_level.value):
                raise APIException('Invalid Auth Level', 403)
            return func(*args, **kwargs)
        return wrapped
    return funcWrapper

def debug(func):
    def funcWrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            return repr(e)
    return funcWrapper

################################################################################
# CLASSES

class APIException(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        Exception.__init__(self)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['error'] = self.message
        return rv

class AuthLevel(enum.Enum):
    User = 1
    Weeb = 2
    Mod = 3
    Admin = 4
    Arshad = 5

    def get_dict():
        return dict(user = 1, weeb = 2, mod = 3, admin = 4, arshad = 5)

class User(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(80), unique = True, index = True)
    password_hash = db.Column(db.String(128))
    auth_level = db.Column(db.Enum(AuthLevel))
    waifu_list_str = db.Column(db.String)
    fcm_token = db.Column(db.String)

    def __init__(self, username, password, auth_level = AuthLevel.User):
        self.username = username
        self.password_hash = self.hash_password(password)
        self.auth_level = auth_level
        self.waifu_list_str = 'lily'
        self.fcm_token = None

    def __repr__(self):
        return '<User {}>'.format(self.username)

    # Password/Auth Stuff

    def hash_password(self, password):
        return pwd_context.encrypt(password)

    def verify_password(self, password):
        return pwd_context.verify(password, self.password_hash)

    def generate_auth_token(self, expiration = 3600):
        s = Serializer(app.config['SECRET_KEY'])#, expires_in = expiration)
        return s.dumps({ 'id': self.id })

    def add_waifu_to_list(self, waifu):
        if (self.username == 'user'):
            return

        waifu_list = self.get_waifu_list()

        if ((wt.isValidWaifu(waifu)
            and (not (waifu in waifu_list)))
            and ((not dbt.isWaifuNSFW(waifu)) or self.auth_level.value > 1)
            and ((not (dbt.isWaifuNSFW(waifu) and dbt.isWaifu3D(waifu))) or self.auth_level.value > 4)):

            waifu_list.append(waifu)
            try:
                waifu_list.remove('')
            except ValueError:
                pass
            waifu_list.sort()
            self.waifu_list_str = ','.join(waifu_list)
            db.session.commit()

    def remove_waifu_from_list(self, waifu):
        if (self.username == 'user'):
            return

        try:
            waifu_list = self.get_waifu_list()
            waifu_list.remove(waifu)
            self.waifu_list_str = ','.join(waifu_list)
            db.session.commit()
        except ValueError:
            pass

    def get_waifu_list(self):
        return self.waifu_list_str.split(',')

    def get_dict(self):
        return dict(username = self.username, auth_level = self.auth_level.value, waifus = self.get_waifu_list())

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except SignatureExpired:
            return None
        except BadSignature:
            return None
        user = User.query.get(data['id'])
        return user

    @staticmethod
    def get_user(username):
        return User.query.filter_by(username = username).first()

    @staticmethod
    def is_valid_user(username):
        return (User.query.filter_by(username = username).first() != None)

################################################################################
# HELPER METHODS

@auth.verify_password
def verify_login_info(username_or_token, password):
    user = User.verify_auth_token(username_or_token)
    if not user:
        user = User.query.filter_by(username = username_or_token).first()
        if ((not user) or (not user.verify_password(password))):
            return False
    g.user = user
    return True

@auth.error_handler
def unauthorized():
    raise APIException('Not Logged In', 403)

def verify_auth_level(user, auth_level):
    if not isinstance(user, User):
        user = User.query.filter_by(username = user).first()
        if user == None:
            return False

    return (user.get_auth_level().value >= (auth_level.value if isinstance(auth_level, AuthLevel) else auth_level))

@app.errorhandler(APIException)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

def get_web_link(filepath):
    return filepath.replace(os.path.join(os.getcwd(), 'images'), 'https://arshbox.xyz/mal-images')

def send_notif(user, title, body):
    return push_service.notify_single_device(
        registration_id = user.fcm_token,
        message_title = title,
        message_body = body)


################################################################################
# CELERY STUFF

@celery.task
def async_scrape(waifu, url, ua, username):
    response = ist.executeScrape([waifu, url])

    user = User.get_user(username)

    if ('Android' in ua) and (user.fcm_token):
        send_notif(user, 'Scraping Completed', response)
    else:
        postMsg(response, settings.getDebugChannel())

@celery.task
def async_delete_image(args):
    waifu, imageNum, user = args
    postMsg('Deleting image {} from {} at the request of {} through API.'.format(*args), settings.getDebugChannel())
    postMsg(ist.removeSingleImage(waifu, imageNum), settings.getDebugChannel())

@celery.task
def async_delete_entire_folder(args):
    waifu, user = args
    postMsg('Deleting the entire waifu, {}, at the request of {} through API.'.format(*args), settings.getDebugChannel())
    dbt.deleteWaifu(waifu)
    postMsg('{} deleted successfully.'.format(waifu), settings.getDebugChannel())

################################################################################
# WEB STUFF

@app.route('/')
def index():
    #return 'ok'
    return render_template('landing.html')

@app.route('/lily')
def lily():
    image_url = wt.pickImage('lily').replace(os.path.join(os.getcwd(), 'images'), 'https://arshbox.xyz/mal-images')
    return render_template('picture.html', title = 'Lily!', response = url_for('lily'), url = image_url)

@app.route('/sad')
def sad():
    image_url = wt.pickImage('sad').replace(os.path.join(os.getcwd(), 'images'), 'https://arshbox.xyz/mal-images')
    return render_template('picture.html', title = 'Cute Sadness', response = url_for('sad'), url = image_url)

@app.route('/waifu/<folder>')
def waifu(folder):
    from tools.database_tools import getWaifuDict

    if not wt.isValidWaifu(folder):
        return 'Not a valid waifu!'

    image_url = wt.pickImage(folder).replace(os.path.join(os.getcwd(), 'images'), 'https://arshbox.xyz/mal-images')

    title = getWaifuDict(folder)['message']
    if not title:
        title = folder
    else:
        title = title.replace('*', '')

    return render_template('picture.html', title = title, response = url_for('waifu', folder = folder), url = image_url)

@app.route('/waifus')
def list_waifus():
    from tools.database_tools import getWaifuList2D

    return render_template('waifus.html', waifus = getWaifuList2D())


################################################################################
# NEW API STUFF


@app.route(API_BASE + '/status')
def boop():
    send_notif(User.get_user('chaosking121'), 'boop', 'poop')
    return 'ok'

# Waifus Stuff

@app.route(API_BASE)
@auth.login_required
def get_waifus():
    if g.user.auth_level == AuthLevel.Arshad:
        return Response(json.dumps(dbt.getWaifuDictAll(safe = False)), mimetype = 'application/json')
    else:
        return Response(json.dumps(dbt.getWaifuDictAll()), mimetype = 'application/json')

@app.route(API_BASE + '/<folder>/delete')
@auth.login_required
@limit_access(AuthLevel.Arshad)
def delete_waifu(folder):
    args = [folder, g.user.username]
    task = async_delete_entire_folder.delay(args)
    return 'Deleting {}. Task ID: {}'.format(args[0], task.id)

# Image Stuff

@app.route(API_BASE + '/<folder>')
@auth.login_required
def get_random_image(folder):

    if ((not wt.isValidWaifu(folder))
        or ((dbt.isWaifuNSFW) and (g.user.auth_level.value < 2))
        or (dbt.isWaifu3D(folder)
            and dbt.isWaifuNSFW(folder)
            and g.user.auth_level != AuthLevel.Arshad)):
        image = dbt.getImage('lily')
    else:
        image = dbt.getImage(folder)

    imageDict = image.getDict()
    del imageDict['filename']

    imageDict['image_url'] = get_web_link(image.getPath())
    return jsonify(imageDict)

@app.route(API_BASE + '/<folder>/<int:imageNum>')
@auth.login_required
@limit_access(AuthLevel.Admin)
def get_specific_image(folder, imageNum):
    if ((not wt.isValidWaifu(folder))
        or ((dbt.isWaifuNSFW) and (g.user.auth_level.value < 2))
        or (dbt.isWaifu3D(folder)
            and dbt.isWaifuNSFW(folder)
            and g.user.auth_level != AuthLevel.Arshad)):
        image = dbt.getImage('lily')
    else:
        image = dbt.getImage(folder, imageNum)

    imageDict = image.getDict()
    del imageDict['filename']

    imageDict['image_url'] = get_web_link(image.getPath())
    return jsonify(imageDict)

@app.route(API_BASE + '/<folder>/<int:imageNum>/delete')
@auth.login_required
def delete_image(folder, imageNum):
    if not wt.isValidWaifu(folder):
        APIException('Waifu Does Not Exist.', 400)

    return ist.removeSingleImage(folder, imageNum)

# List Stuff

@app.route(API_BASE + '/lists/<list_name>')
@auth.login_required
def get_waifu_list(list_name):
    if list_name == 'all':
        if g.user.auth_level == AuthLevel.Arshad:
            waifuDictList = dbt.getWaifuDictAll(safe = False)
        elif g.user.auth_level.value > 1:
            waifuDictList = dbt.getWaifuDictAll()
        else:
            waifuDictList = [dbt.getWaifuDict('lily'), dbt.getWaifuDict('kitties')]
    else:
        user = User.get_user(list_name)
        if ((not user) or
            ((list_name != g.user.username) and (g.user.auth_level.value < 4))):
            waifuDictList = [dbt.getWaifuDict('lily')]
        else:
            waifuDictList = [dbt.getWaifuDict(x) for x in user.get_waifu_list()]

    return Response(json.dumps(waifuDictList), mimetype = 'application/json')

@app.route(API_BASE + '/lists/<list_name>/add', methods = ['POST'])
@auth.login_required
def add_waifu_to_list(list_name):
    user = User.get_user(list_name)
    if not user:
        raise APIException('List Not Found', 404)
    elif ((list_name != g.user.username) and (g.user.auth_level.value < 4)):
        raise APIException('Invalid Authorization', 403)

    waifus = request.form.get('waifus').split(',')

    for waifu in waifus:
        user.add_waifu_to_list(waifu)

    return Response(json.dumps([dbt.getWaifuDict(x) for x in user.get_waifu_list()]))

@app.route(API_BASE + '/lists/<list_name>/remove', methods = ['POST'])
@auth.login_required
def remove_waifu_from_list(list_name):
    user = User.get_user(list_name)
    if not user:
        raise APIException('List Not Found', 404)
    elif ((list_name != g.user.username) and (g.user.auth_level.value < 4)):
        raise APIException('Invalid Authorization', 403)

    waifus = request.form.get('waifus').split(',')

    for waifu in waifus:
        user.remove_waifu_from_list(waifu)

    return Response(json.dumps([dbt.getWaifuDict(x) for x in user.get_waifu_list()]))

# User Stuff

@app.route(API_BASE + '/push-token', methods = ['POST'])
@auth.login_required
def register_token():
    token = request.form.get('token')
    user = g.user
    user.fcm_token = token
    db.session.commit()
    return 'Token Accepted'

@app.route(API_BASE + '/token')
@auth.login_required
def get_token():
    token = g.user.generate_auth_token()
    return jsonify({'token': token})

@app.route(API_BASE + '/login')
@auth.login_required
def try_login():
    return g.user.username

@app.route(API_BASE + '/users', methods = ['GET'])
@auth.login_required
@limit_access(AuthLevel.Admin)
def get_users():
    return Response(json.dumps([x.get_dict() for x in User.query.all()]))

@app.route(API_BASE + '/users', methods = ['POST'])
def signup():
    username = request.form.get('username')
    password = request.form.get('password')

    if User.query.filter_by(username = username).first():
        raise APIException('Username Already In Use', 400)

    new_user = User(username, password, AuthLevel.Weeb)

    db.session.add(new_user)
    db.session.commit()

    return jsonify(new_user.get_dict())

@app.route(API_BASE + '/user/<username>', methods = ['GET'])
@auth.login_required
def get_user_info(username):
    if ((g.user.username != username) and (g.user.auth_level.value < 4)):
        APIException('Invalid Auth Level', 403)

    user = User.get_user(username)

    if not user:
        APIException('User Not Found', 404)

    return jsonify(user.get_dict())

@app.route(API_BASE + '/user/<username>/promote', methods = ['POST'])
@auth.login_required
@limit_access(AuthLevel.Admin)
def change_auth_level(username):
    user = User.get_user(username)

    if not user:
        APIException('User Not Found', 404)

    try:
        user.auth_level = AuthLevel(int(request.form.get('new_auth_level')))
        db.session.commit()
        return jsonify(user.get_dict())
    except ValueError:
        APIException('Invalid Auth Level', 400)


@app.route(API_BASE + '/users/promote-self', methods = ['POST'])
@auth.login_required
def promote_self():

    user = g.user
    token = request.form.get('token')
    result = False

    try:
        with open(os.path.join(os.getcwd(), 'data', 'mal-moe-promotion-tokens'), 'r+') as f:

            lines = f.readlines()
            f.seek(0)

            for line in lines:

                actual_token, new_auth_level = line.strip().split(';')
                new_auth_level = AuthLevel(int(new_auth_level))

                if (new_auth_level == AuthLevel.Arshad):
                    APIException('Forbidden Action', 403)

                if (token.strip() == actual_token):
                    user.auth_level = new_auth_level
                    result = True
                    db.session.commit()
                else:
                    f.write(line)

            f.truncate()

    except ValueError:
        APIException('Invalid Auth Level', 400)

    if result:
        return jsonify(user.get_dict())

    APIException('Invalid Token', 403)


@app.route(API_BASE + '/gen-promotion-token', methods = ['POST'])
@auth.login_required
@limit_access(AuthLevel.Arshad)
def gen_promotion_token():
    import random, string

    auth_level = request.form.get('new_auth_level')
    token = ''.join(random.choice(string.lowercase) for i in range(8))

    with open(os.path.join(os.getcwd(), 'data', 'mal-moe-promotion-tokens'), 'a') as f:
        f.write('{};{}\n'.format(token, auth_level))

    return jsonify(dict(token = token, auth_level = repr(AuthLevel(int(auth_level)))))


@app.route(API_BASE + '/get-promotion-tokens', methods = ['GET'])
@auth.login_required
@limit_access(AuthLevel.Arshad)
def get_tokens():
    tokens = []

    with open(os.path.join(os.getcwd(), 'data', 'mal-moe-promotion-tokens'), 'r') as f:
        for line in f.readlines():
            token, new_auth_level = line.strip().split(';')
            auth_level = AuthLevel(int(new_auth_level))
            tokens.append(dict(token = token, auth_level = repr(auth_level)))

    return Response(json.dumps(tokens))


# Scraping Stuff

@app.route(API_BASE + '/scrape', methods = ['POST'])
@auth.login_required
@limit_access(AuthLevel.Mod)
def scrape():
    args = [request.form.get('waifu'), request.form.get('url'), request.headers.get('User-Agent'), g.user.username]
    waifu, url, ua, username = args
    user = g.user

    if (not (wt.isValidWaifu(waifu))) and (user.auth_level.value < 4):
        APIException('Only Admins can add new waifus.', 400)

    if ((('imgur.com/a/' in url) or ('imgur.com/gallery/' in url))
        and (user.auth_level.value < 4)):
        APIException('Only Admins can scrape imgur albums.', 400)

    if (('4chan.org' in url) and (user.auth_level.value < 4)):
        APIException('Only Admins can scrape 4chan threads.', 400)

    if (url.startswith('/r/') and (user.auth_level.value < 5)):
        APIException('Only Arshad can scrape subreddits.', 400)

    if (url.startswith('/')):
        APIException('Scraping local folders via web is forbidden.', 400)

    task = async_scrape.delay(*args)

    return 'Scraping to {}.'.format(waifu)

################################################################################
# MAIN

def reset_db():
    db.drop_all(bind=None)
    db.create_all()

    arshad = User('chaosking121', 'poop', AuthLevel.Arshad)
    db.session.add(arshad)
    for waifu in dbt.getWaifuList('chaosking121'):
        arshad.add_waifu_to_list(waifu)

    def_user = User('user', 'pass', AuthLevel.User)
    def_user.add_waifu_to_list('lily')
    db.session.add(def_user)

    db.session.commit()
    print 'All done'

if __name__ == '__main__':
    #reset_db()
    app.run('0.0.0.0')
