import json
import logging
import logging.config
import os
import sqlite3
import uuid
import yaml
from datetime import datetime
from functools import wraps, update_wrapper
from importlib import import_module
from time import localtime, strftime
import time
import os.path

from flask import Flask, redirect, json, g, make_response
from flask_socketio import SocketIO

from baseapi import *
from db import DBModel
from modules.core.basetypes import Sensor, Actor
from modules.database.dbmodel import Kettle



class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        try:

            if isinstance(obj, DBModel):
                return obj.__dict__
            elif isinstance(obj, Actor):
                return {"state": obj.value}
            elif isinstance(obj, Sensor):
                return {"value": obj.value, "unit": obj.unit}
            elif hasattr(obj, "callback"):
                return obj()
            else:
                return None

            return None
        except TypeError as e:
            pass
        return None


class Addon(object):
    def __init__(self, cbpi):
        self.step = StepAPI(cbpi)
        self.actor = ActorAPI(cbpi)
        self.sensor = SensorAPI(cbpi)
        self.kettle = KettleAPI(cbpi)
        self.fermenter = FermenterAPI(cbpi)
        self.core = CoreAPI(cbpi)

    def init(self):
        self.core.init()
        self.step.init()
        self.actor.init()
        self.sensor.init()
        # self.kettle.init()
        # self.fermenter.init()


class ActorCore(object):
    key = "actor_types"

    def __init__(self, cbpi):
        self.logger = logging.getLogger(__name__)

        self.cbpi = cbpi
        self.cbpi.cache["actors"] = {}
        self.cbpi.cache[self.key] = {}

    def init(self):
        for key, value in self.cbpi.cache["actors"].iteritems():
            self.init_one(key)

    def init_one(self, id):
        try:

            actor = self.cbpi.cache["actors"][id]
            clazz = self.cbpi.cache[self.key].get(actor.type)["class"]
            cfg = actor.config.copy()
            cfg.update(dict(cbpi=self.cbpi, id=id))
            self.cbpi.cache["actors"][id].instance = clazz(**cfg)
            actor.state = 0
            actor.power = 100
            self.cbpi.emit("INIT_ACTOR", id=id)
        except Exception as e:
            self.logger.error(e)

    def stop_one(self, id):
        self.cbpi.cache["actors"][id]["instance"].stop()
        self.cbpi.emit("STOP_ACTOR", id=id)

    def on(self, id, power=100):
        try:
            actor = self.cbpi.cache["actors"].get(int(id))
            actor.instance.on()
            actor.state = 1
            actor.power = power
            self.cbpi.ws_emit("SWITCH_ACTOR", actor)
            self.cbpi.emit("SWITCH_ACTOR_ON", id=id, power=power)
            return True
        except Exception as e:
            self.logger.error(e)
            return False

    def off(self, id):
        try:
            actor = self.cbpi.cache["actors"].get(int(id))
            actor.instance.off()
            actor.state = 0
            self.cbpi.ws_emit("SWITCH_ACTOR", actor)
            self.cbpi.emit("SWITCH_ACTOR_OFF", id=id)
            return True
        except Exception as e:
            self.logger.error(e)
            return False

    def toggle(self, id):
        if self.cbpi.cache.get("actors").get(id).state == 0:
            self.on(id)
        else:
            self.off(id)

    def power(self, id, power):
        try:
            actor = self.cbpi.cache["actors"].get(int(id))
            actor.instance.power(power)
            actor.power = power
            self.cbpi.ws_emit("SWITCH_ACTOR", actor)
            self.cbpi.emit("SWITCH_ACTOR_POWER_CHANGE", id=id, power=power)
            return True
        except Exception as e:
            self.logger.error(e)
            return False

    def action(self, id, method):
        self.cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()


    def toggle_timeout(self, id, seconds):

        def toggle( id, seconds):
            self.cbpi.cache.get("actors").get(int(id)).timer = int(time.time()) + int(seconds)
            self.toggle(int(id))
            self.cbpi.sleep(seconds)
            self.cbpi.cache.get("actors").get(int(id)).timer = None
            self.toggle(int(id))
        job = self.cbpi._socketio.start_background_task(target=toggle, id=id, seconds=seconds)

    def get_state(self, actor_id):
        pass


