<?php
// ********************************************************************
// Author: Juan Manuel Bardallo (UHU), Natalia Serrano (Qindel)
// Description: Updates UDS with the number of available computers depending on OG's calendars and other circumstances
// *********************************************************************

include_once("./controlacceso.php");
include_once("./includes/CreaComando.php");
include_once("./includes/timezone.php");
include_once("./includes/uds.php");
include_once("./clases/AdoPhp.php");

$OG_REST_URL = 'https://localhost/opengnsys/rest/';
$logfd = fopen ('/opt/opengnsys/log/uds-set-service-pool.log', 'a');
if (!$logfd) {
    error_log ("Can't open '/opt/opengnsys/log/uds-set-service-pool' for appending");
    exit (1);
}

/**
 *         do_log ($msg)
 * @brief  Writes to the log file
 * @param  string  message
 * @return int     number of bytes written, or false on failure
 */
function do_log ($msg) {
    global $logfd;
    $log_ts = date('Y/m/d h:i:s a', time());
    return fwrite ($logfd, sprintf ("%s %s\n", $log_ts, $msg));
}

/**
 *         uds_getHeaders ($auth_token, $scrambler)
 * @brief  Builds set of authentication headers from a couple of UDS params
 * @param  string  authentication token
 * @param  string  scrambler
 * @return array   headers
 */
function uds_getHeaders ($auth_token, $scrambler) {
    $headers = [
        'X-Auth-Token: ' . $auth_token,                  // Authentication token to access REST API of UDS -- MUST be present in all requests except login request
        'Content-Type: ' . 'application/json',
        'User-Agent: '   . 'UDS Test Linux Client 1.0',  // important to include "Linux" on UA to allow UDS to know OS. If not present, UDS will not know wich OS is connecting, and will not send the correct services
        'Scrambler: '    . $scrambler,                   // This header is a cryptographic key -- MUST BE INCLUDED in all requests except login request
    ];
    return $headers;
}

/**
 *         db_fetch_apikey()
 * @brief  Retrieves API key for the first user (assumed admin) from the OG database
 * @return string  api key
 */
function db_fetch_apikey() {
    global $cnx;
    $cmd = CreaComando ($cnx);
    if (!$cmd) { die ('ACCESS_ERROR'); }

    $cmd->texto = 'SELECT apikey FROM usuarios WHERE idusuario=1';
    $rs = new Recordset;
    $rs->Comando = &$cmd;

    if (!$rs->Abrir()) return (null);

    $rs->Primero();
    if ($rs->EOF) { return (null); }

    $k = $rs->campos['apikey'];
    $rs->Cerrar();
    return $k;
}

/**
 *         db_fetchCals()
 * @brief  Retrieves calendars from the database
 * @return array   calendars
 */
function db_fetchCals() {
    global $cnx;

    $tbl = array();
    $cmd = CreaComando ($cnx);
    if (!$cmd) { die ('ACCESS_ERROR'); }

    $cmd->texto = 'SELECT idcalendario, description, json_text FROM calendarios';
    $rs = new Recordset;
    $rs->Comando = &$cmd;

    if (!$rs->Abrir()) return ($tbl);

    $rs->Primero();
    if ($rs->EOF) { return ($tbl); }

    $id = $rs->campos['idcalendario'];
    $desc = $rs->campos['description'];
    $txt = $rs->campos['json_text'];
    $tbl[$id] = array (
        'id'   => $id,
        'desc' => $desc,
        'json' => json_decode($txt),
    );

    while (1) {
        $rs->Siguiente();
        if ($rs->EOF) { break; }

        $id = $rs->campos['idcalendario'];
        $desc = $rs->campos['description'];
        $txt = $rs->campos['json_text'];
        $tbl[$id] = array (
            'id'   => $id,
            'desc' => $desc,
            'json' => json_decode($txt),
        );
    }

    $rs->Cerrar();
    return $tbl;
}

/**
 *         uds_login ($auth, $username, $password)
 * @brief  Log into UDS
 * @param  string  UDS authenticator
 * @param  string  user
 * @param  string  password
 * @return object  response from the server, contains result, token, version and scrambler
 */
