import os
import json
import threading
import logging
import flask
import datetime
from werkzeug.serving import make_server
from attributedict.collections import AttributeDict
from flask_babel import Babel, _
from ..utils import broadcast, common, config, img_utils, time_utils
from .. import heating, sensing, forecasting, scheduling
from ..api_hooks import svc_urls


_logger = logging.getLogger('HelWebSvc')


class CustomJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.timedelta):
            return time_utils.format_timedelta(obj, False)
        elif isinstance(obj, datetime.datetime):
            return time_utils.format(obj)
        elif isinstance(obj, datetime.time):
            return time_utils.format_time(obj)
        elif isinstance(obj, heating.LpdDeviceState) or\
                isinstance(obj, heating.HeatingTask) or\
                isinstance(obj, sensing.TemperatureState) or\
                isinstance(obj, common.CpuInfo) or\
                isinstance(obj, common.ProcessInfo) or\
                isinstance(obj, common.StorageInfo):
            return obj.to_dict()
        return json.JSONEncoder.default(self, obj)


def _return_response(rsp):
    """
    Unless the client requested JSON output, we'll nicely format the response
    using the status_response template. The response must be a jsonify-able object
    with at least a "success" attribute.
    """
    if flask.request.args.get('json', default=False, type=bool):
        return flask.jsonify(rsp)
    else:
        return flask.render_template('status_response.html', rsp=rsp)


def _render_status_response_html(rsp):
    """Render a function's response (see common.make_response()) as HTML."""
    # Pretty/indented JSON dump
    def _jd(o):
        return json.dumps(o, indent=2, sort_keys=True, cls=CustomJSONEncoder)

    if hasattr(rsp, 'message'):
        txt = rsp.message
    elif hasattr(rsp, 'output'):
        txt = rsp.output
    elif hasattr(rsp, 'log'):
        txt = rsp.log
    elif hasattr(rsp, 'broadcasts'):
        txt = _jd(rsp.broadcasts)
    elif hasattr(rsp, 'temperature_states'):
        txt = _jd(rsp.temperature_states)
    elif hasattr(rsp, 'heating_jobs'):
        txt = _jd(rsp.heating_jobs)
    elif hasattr(rsp, 'non_heating_jobs'):
        txt = _jd(rsp.non_heating_jobs)
    elif hasattr(rsp, 'district_heating_status'):
        txt = _jd(rsp.district_heating_status)
    elif hasattr(rsp, 'status'):
        txt = _jd(rsp.status)
    else:
        txt = _jd(rsp)
    return u'<pre><code>{:s}</code></pre>'.format(
            flask.escape(txt))


# def not_yet_implemented():
#     rsp = common.make_response(False, message=_('Not yet implemented'))
#     return _return_response(rsp)


