from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

from flask import current_app, request, url_for
from flask_login import UserMixin, AnonymousUserMixin
from app.exceptions import ValidationError
from . import db, lm

import os
import base64
import hashlib
from decimal import Decimal
from datetime import date, time, datetime, timedelta
import json
from sortedcontainers import SortedDict
import requests
import onetimepass

class Permission:
    DEPLOY = 0x01
    ADMINISTER = 0x80

class Role(db.Model):
    __tablename__ = 'roles'
    pid = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role', lazy='dynamic')

    @staticmethod
    def insert_roles():
        roles = {
        'User': (Permission.DEPLOY, True),
        'Administrator': (0xff, False)
    }
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.permissions = roles[r][0]
            role.default = roles[r][1]
            db.session.add(role)
        db.session.commit()

    def __repr__(self):
        return '<Role %r>' % self.name


class User(db.Model, UserMixin):
    __tablename__ = 'users'

    pid = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.pid')) #FK
    password_hash = db.Column(db.String(128))

    confirmed = db.Column(db.Boolean, default=False)
    active = db.Column(db.Boolean, default=True)
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
    last_ip = db.Column(db.String(128))
    twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth
    otp_secret = db.Column(db.String(16))
    avatar_hash = db.Column(db.String(32))

    name = db.Column(db.Unicode(256))
    address = db.Column(db.Unicode(256))
    city = db.Column(db.Unicode(64))
    postcode = db.Column(db.String(10))
    country = db.Column(db.String(64))
    phone = db.Column(db.String(64))
    org_companyname = db.Column(db.Unicode(64))
    org_regaddress = db.Column(db.Unicode(128))
    org_responsible = db.Column(db.Unicode(128))
    org_bulstat = db.Column(db.String(16))
    org_vat = db.Column(db.Boolean, default=False)
    org_vatnum = db.Column(db.String(16))

    wallet = db.Column(db.Float, default=0.0)
    credit = db.Column(db.Float)
    creditlimit = db.Column(db.Float, default=20.0)

    currency = db.Column(db.String(3), default='BGN')
    language = db.Column(db.String(2), default='BG')    

    inv_deployments = db.relationship('Deployment', backref='owner', lazy='dynamic')
    inv_contracts = db.relationship('Contract', backref='owner', lazy='dynamic')
    inv_domains = db.relationship('Domain', backref='owner', lazy='dynamic')

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.role is None:
            if self.email == current_app.config['ADMIN_EMAIL']:
                #if email match config admin name create admin user
                self.role = Role.query.filter_by(permissions=0xff).first()
            if self.role is None:
                #if role is stil not set, create default user role
                self.role = Role.query.filter_by(default=True).first()

        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()

        if self.otp_secret is None:
            # generate a random secret
            self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def get_totp_uri(self):
        return 'otpauth://totp/DataPanel:{0}?secret={1}&issuer=datapanel'.format(self.email, self.otp_secret)

    def verify_totp(self, token):
        return onetimepass.valid_totp(token, self.otp_secret)

    def generate_confirmation_token(self, expiration=86400):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.pid})

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') != self.pid:
            return False
        self.confirmed = True
        db.session.add(self)
        db.session.commit()
        return True

    def generate_reset_token(self, expiration=86400):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'reset': self.pid})

    def reset_password(self, token, new_password):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('reset') != self.pid:
            return False
        self.password = new_password
        db.session.add(self)
        db.session.commit()
        return True

    def can(self, permissions):
        return self.role is not None and (self.role.permissions & permissions) == permissions

    def is_administrator(self):
        return self.can(Permission.ADMINISTER)

    def ping(self):
        self.last_seen = datetime.utcnow()
        db.session.add(self)
        db.session.commit()

    def gravatar(self, size=100, default='identicon', rating='g'):
        #this check is disabled because it didnt work for me but forcing https to gravatar is okay.
        #if request.is_secure:
        #    url = 'https://secure.gravatar.com/avatar'
        #else:
        #    url = 'http://www.gravatar.com/avatar'
        url = 'https://secure.gravatar.com/avatar'
        hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)

    def is_authenticated(self):
        return self.is_authenticated

    def get_id(self):
        return str(self.pid)

    def __repr__(self):
        return '<User %r>' % self.email

class AnonymousUser(AnonymousUserMixin):
    def can(self, permissions):
        return False

    def is_administrator(self):
        return False