function uds_login ($auth, $username, $password) {
    global $UDS_REST_URL;
    $headers = [];

    $parameters = [
        'auth'     => $auth,
        'username' => $username,
        'password' => $password
    ];

    $payload = json_encode ($parameters);

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $UDS_REST_URL."auth/login");
    curl_setopt ($ch, CURLOPT_POST, 1);
    curl_setopt ($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, array ('Content-Type:application/json'));
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    // https://stackoverflow.com/questions/9183178/can-php-curl-retrieve-response-headers-and-body-in-a-single-request
    curl_setopt ($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$headers) {
        $len = strlen ($header);
        $header = explode (':', $header, 2);
        if (count ($header) < 2) { // ignore invalid headers
            return $len;
        }
        $headers[strtolower(trim($header[0]))][] = trim($header[1]);
        return $len;
    });

    // In real life you should use something like:
    // curl_setopt ($ch, CURLOPT_POSTFIELDS, http_build_query (array ('postvar1' => 'value1')));

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    if (preg_match ('/text\/html/', $headers['content-type'][0])) {
        do_log ('login API call returned HTML, not JSON');
        return null;
    }

    return json_decode ($json);
}

/**
 *         uds_getServiceInfo ($headers, $providerId, $serviceId)
 * @brief  Get service information from UDS
 * @param  array   UDS headers
 * @param  string  provider id
 * @param  string  service id
 * @return object  response from the server
 */
function uds_getServiceInfo ($headers, $providerId, $serviceId) {
    global $UDS_REST_URL;

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $UDS_REST_URL."providers/".$providerId."/services/".$serviceId);
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, $headers);

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    return json_decode ($json);
}

/**
 *         uds_getAllServicePools ($headers)
 * @brief  Get all service pools from UDS
 * @param  array   UDS headers
 * @return object  response from the server
 */
function uds_getAllServicePools ($headers) {
    global $UDS_REST_URL;

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $UDS_REST_URL."servicespools/overview");
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, $headers);

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    return json_decode ($json);
}

/**
 *         uds_setServicePool ($headers, $servicePool)
 * @brief  Writes service pool information to UDS
 * @param  array   UDS headers
 * @param  object  service pool details
 * @return object  response from the server
 */
function uds_setServicePool ($headers, $servicePool) {
    global $UDS_REST_URL;

    $payload = json_encode ($servicePool);

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $UDS_REST_URL."servicespools/".$servicePool->id);
    curl_setopt ($ch, CURLOPT_POST, 1);
    curl_setopt ($ch, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_setopt ($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, $headers);

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    return json_decode ($json);
}

/**
 *         og_getAula ($idCentro, $idAula)
 * @brief  Gets lab information from OG
 * @param  int     building id
 * @param  int     lab id
 * @return object  response from the server
 */
function og_getAula ($idCentro, $idAula) {
    global $OG_REST_URL;
    global $OG_REST_AUTH;

    $result = null;
    $url = $OG_REST_URL."ous/".$idCentro."/labs/".$idAula;

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $url);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json',
        'Authorization: '.$OG_REST_AUTH
    ]);


    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    $code = curl_getinfo ($ch, CURLINFO_RESPONSE_CODE);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    if ($code <= 201) {
        $result = json_decode ($json);
    }

    return $result;
}

/**
 *         og_getClientDiskConfig ($idCentro, $idAula, $idCliente)
 * @brief  Gets information about disks of a client from OG
 * @param  int     building id
 * @param  int     lab id
 * @param  int     client id
 * @return object  response from the server
 */
function og_getClientDiskConfig ($idCentro, $idAula, $idCliente) {
    global $OG_REST_URL;
    global $OG_REST_AUTH;

    $result = null;
    $url = $OG_REST_URL."ous/".$idCentro."/labs/".$idAula."/clients/".$idCliente."/diskcfg";

    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $url);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json',
        'Authorization: '.$OG_REST_AUTH
    ]);

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    $code = curl_getinfo ($ch, CURLINFO_RESPONSE_CODE);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("curl error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    if ($code <= 201) {
        $result = json_decode ($json);
    }

    return $result;
}

/**
 *         og_aula_is_remote ($cal_id)
 * @brief  Checks whether lab is remote right now
 * @param  int     calendar id
 * @return bool    whether lab is remote (true) or not (false)
 */