class SensorCore(object):
    key = "sensor_types"

    def __init__(self, cbpi):
        self.logger = logging.getLogger(__name__)

        self.cbpi = cbpi
        self.cbpi.cache["sensors"] = {}
        self.cbpi.cache["sensor_instances"] = {}
        self.cbpi.cache["sensor_types"] = {}

    def init(self):
        for key, value in self.cbpi.cache["sensors"].iteritems():
            self.init_one(key)

    def init_one(self, id):
        try:
            sensor = self.cbpi.cache["sensors"][id]
            clazz = self.cbpi.cache[self.key].get(sensor.type)["class"]
            cfg = sensor.config.copy()
            cfg.update(dict(cbpi=self.cbpi, id=id))
            self.cbpi.cache["sensors"][id].instance = clazz(**cfg)
            self.cbpi.cache["sensors"][id].instance.init()

            self.cbpi.emit("INIT_SENSOR", id=id)

            def job(obj):
                obj.execute()

            t = self.cbpi._socketio.start_background_task(target=job, obj=self.cbpi.cache["sensors"][id].instance)
            self.cbpi.emit("INIT_SENSOR", id=id)

        except Exception as e:
            self.logger.error(e)

    def stop_one(self, id):

        self.cbpi.cache["sensors"][id].instance.stop()
        self.cbpi.emit("STOP_SENSOR", id=id)

    def get_value(self, sensorid):
        try:
            return self.cbpi.cache["sensors"][sensorid].instance.value
        except:
            return None

    def get_state(self, actor_id):
        pass

    def write_log(self, id, value, prefix="sensor"):
        filename = "./logs/%s_%s.log" % (prefix, str(id))
        formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
        msg = str(formatted_time) + "," + str(value) + "\n"

        with open(filename, "a") as file:
            file.write(msg)

    def action(self, id, method):
        self.cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()