lm.anonymous_user = AnonymousUser

@lm.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

def contact_proxmaster(data, method, cubeid=0):
    url = current_app.config['PROXMASTER_URL']
    data['apikey'] = current_app.config['APIKEY']
    data_json = json.dumps(data)
    #print('--> {}'.format(data))

    if method == 'vmcreate':
        url = '{}/{}'.format(url, method)
    else:
        url = '{}/{}/{}'.format(url, method, cubeid)

    db_result = requests.post( url, data=data_json, headers={"content-type": "application/json"}, timeout=30 )

    try:
        proxjson = db_result.json()
        #print('proxmaster query {}'.format(str(proxjson)))
        return proxjson
    except:
        return None

#TEMPLATE CLASSES
class Product(db.Model):
    __tablename__ = 'products'
    pid = db.Column(db.Integer, primary_key=True) #PK
    group = db.Column(db.Integer)
    name = db.Column(db.String(64))
    image = db.Column(db.String(128))
    description = db.Column(db.String(128))
    cpu = db.Column(db.Integer) #default cpu
    mem = db.Column(db.Integer) #default mem
    hdd = db.Column(db.Integer) #default hdd
    recipe = db.Column(db.String(128)) #defaut template name
    enabled = db.Column(db.Boolean)

    @staticmethod
    def insert_products():
        products = current_app.config['PRODUCTS']
        for p in products:
            product = Product.query.filter_by(pid=p).first()
            if product is None:
                product = Product(name=p)

            #insert default values
            product.group = products[p][0]
            product.name = products[p][1]
            product.image = products[p][2]
            product.description = products[p][3]
            product.cpu = products[p][4]
            product.mem = products[p][5]
            product.hdd = products[p][6]
            product.recipe = products[p][7]
            product.enabled = products[p][8]
            db.session.add(product)
        db.session.commit()

    @staticmethod
    def get_products():
        result = Product.query.all()
        products = {}
        for product in result:
            if product.enabled == True:
                products[int(product.pid)] = { 'group': product.group,
                                               'img': '/static/images/' + product.image, 
                                               'name': product.name, 
                                               'description': product.description, 
                                               'cpu': product.cpu,
                                               'mem': product.mem,
                                               'hdd': product.hdd,
                                               'recipe': product.recipe
                                             }
        return products

class Service(db.Model):
    __tablename__ = 'services'
    pid = db.Column(db.Integer, primary_key=True) #PK
    name = db.Column(db.String(64))
    image = db.Column(db.String(128))
    description = db.Column(db.Unicode(128))
    unitprice = db.Column(db.Float)
    enabled = db.Column(db.Boolean)

    @staticmethod
    def insert_services():
        services = current_app.config['SERVICES']
        for s in services:
            service = Service.query.filter_by(pid=p).first()
            if service is None:
                service = Service(name=s)

            #insert default values
            service.name = products[p][1]
            service.image = products[p][2]
            service.description = products[p][3]
            service.unitprice = products[p][4]
            service.enabled = products[p][5]
            db.session.add(service)
        db.session.commit()

    @staticmethod
    def get_services():
        result = Service.query.all()
        services = {}
        for service in result:
            if service.enabled == True:
                services[int(service.pid)] = {'img': '/static/images/' + service.image, 
                                              'name': service.name, 
                                              'description': service.description, 
                                              'unitprice': service.unitprice
                                             }
        return services

#TRANSACTIONS
class Transaction(db.Model):
    __tablename__ = 'transaction'
    pid = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
    date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    currency = db.Column(db.String, default='BGN')
    value = db.Column(db.Float)