function og_aula_is_remote ($cal_id) {
    global $cals, $tz;

    if (!array_key_exists ($cal_id, $cals)) { return 0; }
    $ruleset = $cals[$cal_id]['json']->remotepc_calendar->ruleset;

    if (null === $cal_id or 0 === $cal_id) {
        return 0;
    }

    $dt = new DateTime ('now', new DateTimeZone ($tz));
    $dow = strtolower ($dt->format ('D'));  // textual representation of a day, three letters, 'Mon' through 'Sun'
    $hour = $dt->format ('G');              // 24-hour without leading zeros
    $ts = $dt->format ('U');                // unix epoch

    // primero recorremos todas las reglas mirando solo las que tienen remote=false (ie. presencial)
    // si estamos fuera de todos estos rangos, es que estamos en remoto: return 1
    // si estamos dentro de alguno de ellos, es que estamos en presencial: continuamos
    $presencial = 0;
    foreach ($ruleset as $r) {
        if (array_key_exists ('remote', $r) and $r->remote) { continue; }   // remote=true, no nos interesa ahora
        if (!array_key_exists ($dow, $r) or !$r->$dow) { continue; }        // si el día de hoy no está, o está pero es false, es que no es presencial: no nos interesa
        if ($hour < $r->from_hr or $hour > $r->to_hr) { continue; }         // miramos las horas. Si estamos fuera del rango, es que no es presencial: no nos interesa
        $presencial = 1;                                                    // si hemos llegado aqui, es que estamos en un rango presencial
        break;
    }

    if (0 == $presencial) {
        do_log ('Outside of "presencial" time slots, aula_is_remote: true');
        return 1;
    }

    // si llegamos aqui, es que estamos en uno de los rangos de presencial, pero puede haber excepciones
    // recorremos todas las reglas mirando las que tienen remote=true
    // si estamos en alguno de esos rangos, return 1
    foreach ($ruleset as $r) {
        if (!array_key_exists ('remote', $r) or !$r->remote) { continue; }   // remote no está presente o es falso, no nos interesa ahora
        if ($ts >= $r->from_ts and $ts <= $r->to_ts) {                       // estamos en un rango remoto
            do_log ('Inside "presencial" time slots but within an exception, aula_is_remote: true');
            return 1;
        }
    }

    do_log ('Inside "presencial" time slots and not within any exception, aula_is_remote: false');
    return 0;
}
// {"remotepc_calendar":{"timezone":"Europe/Madrid","ruleset":[
//     {"remote":false,"mon":true,"tue":true,"wed":true,"thu":true,"fri":true,"from_hr":"8","to_hr":"17"},
//     {"remote":true,"reason":"exc","from_ts":1709074800,"to_ts":1706569199}
// ]}}

function og_ordenador_cumple_criterios ($client) {
    if ($client->status == 'oglive' || $client->status == 'linux' || $client->status == 'windows') {
        return 1;
    }

    return 0;
}

/**
 *         og_sondeoAula ($idCentro, $idAula, $idImagen)
 * @brief  Gets how many clients are available for remotepc from OG
 * @param  int     building id
 * @param  int     lab id
 * @param  int     image id
 * @return int     number of clients available for remotepc, -1 if aula is not remote or null on error
 */
function og_sondeoAula ($idCentro, $idAula, $idImagen) {
    global $OG_REST_URL;
    global $OG_REST_AUTH;

    $aula = og_getAula ($idCentro, $idAula);
    if (null === $aula || $aula->inremotepc != 1) {
        do_log ('lab is null, or not in remotepc, ignoring lab');
        return -1;
    }

    if (!og_aula_is_remote ($aula->idcalendario)) {
        do_log ('lab is not remote, ignoring lab');
        return -1;
    }

    $url = $OG_REST_URL."ous/".$idCentro."/labs/".$idAula."/clients/status";
    $ch = curl_init();
    curl_setopt ($ch, CURLOPT_URL, $url);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_HTTPHEADER, [
        'Accept: application/json',
        'Authorization: '.$OG_REST_AUTH
    ]);

    $json = curl_exec ($ch);
    $curl_errno = curl_errno ($ch);
    $curl_error = curl_error ($ch);
    $code = curl_getinfo ($ch, CURLINFO_RESPONSE_CODE);
    curl_close ($ch);

    if ($curl_errno) {
        do_log (sprintf ("error '%s' errno '%d'", $curl_error, $curl_errno));
        return null;
    }

    if ($code > 201) { return 0; }

    $server_output = json_decode ($json);
    $clientsOn = 0;
    foreach ($server_output as $client) {
        do_log (sprintf ("Evaluating client id '%s' with ip '%s' in status '%s'", $client->id, $client->ip, $client->status));
        if (!og_ordenador_cumple_criterios ($client)) {
            do_log ('og_ordenador_cumple_criterios is false: ignoring client');
            continue;
        }

        $clientDisk = og_getClientDiskConfig ($idCentro, $idAula, $client->id);
        if (null === $clientDisk) {
            do_log ('og_getClientDiskConfig returned false: ignoring client');
            continue;
        }

        $part_ok = 0;
        for ($p = 0; $p < count ($clientDisk->diskcfg); $p++) {
            $part = $clientDisk->diskcfg[$p];
            if (isset ($part->image) && $part->image->id == $idImagen) {
                $part_ok = 1;
                break;
            }
        }
        if (!$part_ok) {
            do_log ('Client does not have the requested image: ignoring client');
            continue;
        }

        do_log ('Adding client');
        $clientsOn++;
    }

    return $clientsOn;
}