class BrewingCore(object):
    def __init__(self, cbpi):
        self.cbpi = cbpi
        self.cbpi.cache["step_types"] = {}
        self.cbpi.cache["controller_types"] = {}

    def log_action(self, text):
        filename = "./logs/action.log"
        formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
        with open(filename, "a") as file:
            text = text.encode("utf-8")
            file.write("%s,%s\n" % (formatted_time, text))

    def get_controller(self, name):
        return self.cbpi.cache["controller_types"].get(name)

    def set_target_temp(self, id, temp):
        self.cbpi.cache.get("kettle")[id].target_temp = float(temp)
        Kettle.update(**self.cbpi.cache.get("kettle")[id].__dict__)
        self.cbpi.ws_emit("UPDATE_KETTLE_TARGET_TEMP", {"id": id, "target_temp": temp})
        self.cbpi.emit("SET_KETTLE_TARGET_TEMP", id=id, temp=temp)


    def toggle_automatic(self, id):
        kettle = self.cbpi.cache.get("kettle")[id]
        if kettle.state is False:
            # Start controller
            if kettle.logic is not None:
                cfg = kettle.config.copy()
                cfg.update(dict(api=self.cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
                instance = self.get_controller(kettle.logic).get("class")(**cfg)
                instance.init()
                kettle.instance = instance

                def run(instance):
                    instance.run()

                t = self.cbpi._socketio.start_background_task(target=run, instance=instance)
            kettle.state = not kettle.state
            self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
            self.cbpi.emit("KETTLE_CONTROLLER_STARTED", id=id)
        else:
            # Stop controller
            kettle.instance.stop()
            kettle.state = not kettle.state
            self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
            self.cbpi.emit("KETTLE_CONTROLLER_STOPPED", id=id)


class FermentationCore(object):
    def __init__(self, cbpi):
        self.cbpi = cbpi
        self.cbpi.cache["fermenter"] = {}
        self.cbpi.cache["fermentation_controller_types"] = {}

    def get_controller(self, name):
        return self.cbpi.cache["fermentation_controller_types"].get(name)


class CraftBeerPI(object):

    _logger_configuration_file = './config/logger.yaml'

    cache = {}
    eventbus = {}

    def __init__(self):
        if os.path.isfile(self._logger_configuration_file):
            logging.config.dictConfig(yaml.load(open(self._logger_configuration_file, 'r')))

        self.logger = logging.getLogger(__name__)
        self.logger.info("Logger got initialized.")

        self.cache["messages"] = []
        self.cache["version"] = "3.1"
        self.modules = {}

        self.addon = Addon(self)
        self.actor = ActorCore(self)
        self.sensor = SensorCore(self)
        self.brewing = BrewingCore(self)
        self.fermentation = FermentationCore(self)
        self._app = Flask(__name__)
        self._app.secret_key = 'Cr4ftB33rP1'
        self._app.json_encoder = ComplexEncoder
        self._socketio = SocketIO(self._app, json=json, logging=False)

        @self._app.route('/')
        def index():
            return redirect('ui')

    def run(self):
        self.__init_db()
        self.loadPlugins()
        self.addon.init()
        self.sensor.init()
        self.actor.init()
        self.beep()
        try:
            port = int(cbpi.get_config_parameter('port', '5000'))
        except ValueError:
            port = 5000
        self.logger.info("port [%s]", port)
        self._socketio.run(self._app, host='0.0.0.0', port=port)

    def beep(self):
        self.buzzer.beep()

    def sleep(self, seconds):
        self._socketio.sleep(seconds)

    def notify(self, headline, message, type="success", timeout=5000):
        msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
        self.ws_emit("NOTIFY", msg)

    def ws_emit(self, key, data):
        self._socketio.emit(key, data, namespace='/brew')

    def __init_db(self, ):

        with self._app.app_context():
            db = self.get_db()
            try:
                with self._app.open_resource('../../config/schema.sql', mode='r') as f:
                    db.cursor().executescript(f.read())
                db.commit()
            except Exception as e:
                self.logger.error(e)
                pass

    def nocache(self, view):
        @wraps(view)
        def no_cache(*args, **kwargs):
            response = make_response(view(*args, **kwargs))
            response.headers['Last-Modified'] = datetime.now()
            response.headers[
                'Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
            response.headers['Pragma'] = 'no-cache'
            response.headers['Expires'] = '-1'
            return response

        return update_wrapper(no_cache, view)

    def get_db(self):
        db = getattr(g, '_database', None)
        if db is None:
            def dict_factory(cursor, row):
                d = {}
                for idx, col in enumerate(cursor.description):
                    d[col[0]] = row[idx]
                return d

            db = g._database = sqlite3.connect('craftbeerpi.db')
            db.row_factory = dict_factory
        return db

    def add_cache_callback(self, key, method):
        method.callback = True
        self.cache[key] = method

    def get_config_parameter(self, key, default):
        cfg = self.cache["config"].get(key)
        if cfg is None:
            return default
        else:
            return cfg.value

    def emit(self, key, **kwargs):

        if self.eventbus.get(key) is not None:
            for value in self.eventbus[key]:
                if value["async"] is False:
                    value["function"](**kwargs)
                else:
                    t = self.cbpi._socketio.start_background_task(target=value["function"], **kwargs)

    def loadPlugins(self):
        for filename in os.listdir("./modules/plugins"):

            if os.path.isdir("./modules/plugins/" + filename) is False:
                continue
            try:
                self.modules[filename] = import_module("modules.plugins.%s" % (filename))
            except Exception as e:
                self.logger.error(e)
                self.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)

cbpi = CraftBeerPI()
addon = cbpi.addon