#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2024 Qindel Formación y Servicios S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
#      may be used to endorse or promote products derived from this software
#      without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Ramón M. Gómez, ramongomez at us dot es
@author: Natalia Serrano, nserrano at qindel dot com
"""

import base64
#import threading
#import time
import os
import signal
import subprocess
from pathlib import Path
from urllib.parse import unquote

#from opengnsys import operations
from opengnsys.log import logger
from opengnsys.workers import ogLiveWorker

# Check authorization header decorator
def check_secret (fnc):
    """
    Decorator to check for received secret key and raise exception if it isn't valid.
    """
    def wrapper (*args, **kwargs):
        return fnc (*args, **kwargs)
        #try:
        #    this, path, get_params, post_params, server = args
        #    # Accept "status" operation with no arguments or any function with Authorization header
        #    if fnc.__name__ == 'process_status' and not get_params:
        #        return fnc (*args, **kwargs)
        #    elif this.random == server.headers['Authorization']:
        #        return fnc (*args, **kwargs)
        #    else:
        #        raise Exception ('Unauthorized operation')
        #except Exception as e:
        #    logger.error (str (e))
        #    raise Exception (e)

    return wrapper

class ogAdmClientWorker (ogLiveWorker):
    name          = 'ogAdmClient'  # Module name
    #interface     = None  # Bound interface for OpenGnsys (el otro modulo lo usa para obtener .ip y .mac
    REST          = None  # REST object

    def onDeactivation (self):
        """
        Sends OGAgent stopping notification to OpenGnsys server
        """
        logger.debug ('onDeactivation')

    #def processClientMessage (self, message, data):
    #    logger.debug ('Got OpenGnsys message from client: {}, data {}'.format (message, data))

    #def onLogin (self, data):
    #    logger.warning ('in onLogin, should not happen')

    #def onLogout (self, user):
    #    logger.warning ('in onLogout, should not happen')

    #@check_secret
    #def process_reboot (self, path, get_params, post_params, server):
    #    """
    #    Launches a system reboot operation
    #    :param path:
    #    :param get_params:
    #    :param post_params:
    #    :param server: authorization header
    #    :return: JSON object {"op": "launched"}
    #    """
    #    logger.debug ('Received reboot operation')

    #    # Rebooting thread
    #    def rebt():
    #        operations.reboot()
    #    threading.Thread (target=rebt).start()
    #    return {'op': 'launched'}

    #@check_secret
    #def process_poweroff (self, path, get_params, post_params, server):
    #    """
    #    Launches a system power off operation
    #    :param path:
    #    :param get_params:
    #    :param post_params:
    #    :param server: authorization header
    #    :return: JSON object {"op": "launched"}
    #    """
    #    logger.debug ('Received poweroff operation')

    #    # Powering off thread
    #    def pwoff():
    #        time.sleep (2)
    #        operations.poweroff()
    #    threading.Thread (target=pwoff).start()
    #    return {'op': 'launched'}










    ## process_*        are invoked from opengnsys/httpserver.py:99 "data = module.processServerMessage (path, get_params, post_params, self)" (via opengnsys/workers/server_worker.py)
    ## process_client_* are invoked from opengnsys/service.py:123   "v.processClientMessage (message, json.loads (data))"                      (via opengnsys/workers/server_worker.py)

    def ejecutaArchivo (self,fn):
        logger.debug ('fn ({})'.format (fn))

        ## in the "file" there's not just some bash, but a sequence of parameters such as "nfn=Function\rparam1=foo\rparam2=bar"
        buffer = subprocess.run (['cat', fn], capture_output=True).stdout.strip().decode ('utf-8')
        logger.debug ('buffer ({})'.format (buffer.replace ('\r', '\\r')))    ## change \r so as not to mess with the log
        if buffer:
            for l in buffer.split ('@'):
                if not len (l): continue
                logger.debug ('line ({})'.format (l))
                ## at this point, an option would be fire up a curl to localhost, but we can also parse the params and locally call the desired function:
                post_params = {}
                for param in l.split ("\r"):
                    k, v = param.split ('=')
                    post_params[k] = v
                logger.debug ('post_params "{}"'.format (post_params))

                func_name = post_params.pop ('nfn', None)
                if func_name is None:
                    logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
                    break
                func = getattr (self, 'process_' + func_name)
                ## func is already a ref to self.func, so we don't have to call self.func(...) or func(self, ...)

                logger.debug ('calling function "{}" with post_params "{}"'.format (func_name, post_params))
                output = func ([], {}, post_params, None)
                logger.debug ('output "{}"'.format (output))
                if not output:
                    logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
                    break

    def inclusionCliente (self):
        cfg = self.LeeConfiguracion()
        if not cfg:
            logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
            logger.warning ('Ha ocurrido algún problema en el proceso de inclusión del cliente')
            logger.error ('LeeConfiguracion() failed')
            return False
        res = self.enviaMensajeServidor ('InclusionCliente', { 'cfg': self.cfg2obj (cfg) })
        logger.debug ('res ({})'.format (res))

        ## RESPUESTA_InclusionCliente
        if (type (res) is not dict or 0 == res['res']) :
            logger.error ('Ha ocurrido algún problema en el proceso de inclusión del cliente')
            return False

        if (not res['ido'] or not res['npc']):
            logger.error ('Se han recibido parámetros con valores no válidos')
            return False

        self.idordenador     = res['ido']   ## Identificador del ordenador
        self.nombreordenador = res['npc']   ## Nombre del ordenador
        self.cache           = res['che']   ## Tamaño de la caché reservada al cliente
        self.idproautoexec   = res['exe']   ## Procedimento de inicio (Autoexec)
        self.idcentro        = res['idc']   ## Identificador de la Unidad Organizativa
        self.idaula          = res['ida']   ## Identificador del aula

        return True

    def cuestionCache (self):
        return True         ## ogAdmClient.c:425

    def autoexecCliente (self):
        res = self.enviaMensajeServidor ('AutoexecCliente', { 'exe': self.idproautoexec })
        logger.debug ('res ({})'.format (res))

        if (type (res) is not dict):
            logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
            logger.error ('Ha ocurrido algún problema al recibir una petición de comandos o tareas pendientes desde el Servidor de Administración')
            return False

        ## RESPUESTA_AutoexecCliente
        if (type (res) is not dict or 0 == res['res']) :
            logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
            return False

        logger.info (res)
        res = self.enviaMensajeServidor ('enviaArchivo', { 'nfl': res['nfl'] })
        if (type (res) is not dict):
            logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
            logger.error ('Ha ocurrido algún problema al recibir un archivo por la red')
            return False
        logger.debug (f'res ({res})')

        fileautoexec = '/tmp/_autoexec_{}'.format (self.IPlocal)
        logger.debug ('fileautoexec ({})'.format (fileautoexec))
        with open (fileautoexec, 'w') as fd:
            fd.write (base64.b64decode (res['contents']).decode ('utf-8'))

        self.ejecutaArchivo (fileautoexec)

        return True

    def comandosPendientes (self):
        while (True):
            res = self.enviaMensajeServidor ('ComandosPendientes')   ## receives just one command
            if (type (res) is not dict):
                logger.error ('Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración')
                logger.error ('Ha ocurrido algún problema al recibir una petición de comandos o tareas pendientes desde el Servidor de Administración')
                return False

            logger.info (res)
            if ('NoComandosPtes' == res['nfn']):
                break

            ## TODO manage the rest of cases... we might have to do something similar to ejecutaArchivo
            #if (!gestionaTrama (ptrTrama)){   // Análisis de la trama
            #    logger.error ('Ha ocurrido algún problema al procesar la trama recibida')
            #    return False
            #}
            ## ATM let's just return false to avoid a possible infinite loop
            return False

        return True

    def procesaComandos (self):
        res = self.enviaMensajeServidor ('DisponibilidadComandos', { 'tpc': 'OPG' })     ## Activar disponibilidad
        logger.debug ('res ({})'.format (res))

        if (type (res) is not dict):
            logger.error ('Ha ocurrido algún problema al enviar una petición de comandos interactivos al Servidor de Administración')
            return False

        logger.info ('Disponibilidad de comandos activada')     ## Disponibilidad de cliente activada

        ## we now return true and the outer agent code gets to wait for requests from outside
        ## TODO thing is, ogAdmClient always calls comandosPendientes() after every received request. How do we do that here?
        #
        #ptrTrama=recibeMensaje (&socket_c);
        #if (!ptrTrama){
        #    errorLog (modulo,46,FALSE);     'Ha ocurrido algún problema al recibir un comando interactivo desde el Servidor de Administración'
        #    return;
        #}
        #close (socket_c);
        #if (!gestionaTrama (ptrTrama)){   // Análisis de la trama
        #    errorLog (modulo,39,FALSE);     'Ha ocurrido algún problema al procesar la trama recibida'
        #    return;
        #}
        #if (!comandosPendientes (ptrTrama)){
        #    errorLog (modulo,42,FALSE);     'Ha ocurrido algún problema al enviar una petición de comandos o tareas pendientes al Servidor de Administración'
        #}

    def onActivation (self):
        super().onActivation (run_monitoring_thread=True)
        logger.info ('Inicio de sesion')
        logger.info ('Abriendo sesión en el servidor de Administración')
        if (not self.inclusionCliente()):
            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')

        logger.info ('Cliente iniciado')
        logger.info ('Procesando caché')
        if not self.cuestionCache():
            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')

        if self.idproautoexec > 0:
            logger.info ('Ejecución de archivo Autoexec')
            if not self.autoexecCliente():
                raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')

        logger.info ('Procesa comandos pendientes')
        if not self.comandosPendientes():
            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')

        logger.info ('Acciones pendientes procesadas')

        self.muestraMenu()
        self.procesaComandos()

        logger.info ('onActivation ok')

    @check_secret
    def process_status (self, path, get_params, post_params, server):
        logger.debug ('in process_status, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        cfg = self.LeeConfiguracion()
        thr_status = {}
        for k in self.thread_list:
            thr_status[k] = {
                'running': self.thread_list[k]['running'],
                'result': self.thread_list[k]['result'],
            }
        return {
            'nfn': 'RESPUESTA_status',
            'mac': self.mac,
            'ip': self.IPlocal,
            'cfg': self.cfg2obj (cfg),
            'threads': thr_status,
        }

    @check_secret
    def process_popup (self, path, get_params, post_params, server):
        logger.debug ('in process_popup, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        logger.debug ('type(post_params) "{}"'.format (type (post_params)))
        ## in process_popup, should not happen, path "[]" get_params "{}" post_params "{'title': 'mi titulo', 'message': 'mi mensaje'}" server "<opengnsys.httpserver.HTTPServerHandler object at 0x7fa788cb8fa0>"
        ## type(post_params) "<class 'dict'>"
        return {'debug':'test'}

    def do_Actualizar (self, post_params):
        self.muestraMensaje (1)
        #if !comandosPendientes: error 84 'Ha ocurrido algún problema al reiniciar la sesión del cliente'
        cfg = self.LeeConfiguracion()
        if not cfg:
            logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
            logger.error ('LeeConfiguracion() failed')
            return {}

        cmd = {
            'nfn': 'RESPUESTA_Actualizar',
            'cfg': self.cfg2obj (cfg),
        }
        self.muestraMenu()
        return self.respuestaEjecucionComando (cmd, 0)

    def do_Comando (self, post_params):
        for k in ['nfn', 'ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        ids = post_params['ids']

        try:
            self.interfaceAdmin (nfn)
            herror = 0
        except:
            logger.warning ('Error al ejecutar el comando')
            herror = 1

        cmd = {
            'nfn': 'RESPUESTA_Comando',
        }
        return self.respuestaEjecucionComando (cmd, herror, ids)

    def do_ConsolaRemota (self, post_params):
        for k in ['nfn', 'scp']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        scp = unquote (post_params['scp'])
        filescript = f'/tmp/_script_{self.IPlocal}'
        ecosrc = f'/tmp/_econsola_{self.IPlocal}'
        ecodst = f'/tmp/_Seconsola_{self.IPlocal}'    ## Nombre que tendra el archivo en el Servidor

        with open (filescript, 'w') as fd:
            fd.write (scp)

        try:
            self.interfaceAdmin (nfn, [filescript, ecosrc])
            ecosrc_contents = Path (ecosrc).read_bytes()
        except:
            logger.error ('Error al ejecutar el comando')
            return {}

        logger.debug ('sending recibeArchivo to server')
        res = self.enviaMensajeServidor ('recibeArchivo', { 'nfl': ecodst, 'contents': base64.b64encode (ecosrc_contents).decode ('utf-8') })
        logger.debug (res)
        if not res:
            logger.error ('Ha ocurrido algún problema al enviar un archivo por la red')

        return {}

    def do_Apagar (self, post_params):
        for k in ['nfn', 'ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        ids = post_params['ids']

        try:
            self.interfaceAdmin (nfn)
            herror = 0
        except:
            logger.warning ('Error al ejecutar el comando')
            herror = 1

        cmd = {
            'nfn': 'RESPUESTA_Apagar',
        }
        return self.respuestaEjecucionComando (cmd, herror, ids)

    def do_Reiniciar (self, post_params):
        for k in ['nfn', 'ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        ids = post_params['ids']

        try:
            self.interfaceAdmin (nfn)
            herror = 0
        except:
            logger.warning ('Error al ejecutar el comando')
            herror = 1

        cmd = {
            'nfn': 'RESPUESTA_Reiniciar',
        }
        return self.respuestaEjecucionComando (cmd, herror, ids)

    def do_IniciarSesion (self, post_params):
        for k in ['nfn', 'dsk', 'par', 'ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        dsk = post_params['dsk']
        par = post_params['par']
        ids = post_params['ids']

        try:
            self.interfaceAdmin (nfn, [dsk, par])
            herror = 0
        except:
            logger.warning ('Error al ejecutar el comando')
            herror = 1

        cmd = {
            'nfn': 'RESPUESTA_IniciarSesion',
        }
        return self.respuestaEjecucionComando (cmd, herror, ids)

    def do_EjecutarScript (self, post_params):
        for k in ['nfn', 'scp', 'ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        nfn = post_params['nfn']
        scp = unquote (post_params['scp'])
        ids = post_params['ids']

        self.muestraMensaje (8)

        filescript = f'/tmp/_script_{self.IPlocal}'     ## Nombre del archivo de script
        with open (filescript, 'w') as fd:
            fd.write (scp)

        try:
            self.interfaceAdmin (nfn, [filescript])
            self.muestraMensaje (22)
            herror = 0
        except:
            logger.warning ('Error al ejecutar el comando')
            self.muestraMensaje (21)
            herror = 1

        ## Toma configuración de particiones
        cfg = self.LeeConfiguracion()
        if not cfg:
            logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
            herror = 36

        #herror=ejecutarCodigoBash(scp);       ## ogAdmClient.c:2004

        cmd = {
            'nfn': 'RESPUESTA_EjecutarScript',
            'cfg': self.cfg2obj (cfg),
        }
        self.muestraMenu()
        return self.respuestaEjecucionComando (cmd, herror, ids)

    def process_Actualizar (self, path, get_params, post_params, server):
        logger.debug ('in process_Actualizar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('Actualizar', self.do_Actualizar, args=(post_params,))

    def process_Purgar (self, path, get_params, post_params, server):
        logger.debug ('in process_Purgar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        os.kill (os.getpid(), signal.SIGTERM)
        return {}
        #exit (0)      ## ogAdmClient.c:905

    def process_Comando (self, path, get_params, post_params, server):
        logger.debug ('in process_Comando, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('Comando', self.do_Comando, args=(post_params,))

    def process_Sondeo (self, path, get_params, post_params, server):
        logger.debug ('in process_Sondeo, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return {}      ## ogAdmClient.c:920

    def process_ConsolaRemota (self, path, get_params, post_params, server):
        logger.debug ('in process_ConsolaRemota, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('ConsolaRemota', self.do_ConsolaRemota, args=(post_params,))

    def process_Arrancar (self, path, get_params, post_params, server):
        logger.debug ('in process_Arrancar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))

        for k in ['ids']:
            if k not in post_params:
                logger.error (f'required parameter ({k}) not in POST params')
                return {}

        ids = post_params['ids']

        cmd = {
            'nfn': 'RESPUESTA_Arrancar',
            'tpc': 'OPG',
        }
        return self.respuestaEjecucionComando (cmd, 0, ids)

    def process_Apagar (self, path, get_params, post_params, server):
        logger.debug ('in process_Apagar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('Apagar', self.do_Apagar, args=(post_params,))

    def process_Reiniciar (self, path, get_params, post_params, server):
        logger.debug ('in process_Reiniciar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('Reiniciar', self.do_Reiniciar, args=(post_params,))

    def process_IniciarSesion (self, path, get_params, post_params, server):
        logger.debug ('in process_IniciarSesion, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('IniciarSesion', self.do_IniciarSesion, args=(post_params,))

    def process_EjecutarScript (self, path, get_params, post_params, server):
        logger.debug ('in process_EjecutarScript, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return self._long_running_job ('EjecutarScript', self.do_EjecutarScript, args=(post_params,))

    def process_EjecutaComandosPendientes (self, path, get_params, post_params, server):
        logger.debug ('in process_EjecutaComandosPendientes, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        return {'true':'true'}         ## ogAdmClient.c:2138

    ## curl --insecure -X POST --data '{"job_id":"foo"}' https://192.168.2.199:8000/ogAdmClient/KillJob
    def process_KillJob (self, path, get_params, post_params, server):
        logger.debug ('in process_KillJob, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
        jid = post_params['job_id']
        r = self.killer (jid)
        logger.debug (f'r bef ({r})')
        r.update ({ 'nfn':'RESPUESTA_KillJob', 'job':jid })
        logger.debug (f'r aft ({r})')
        return r