/**
 *         _set_lab_reserved ($idAula)
 * @brief  Mark lab as reserved or unreserved in the OG DB
 * @param  int     lab id
 * @param  int     reserved (1) or unreserved (0)
 * @return bool    success (true) or failure (false)
 */
function _set_lab_reserved ($idAula, $reserved) {
    global $cnx;
    $cmd = CreaComando ($cnx);
    if (!$cmd) { die ('ACCESS_ERROR'); }

    if ($reserved) {
        do_log ("reserving lab $idAula");
    } else {
        do_log ("unreserving lab $idAula");
    }
    $cmd->CreaParametro ('@idaula', $idAula, 1);
    $cmd->CreaParametro ('@reserved', $reserved, 1);
    $cmd->texto = 'UPDATE aulas SET remotepc_reserved=@reserved WHERE idaula=@idaula';
    $resul=$cmd->Ejecutar();

    return $resul;
}

/**
 *         reserve_lab ($idAula)
 * @brief  Mark lab as reserved in the OG DB
 * @param  int     lab id
 * @return bool    success (true) or failure (false)
 */
function reserve_lab ($idAula) {
    _set_lab_reserved ($idAula, 1);
}

/**
 *         unreserve_lab ($idAula)
 * @brief  Mark lab as unreserved in the OG DB
 * @param  int     lab id
 * @return bool    success (true) or failure (false)
 */
function unreserve_lab ($idAula) {
    _set_lab_reserved ($idAula, 0);
}


$OG_REST_AUTH = db_fetch_apikey();
if (null == $OG_REST_AUTH) {
    do_log ('Failed to fetch OG API key from the database, exiting');
    exit (1);
}

$cals = db_fetchCals();
if (!$cals) {
    do_log ('No calendars, exiting');
    exit (0);
}

$server_output = uds_login ($UDS_AUTHENTICATOR, $UDS_USER, $UDS_PASS);
if (null === $server_output) {
    do_log ('Failed to log into UDS');
    exit (1);
}

$headers = uds_getHeaders ($server_output->token, $server_output->scrambler);
$servicePools = uds_getAllServicePools ($headers);

foreach ($servicePools as $servicePool) {
    $providerId = $servicePool->provider_id;
    $serviceId = $servicePool->service_id;
    do_log ("Service pool with providerId '$providerId', serviceId '$serviceId'");

    $service = uds_getServiceInfo ($headers, $providerId, $serviceId);
    if ($service === null) {
        do_log ('Its service is null, ignoring this service pool');
        continue;
    }
    do_log (sprintf ("Its service has OU '%s', lab '%s', image '%s'", $service->ou, $service->lab, $service->image));

    // Conectar con opengnsys para ver cuantos pcs hay disponibles y enviar dicho parametro
    $max_srvs = og_sondeoAula ($service->ou, $service->lab, $service->image);
    if (null === $max_srvs) {
        do_log ('og_sondeoAula for the OU/lab/image failed, ignoring this service pool');
        continue;
    } elseif (-1 === $max_srvs) {
        unreserve_lab ($service->lab);
        continue;
    }
    $servicePool->osmanager_id = null;
    $servicePool->max_srvs = $max_srvs;
    $servicePool->initial_srvs = $servicePool->max_srvs;
    $servicePool->cache_l1_srvs = 0;
    $servicePool->visible = ($servicePool->max_srvs > 0);
    do_log (sprintf ("Initial servers '%s', max servers '%s'", $servicePool->initial_srvs, $servicePool->max_srvs));

    /*
    if ($servicePool->initial_srvs > $servicePool->max_srvs){
        $servicePool->initial_srvs = $servicePool->max_srvs;
    }
    /**/
    $sp = uds_setServicePool ($headers, $servicePool);
    if (null === $sp) {
        do_log ('uds_setServicePool failed');
        continue;
    }

    reserve_lab ($service->lab);

    do_log (sprintf ("Service pool '%s': OU id '%d', lab id '%d', image id '%d', max servers '%d', visible '%d'", $sp->name, $service->ou, $service->lab, $service->image, $sp->max_srvs, $sp->visible));
}

fclose ($logfd);