def create_app():
    websvc = flask.Flask('Helheimr Webservice',
                         static_folder=os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')),
                         template_folder=os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')))
    websvc.jinja_env.globals.update(render_status_response=_render_status_response_html)
    websvc.jinja_env.globals.update(format_time=time_utils.format_time)
    websvc.jinja_env.globals.update(format_timedelta=lambda td: time_utils.format_timedelta(td, small_space=True))
    websvc.jinja_env.globals.update(format_datetime=lambda dt: time_utils.format(dt, fmt='%Y-%m-%d %H:%M:%S %Z'))
    websvc.jinja_env.globals.update(format_seconds=lambda sec: time_utils.format_timedelta(datetime.timedelta(seconds=sec), small_space=True))

    websvc.config['BABEL_TRANSLATION_DIRECTORIES'] = config.HELHEIMR_HEATING_LOCALE_DIR
    websvc.config['BABEL_DEFAULT_LOCALE'] = 'de'
    websvc.config['LANGUAGES'] = ['en', 'de']
    websvc.config['DEBUG'] = False

    websvc.json_encoder = CustomJSONEncoder

    babel = Babel(websvc)

    @babel.localeselector
    def get_locale():
        if flask.request.args.get('lang', default=None):
            lang = flask.request.args.get('lang')
            if lang not in websvc.config['LANGUAGES']:
                lang = websvc.config['LANGUAGES'][0]
            flask.session['language'] = lang
        try:
            language = flask.session['language']
        except KeyError:
            language = None
        if language is not None:
            return language
        return flask.request.accept_languages.best_match(websvc.config['LANGUAGES'])

    ###############################################################################
    ###############################################################################
    # Landing page & error handling

    @websvc.route('/')
    def req_index():
        """Index/main."""
        _logger.info('Serving index.')
        return flask.render_template('index.html', svc_urls=svc_urls,
                                     central_heating_status=heating.central_heater.status_dict)

    @websvc.errorhandler(404)
    def handle_404(e):
        """Handle invalid requests."""
        _logger.warning('Invalid URL requested.')
        return flask.render_template('404.html'), 404

    @websvc.errorhandler(500)
    def handle_500(e):  # pragma: no cover
        """Handle internal error."""
        _logger.error('Server error occured.')
        return flask.render_template('500.html'), 500

    @websvc.after_request
    def after_request(response):
        """Disable caching"""
        response.headers["Cache-Control"] = "no-store"
        return response

    ###############################################################################
    ###############################################################################
    # Heating jobs
    @websvc.route(svc_urls.heating_jobs_add, methods=['GET', 'POST'])
    def req_heating_jobs_add():
        _logger.info('Serving heating job ADD')
        if flask.request.method == 'POST':
            # Temperature and hysteresis can't be set via the web interface
            # This is on purpose (we never used it in home automation v1)
            start_hour = flask.request.values.get('start_hour', type=int)
            start_min = flask.request.values.get('start_minute', type=int)
            d_hours = flask.request.values.get('duration_hours', type=int)
            d_mins = flask.request.values.get('duration_minutes', type=int)
            creator = flask.request.values.get('creator', type=str, default='')
            if len(creator.strip()) == 0:
                creator = 'webinterface'
            temperature = flask.request.values.get('temperature', type=float, default=None)

            cfg = AttributeDict({
                'day_interval': 1,
                'at': f'{start_hour:02d}:{start_min:02d}',
                'duration': f'{d_hours:02d}:{d_mins:02d}',
                'temperature': temperature,
                # 'hysteresis': None,
                'created_by': creator
            })
            try:
                job = scheduling.PeriodicHeatingJob.from_attrdict(cfg)
                rsp = scheduling.scheduler.schedule_job(job, get_locale())
            except scheduling.ScheduleError as e:
                return _return_response(common.make_response(False, message=_('Exception:') + f' {e}'))

            if not rsp.success:
                return _return_response(rsp)

            if scheduling.scheduler.serialize_jobs():
                return _return_response(common.make_response(True,
                                                             message=_('Permanently added new heating job.')))
            else:
                return _return_response(common.make_response(False,
                                                             message=_('Added new heating job temporarily, because the new schedule could not be saved to disk.')))
        else:
            return flask.redirect(svc_urls.heating_ctrl)

    @websvc.route(svc_urls.heating_jobs_delete)
    def req_heating_jobs_delete():
        return _req_delete_scheduled_job()

    @websvc.route(svc_urls.heating_jobs_list)
    def req_heating_jobs_list():
        _logger.info('Serving heating job LIST')
        jobs = scheduling.scheduler.job_list(get_locale())
        return _return_response(common.make_response(True, heating_jobs=jobs.heating_jobs))

    @websvc.route(svc_urls.heating_jobs_pause)
    def req_heating_jobs_pause():
        _logger.info('Serving heating job PAUSE')
        user = flask.request.args.get('user', default='webinterface', type=str)
        is_paused = heating.central_heater.toggle_pause(user)
        return _return_response(common.make_response(True,
                                                     message=_('Heating jobs are disabled.') if is_paused else _('Heating jobs are enabled.')))

    ###############################################################################
    ###############################################################################
    # Local heating control & log
    @websvc.route(svc_urls.heating_ctrl)
    def req_heating_ctrl():
        _logger.info('Serving central heating management page.')
        return flask.render_template('mgmt_heating.html',
                                     svc_urls=svc_urls,
                                     central_heating_status=heating.central_heater.status_dict,
                                     temperature_states=sensing.sensor_controller.thermometer_states_dict,
                                     scheduled_jobs=scheduling.scheduler.job_list(get_locale()))

    @websvc.route(svc_urls.heating_turn_on)
    def req_heating_turn_on():
        _logger.info('Serving TURN ON heater')
        dstr = flask.request.args.get('duration', default=None, type=str)
        if dstr is not None:
            duration = time_utils.parse_duration_input_str(dstr)
            if duration is None:
                return _return_response(common.make_response(False,
                                                             message=_('Invalid duration string provided.')))
        else:
            duration = None
        user = flask.request.args.get('user', default='webinterface', type=str)
        temperature = flask.request.args.get('temperature', default=None, type=float)
        hysteresis = flask.request.args.get('hysteresis', default=None, type=float)
        task = heating.HeatingTask(request_type=heating.RequestType.MANUAL,
                                   user=user, duration=duration,
                                   temperature_target=temperature,
                                   temperature_hysteresis=hysteresis)
        rsp = heating.central_heater.start_heating(task, get_locale())
        return _return_response(rsp)

    @websvc.route(svc_urls.heating_turn_off)
    def req_heating_turn_off():
        _logger.info('Serving TURN OFF heater')
        user = flask.request.args.get('user', default='webinterface', type=str)
        rsp = heating.central_heater.stop_heating(user)
        return _return_response(rsp)

    ###############################################################################
    ###############################################################################
    # District heating

    @websvc.route(svc_urls.district_heating_status)
    def req_district_heating_status():
        _logger.info('Serving STATUS district heating')
        rsp = heating.district_heater.query_heating(get_locale())
        if rsp.success and not flask.request.args.get('json', default=False, type=bool):
            return flask.render_template('district_heating_status.html',
                                         dhstat=rsp.district_heating_status)
        else:
            return _return_response(rsp)

    @websvc.route(svc_urls.district_heating_turn_on)
    def req_district_heating_turn_on():  # pragma: no cover
        _logger.info('Serving district heating TURN ON')
        inlet = flask.request.args.get('inlet', default=None, type=int)
        rsp = heating.district_heater.start_heating(inlet, get_locale())
        return _return_response(rsp)

    ###############################################################################
    ###############################################################################
    # Temperature log

    @websvc.route(svc_urls.temperature_query)
    def req_temperature_current():
        """Returns the current temperature readings."""
        rsp = sensing.sensor_controller.query_thermometer_states()
        return _return_response(rsp)

    @websvc.route(svc_urls.temperature_log)
    def req_temperature_log():
        """
        Returns the temperature log from the current time period defined
        by 'duration'.
        """
        duration = time_utils.parse_duration_input_str(flask.request.args.get('duration', default=None, type=str))
        _logger.info(f'Sending temperature log (duration: {duration}).')
        if duration is None:
            return _return_response(common.make_response(False,
                                                         message=_('Invalid duration string provided.')))
        now = time_utils.dt_now_local()
        rsp = sensing.temperature_log.query(now - duration)
        return _return_response(rsp)

    @websvc.route(svc_urls.temperature_plot)
    def req_temperature_plot():
        _logger.info('Serving temperature plot.')
        dstr = flask.request.args.get('duration', default='1d', type=str)
        duration = time_utils.parse_duration_input_str(dstr)
        dt_from = time_utils.dt_now_local() - (datetime.timedelta(days=1) if duration is None else duration)
        img = sensing.temperature_plot(dt_from, None, language=get_locale(), font_size=14)
        mime_type = 'jpeg'  # optionally: 'png'
        b64 = img_utils.b64encode_image(img, format=mime_type)
        if flask.request.args.get('json', default=False, type=bool):
            return flask.jsonify(common.make_response(True, image_base64=b64,
                                                      mime_type=mime_type))
        else:
            return flask.render_template('temperature_plot.html',
                                         svc_urls=svc_urls,
                                         duration_str=dstr,
                                         image_format=mime_type,
                                         image=b64)

    @websvc.route(svc_urls.temperature_reload_sensors)
    def req_temperature_reload_sensors():
        _logger.info('Serving temperature sensor reload')
        return _return_response(sensing.sensor_controller.reload_config())

    ###############################################################################
    ###############################################################################
    # Service & system status queries

    @websvc.route(svc_urls.status_summary)
    def req_status_summary():
        """Service status overview."""
        _logger.info('Serving status summary.')
        #FIXME lang = get_locale() and use for the following queries! (check if we translate the sensor names or not)
        rsp_temp = sensing.sensor_controller.query_thermometer_states()
        status = {
            # Central heating status
            'central_heating_status': heating.central_heater.status_dict,
            'temperature_states': rsp_temp.temperature_states if rsp_temp.success else None
        }
        return _return_response(common.make_response(True, status=status))

    @websvc.route(svc_urls.status_details)
    def req_status_details():
        """Detailed service status."""
        _logger.info('Serving status details.')
        rsp_temp = sensing.sensor_controller.query_thermometer_states()
        rsp_zb = sensing.sensor_controller.query_zigbee_status()
        detailed_status = {
            # Central heating status
            'central_heating_status': heating.central_heater.status_dict,
            # Scheduled jobs (synonym for tasks/programs)
            'scheduled_jobs': scheduling.scheduler.job_list(get_locale(), use_code_tags=True),
            # ZigBee temperature sensors
            'zigbee_state': rsp_zb.zigbee_state if rsp_zb.success else None,
            'temperature_states': rsp_temp.temperature_states if rsp_temp.success else None,
            # General system overview/stats
            'system_status': common.system_status()
        }
        if flask.request.args.get('json', default=False, type=bool):
            return _return_response(common.make_response(True, status=detailed_status))
        else:
            return flask.render_template('detailed_status.html', svc_urls=svc_urls, **detailed_status)

    ###############################################################################
    ###############################################################################
    # Shutdown/restart/reboot (service and/or PC)

    @websvc.route(svc_urls.service_restart)
    def req_svc_restart():  # pragma: no cover
        _logger.info('Initializing SERVICE RESTART via web service request.')
        rsp = common.shell_restart_service()
        if rsp.success:
            return _return_response(common.make_response(True,
                                    message=_('Service will be restarted now.')))
        else:
            return _return_response(common.make_response(False,
                                    message=_('Could not restart service:') + f'\n\n{rsp.output}'))

    @websvc.route(svc_urls.service_update)
    def req_svc_update():  # pragma: no cover
        _logger.info('Updating repository via web service request.')
        rsp = common.shell_update_repository()
        if not rsp.success:
            return _return_response(common.make_response(False,
                                    message=_('Could not update repository:') + f'\n\n{rsp.output}'))
        return req_svc_restart()

    @websvc.route(svc_urls.service_ping)
    def req_svc_ping():
        _logger.debug('Serving ping')
        return _return_response(common.make_response(True, message='pong'))

    @websvc.route(svc_urls.system_reboot)
    def req_sys_reboot():  # pragma: no cover
        """Reboot the PC."""
        _logger.info('Initializing SYSTEM REBOOT via web service request.')
        rsp = common.shell_shutdown('-r', '+1')
        if rsp.success:
            msg = _('Initialized reboot request.')
        else:
            msg = _('Cannot reboot system:') + f'\n\n{rsp.output}'
        return _return_response(common.make_response(rsp.success, message=msg))

    @websvc.route(svc_urls.system_shutdown)
    def req_sys_shutdown():
        """Power off the PC."""
        _logger.info('Initializing SYSTEM SHUTDOWN via web service request.')
        rsp = common.shell_shutdown('-h', '+1')
        if rsp.success:
            msg = _('Initialized system shutdown request.')
        else:
            msg = _('Cannot power off system:') + f'\n\n{rsp.output}'
        return _return_response(common.make_response(rsp.success, message=msg))

    ###############################################################################
    ###############################################################################
    # Query logs

    @websvc.route(svc_urls.broadcast_query)
    def req_broadcast_query():
        """Displays broadcasted (and not yet picked up) messages."""
        rcv = flask.request.args.get('receiver', default=None, type=str)
        if rcv == 'both':
            return _return_response(broadcast.peek_broadcasts(rcv))
        else:
            # The display and bot will query with the corresponding
            # receiver parameter. Thus, we can pop their messages.
            return _return_response(broadcast.pop_broadcasts(rcv))

    @websvc.route(svc_urls.broadcast_clear)
    def req_broadcast_clear():
        """Clears all stored broadcasts."""
        if broadcast.clear_broadcasts():
            return _return_response(common.make_response(True,
                                                         message=_('Cleared all broadcast messages.')))
        else:
            return _return_response(common.make_response(False,
                                                         message=_('Error while clearing broadcast messages, please check the log files.')))

    @websvc.route(svc_urls.service_log)
    def req_svc_log():
        """Displays the latest <num-lines> lines of the heating service log."""
        num_lines = flask.request.args.get('num-lines', default=200, type=int)
        _logger.info(f'Sending service log (num_lines parameter: {num_lines}).')
        if num_lines == 0:
            content = _('You selected 0 lines, adjust the "num-lines" parameter!')
            success = False
        elif num_lines < 0:
            with open(config.HELHEIMR_LOG_FILE_MAIN, 'r') as lf:
                lines = lf.readlines()
                content = ''.join(reversed(lines))
                success = True
        else:
            content = '\n'.join(reversed(common.tail(config.HELHEIMR_LOG_FILE_MAIN,
                                                     lines=num_lines, as_list=True)))
            success = True
        return _return_response(common.make_response(success, log=common.remove_ansi_escapes(content)))

    @websvc.route(svc_urls.ragnaroek_log)
    def req_ragnaroek_log():
        """Displays the latest <num-lines> lines of the 'ragnaroek' log (used by our bash-only services)."""
        num_lines = flask.request.args.get('num-lines', default=200, type=int)
        _logger.info(f'Sending system log (num_lines: {num_lines}).')
        if num_lines <= 0:
            return _return_response(common.make_response(False,
                                                         message=_('"num-lines" parameter must be > 0.')))
        else:
            rsp = common.shell_ragnaroek_log(num_lines)
            return _return_response(common.make_response(rsp.success,
                                                         log=common.remove_ansi_escapes(rsp.output)))

    @websvc.route(svc_urls.download_dbase)
    def req_download_dbase():
        """Allows downloading the system dbase (useful for future inspection)."""
        _logger.info('Sending sqlite database file.')
        dn = os.path.dirname(config.HELHEIMR_DATABASE)
        fn = os.path.basename(config.HELHEIMR_DATABASE)
        return flask.send_from_directory(dn, fn, as_attachment=True)

    @websvc.route(svc_urls.system_log)
    def req_sys_log():
        """Displays the latest <num-lines> lines of the system log."""
        num_lines = flask.request.args.get('num-lines', default=200, type=int)
        _logger.info(f'Sending system log (num_lines: {num_lines}).')
        if num_lines <= 0:
            return _return_response(common.make_response(False,
                                                         message=_('"num-lines" parameter must be > 0.')))
        else:
            rsp = common.shell_system_log(num_lines)
            return _return_response(common.make_response(rsp.success,
                                                         log=common.remove_ansi_escapes(rsp.output)))
        # return flask.Response(content, mimetype='text/plain')

    ###############################################################################
    ###############################################################################
    # Scheduler control:
    @websvc.route(svc_urls.scheduler_ctrl)
    def req_scheduler_ctrl():
        _logger.info('Serving scheduler control.')
        lang = get_locale()
        return flask.render_template('mgmt_scheduling.html',
                                     svc_urls=svc_urls,
                                     scheduled_jobs=scheduling.scheduler.job_list(lang),
                                     available_non_heating_jobs=scheduling.function_names_for_non_heating_jobs(),
                                     available_scheduling_units=scheduling.available_scheduling_units(lang),
                                     available_start_days=scheduling.available_weekly_start_days(lang))

    @websvc.route(svc_urls.scheduler_jobs_list)
    def req_scheduler_job_list():
        _logger.info('Serving scheduler job list')
        jobs = scheduling.scheduler.job_list(get_locale())
        return _return_response(common.make_response(True, non_heating_jobs=jobs.non_heating_jobs))

    @websvc.route(svc_urls.scheduler_jobs_delete)
    def req_scheduler_del():
        return _req_delete_scheduled_job()

    def _req_delete_scheduled_job():
        juid = flask.request.args.get('juid', default=None, type=int)
        _logger.info(f'Handle request to delete scheduled job with ID {juid}.')
        if juid is None:
            return _return_response(common.make_response(False, message=_('Job ID is missing for deletion.')))
        deleted = scheduling.scheduler.remove_job(juid)
        if deleted is None:
            return _return_response(common.make_response(False, message=_('Error while deleting job (%(uid)d).', uid=juid)))
        else:
            if scheduling.scheduler.serialize_jobs():
                return _return_response(common.make_response(True, message=_('Job (%(uid)d) has been deleted.', uid=juid)))
            else:
                return _return_response(common.make_response(False, message=_('Job (%(uid)d) has been deleted but job list could not be saved to disk.', uid=juid)))

    @websvc.route(svc_urls.scheduler_jobs_add, methods=['GET', 'POST'])
    def req_scheduler_add():
        _logger.info('Serving NON-HEATING job ADD')
        if flask.request.method == 'POST':
            # POSTs from the _form_add_non_heating_job will post
            # empty strings for 'at' and 'description', thus replace
            # those by actual None
            at = flask.request.values.get('at', type=str, default=None)
            if at is not None and len(at.strip()) == 0:
                at = None

            description = flask.request.values.get('description', type=str, default=None)
            if description is not None and len(description.strip()) == 0:
                description = None

            start_day = flask.request.values.get('start_day', type=str, default=None)
            if start_day is not None and len(start_day.strip()) == 0:
                start_day = None

            cfg = AttributeDict({
                'type': flask.request.values.get('type', type=str),
                'description': description,
                'interval': flask.request.values.get('interval', type=int, default=1),
                'unit': flask.request.values.get('unit', type=str, default=None),
                'start_day': start_day,
                'at': at
            })

            try:
                job = scheduling.NonHeatingJob.from_attrdict(cfg)
                rsp = scheduling.scheduler.schedule_job(job, get_locale())
            except scheduling.ScheduleError as e:
                return _return_response(common.make_response(False, message=_('Exception:') + f' {e}'))

            if not rsp.success:
                return _return_response(rsp)

            if scheduling.scheduler.serialize_jobs():
                return _return_response(common.make_response(True,
                                                             message=_('Permanently added new non-heating job.')))
            else:
                return _return_response(common.make_response(False,
                                                             message=_('Added new non-heating job temporarily, because the schedule could not be saved to disk.')))
        else:
            return flask.redirect(svc_urls.scheduler_ctrl)

    ###############################################################################
    ###############################################################################
    # Nice-to-have extra functionality

    @websvc.route(svc_urls.weather_forecast)
    def req_weather_forecast():
        """Returns the current weather forecast."""
        _logger.info('Serving weather forecast.')
        ws = forecasting.WeatherService(get_locale())
        return flask.render_template('forecast_weather.html', report=ws.report())

    return websvc


###############################################################################
###############################################################################
# Run web service as a separate thread

def _load_config():
    cfg = config.load_configuration(config.HELHEIMR_CONFIG_FILE_CONTROL)
    # Perform sanity check
    if len(cfg.web_service.host.strip()) == 0:
        # Assignment must use keys (attribute access yields shallow copies)
        cfg['web_service']['host'] = '0.0.0.0'
    return cfg


class ServerThread(threading.Thread):
    """Runs the web service as a separate join-/terminatable thread."""
    def __init__(self):
        threading.Thread.__init__(self)
        cfg = _load_config()
        self.port = cfg.web_service.port
        self.host = cfg.web_service.host
        self.websvc = create_app()
        self.websvc.secret_key = cfg.web_service.secret_key.encode('utf-8')
        self.srv = make_server(self.host, self.port, self.websvc)
        self.ctx = self.websvc.app_context()
        self.ctx.push()

    def run(self):
        _logger.info(f'Starting web service on {self.host}, listening on port {self.port}.')
        self.srv.serve_forever()

    def shutdown(self):
        _logger.info('Shutting down web service.')
        self.srv.shutdown()


_server_thread = None


def start_service():
    """Starts the web service. Must only be called once."""
    global _server_thread
    if _server_thread is not None:
        raise RuntimeError('Only a single web service instance is allowed to run.')
    _server_thread = ServerThread()
    _server_thread.daemon = True
    _server_thread.start()


def stop_service():
    """Stops the web service."""
    global _server_thread
    if _server_thread is not None:
        _server_thread.shutdown()
        _server_thread = None


def join_service():
    """Blocks until the web service has terminated."""
    global _server_thread
    if _server_thread is not None:
        _logger.info('Joining the web service thread.')
        _server_thread.join()
        _logger.info('Web service thread terminated.')