#PROFORMA INVOICE CLASS
class Invoice(db.Model):
    __tablename__ = 'invoice'
    pid = db.Column(db.Integer, primary_key=True)
    invoice_number = db.Column(db.Integer, unique=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
    date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    currency = db.Column(db.String, default='BGN')
    tax = db.Column(db.Float) #VAT
    description = db.Column(db.Unicode(255))

    def sub_total(self):
        items = self.items
        sub_total = 0
        for item in items:
            sub_total += item.total()
        return sub_total
        
    def tax_amount(self):
        if not self.tax:
            return 0
        return self.sub_total() * self.tax / 100.0
        
    def grand_total(self):
        amount_str = str(self.sub_total() + self.tax_amount())
        amount = Decimal(amount_str)
        rounder = Decimal("0.05")  # precision for rounding
        return amount - amount.remainder_near(rounder)

class InvoiceItem(db.Model):
    __tablename__ = 'invoice_item'
    pid = db.Column(db.Integer, primary_key=True)
    item_number = db.Column(db.Integer)
    invoice_id = db.Column(db.Integer, db.ForeignKey('invoice.pid')) #FK
    item_title = db.Column(db.Unicode(255))
    item_quantity = db.Column(db.Float)
    item_price = db.Column(db.Float)
    amount = db.Column(db.Float)
    invoice = db.relationship(Invoice, backref=db.backref('items', order_by=item_number, cascade="delete"))

    def total(self):
        return self.item_quantity * self.item_price

#INVENTORY CLASSES
class Deployment(db.Model):
    __tablename__ = 'deployments'
    pid = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
    product_id = db.Column(db.Integer, db.ForeignKey('products.pid')) #FK
    date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    date_expire = db.Column(db.DateTime)
    enabled = db.Column(db.Boolean)

    machine_id = db.Column(db.BigInteger) #cubeid
    machine_alias = db.Column(db.String) #dns name
    machine_cpu = db.Column(db.Integer)
    machine_mem = db.Column(db.Integer)
    machine_hdd = db.Column(db.Integer)

    def charge():
        result = Deployment.query.all()
        for deploy in result:
            if deploy.enabled == True:
                managed_user = User.query.get(deploy.user_id)
                db.session.add(managed_user)
                current_product = Product.query.get(int(deploy.product_id))
                cpu_cost = deploy.machine_cpu * current_app.config['CPU_RATIO']
                mem_cost = ( deploy.machine_mem / 1024 ) * current_app.config['MEM_RATIO']
                hdd_cost = deploy.machine_hdd * current_app.config['HDD_RATIO']
                total = cpu_cost + mem_cost + hdd_cost

                if managed_user.wallet - managed_user.credit > managed_user.creditlimit:
                    print('{}> Deployment #{} costs {} today. Credit is now {}'.format(managed_user.email, deploy.machine_id, total, managed_user.credit))
                    managed_user.credit += total
                else:
                    print('{}> Deployment #{} costs {} today. Credit now is {} and its lower than the credit limit {}.'.format(managed_user.email, deploy.machine_id, total, managed_user.credit, managed_user.creditlimit))
                    print('')
            db.session.commit()

class Contract(db.Model):
    __tablename__ = 'contracts'
    pid = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
    service_id = db.Column(db.Integer, db.ForeignKey('services.pid')) #FK
    date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    date_expire = db.Column(db.DateTime)
    enabled = db.Column(db.Boolean)

    description = db.Column(db.Unicode)
    units = db.Column(db.Integer)
    discount = db.Column(db.Integer) #percent
    credit = db.Column(db.Float)

    def charge():
        result = Contract.query.all()
        for contract in result:
            managed_user = User.query.get(contract.user_id)
            db.session.add(contract)
            #if datetime.utcnow.date() > (contract.date_expire - timedelta(days=10)):
            if contract.enabled == True:
                print('{}> Contract {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, contract.pid, contract.date_expire))
                current_service = Service.query.get(int(contract.product_id))
                transaction = Transaction(user_id=managed_user.id, units=contract.units, unitvalue=(current_service.unitprice * contract.units), description=current_service.name)
                db.session.add(transaction)
                contract.data_expire = datetime.utcnow.date() + timedelta(days=30)
            db.session.commit()
        return True

class Domain(db.Model):
    __tablename__ = 'domains'
    pid = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
    date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    date_expire = db.Column(db.DateTime)
    enabled = db.Column(db.Boolean)

    fqdn = db.Column(db.String, unique=True)
    auto_update = db.Column(db.Boolean)
    credit = db.Column(db.Float)

    def charge():
        result = Domain.query.all()
        for domain in result:
            managed_user = User.query.get(domain.user_id)
            db.session.add(domain)
            #if datetime.utcnow.date() > (domain.date_expire - timedelta(days=60)):
            if domain.enabled == True:
                print('{}> Domain {} will expire in 60 days at {}. Creating new order...'.format(managed_user.email, domain.fqdn, domain.date_expire))
                transaction = Transaction(user_id=managed_user.id, unitvalue=25, description=domain.fqdn)
                db.session.add(transaction)
            db.session.commit() 
        return True        

