/*
  authentication_service.cpp - ESP3D authentication service class

  Copyright (c) 2014 Luc Lebosse. All rights reserved.

  This code is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This code is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with This code; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "authentication_service.h"
#include "../../core/esp3doutput.h"
#include "../../core/settings_esp3d.h"

#if defined(AUTHENTICATION_FEATURE)
#if defined(HTTP_FEATURE)
#if defined(ARDUINO_ARCH_ESP32)
#include <WebServer.h>
#endif //ARDUINO_ARCH_ESP32
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WebServer.h>
#endif //ARDUINO_ARCH_ESP8266
Authwebserver *AuthenticationService::_webserver = nullptr;
#endif //HTTP_FEATURE
#endif //AUTHENTICATION_FEATURE

#if defined(AUTHENTICATION_FEATURE)
String AuthenticationService::_adminpwd = "";
String AuthenticationService::_userpwd = "";
#if defined(HTTP_FEATURE)
uint32_t AuthenticationService::_sessionTimeout = 360000;
auth_ip *AuthenticationService::_head = nullptr;
uint8_t AuthenticationService::_current_nb_ip = 0;
#endif //HTTP_FEATURE
#endif //AUTHENTICATION_FEATURE

#define MAX_AUTH_IP 10
//#define ALLOW_MULTIPLE_SESSIONS

//check authentification
level_authenticate_type AuthenticationService::authenticated_level(const char *pwd, ESP3DOutput *output)
{
#ifdef AUTHENTICATION_FEATURE
    level_authenticate_type auth_type = LEVEL_GUEST;
    if (pwd != nullptr) {

        if (isadmin(pwd)) {
            auth_type = LEVEL_ADMIN;
        }
        if (isuser(pwd) && (auth_type != LEVEL_ADMIN)) {
            auth_type = LEVEL_USER;
        }
        return auth_type;
    } else {
        if (output) {
            if (output->client() != ESP_HTTP_CLIENT) {
                return auth_type;
            }
        }
#if defined(HTTP_FEATURE)
        if (_webserver) {
            if (_webserver->hasHeader("Authorization")) {
                //log_esp3d("Check authorization %",(_webserver->uri()).c_str());
                if (_webserver->authenticate(DEFAULT_ADMIN_LOGIN, _adminpwd.c_str())) {
                    auth_type = LEVEL_ADMIN;
                } else {
                    if (_webserver->authenticate(DEFAULT_USER_LOGIN, _userpwd.c_str())) {
                        auth_type = LEVEL_USER;
                    }
                }
            }
            if (_webserver->hasHeader("Cookie")) {
                //log_esp3d("Check Cookie %s",(_webserver->uri()).c_str());
                String cookie = _webserver->header("Cookie");
                int pos = cookie.indexOf("ESPSESSIONID=");
                if (pos != -1) {
                    int pos2 = cookie.indexOf(";", pos);
                    String sessionID = cookie.substring(pos + strlen("ESPSESSIONID="), pos2);
                    IPAddress ip = _webserver->client().remoteIP();
                    //check if cookie can be reset and clean table in same time
                    auth_type = ResetAuthIP(ip, sessionID.c_str());
                    //log_esp3d("Authentication = %d", auth_type);
                }
            }
        }
#endif //HTTP_FEATURE
    }
    return auth_type;
#else
    (void)pwd;
    (void)output;
    return LEVEL_ADMIN;
#endif //AUTHENTICATION_FEATURE
}
#ifdef AUTHENTICATION_FEATURE

#if defined(HTTP_FEATURE)
uint32_t AuthenticationService::setSessionTimeout(uint32_t timeout)
{
    if (timeout >= 0) {
        _sessionTimeout = timeout;
    }
    return _sessionTimeout;
}
uint32_t AuthenticationService::getSessionTimeout()
{
    return _sessionTimeout;
}
#endif //HTTP_FEATURE

bool AuthenticationService::begin(Authwebserver *webserver)
{
    end();
    update();
#if defined(HTTP_FEATURE)
    _webserver = webserver;
#endif //HTTP_FEATURE
    //value is in ms but storage is in min
    _sessionTimeout = 1000 * 60 * Settings_ESP3D::read_byte(ESP_SESSION_TIMEOUT);
    return true;
}
void AuthenticationService::end()
{
#if defined(HTTP_FEATURE)
    _webserver = nullptr;
    ClearAllSessions();
#endif //HTTP_FEATURE
}

void AuthenticationService::update()
{
    _adminpwd = Settings_ESP3D::read_string(ESP_ADMIN_PWD);
    _userpwd = Settings_ESP3D::read_string(ESP_USER_PWD);
}

void AuthenticationService::handle()
{
}

//check admin password
bool AuthenticationService::isadmin(const char *pwd)
{
    if (strcmp(_adminpwd.c_str(), pwd) != 0) {
        return false;
    } else {
        return true;
    }
}

//check user password - admin password is also valid
bool AuthenticationService::isuser(const char *pwd)
{
    //it is not user password
    if (strcmp(_userpwd.c_str(), pwd) != 0) {
        //check admin password
        return isadmin(pwd);
    } else {
        return true;
    }
}

#if defined(HTTP_FEATURE)
//add the information in the linked list if possible
bool AuthenticationService::AddAuthIP(auth_ip *item)
{
    if (_current_nb_ip > MAX_AUTH_IP) {
        return false;
    }
    item->_next = _head;
    _head = item;
    _current_nb_ip++;
    return true;
}

//Session ID based on IP and time using 16 char
char *AuthenticationService::create_session_ID()
{
    static char sessionID[17];
    //reset SESSIONID
    for (int i = 0; i < 17; i++) {
        sessionID[i] = '\0';
    }
    //get time
    uint32_t now = millis();
    //get remote IP
    IPAddress remoteIP = _webserver->client().remoteIP();
    //generate SESSIONID
    if (0 > sprintf(sessionID, "%02X%02X%02X%02X%02X%02X%02X%02X", remoteIP[0], remoteIP[1], remoteIP[2], remoteIP[3], (uint8_t)((now >> 0) & 0xff), (uint8_t)((now >> 8) & 0xff), (uint8_t)((now >> 16) & 0xff), (uint8_t)((now >> 24) & 0xff))) {
        strcpy(sessionID, "NONE");
    }
    return sessionID;
}

bool AuthenticationService::ClearAllSessions()
{
    while (_head) {
        auth_ip *current = _head;
        _head = _head->_next;
        delete current;
    }
    _current_nb_ip = 0;
    _head = nullptr;

    return true;
}

bool AuthenticationService::ClearCurrentSession()
{
    String cookie = _webserver->header("Cookie");
    int pos = cookie.indexOf("ESPSESSIONID=");
    String sessionID;
    if (pos != -1) {
        int pos2 = cookie.indexOf(";", pos);
        sessionID = cookie.substring(pos + strlen("ESPSESSIONID="), pos2);
    }
    return ClearAuthIP(_webserver->client().remoteIP(), sessionID.c_str());
}

bool AuthenticationService::CreateSession(level_authenticate_type auth_level, const char *username, const char *session_ID)
{
    auth_ip *current_auth = new auth_ip;
    current_auth->level = auth_level;
    current_auth->ip = _webserver->client().remoteIP();
    strcpy(current_auth->sessionID, session_ID);
    strcpy(current_auth->userID, username);
    current_auth->last_time = millis();
#ifndef ALLOW_MULTIPLE_SESSIONS
    //if not multiple session no need to keep all session, current one is enough
    ClearAllSessions();
#endif //ALLOW_MULTIPLE_SESSIONS
    if (AddAuthIP(current_auth)) {
        return true;
    } else {
        delete current_auth;
        return false;
    }
}

bool AuthenticationService::ClearAuthIP(IPAddress ip, const char *sessionID)
{
    auth_ip *current = _head;
    auth_ip *previous = NULL;
    bool done = false;
    while (current) {
        if ((ip == current->ip) && (strcmp(sessionID, current->sessionID) == 0)) {
            //remove
            done = true;
            if (current == _head) {
                _head = current->_next;
                _current_nb_ip--;
                delete current;
                current = _head;
            } else {
                previous->_next = current->_next;
                _current_nb_ip--;
                delete current;
                current = previous->_next;
            }
        } else {
            previous = current;
            current = current->_next;
        }
    }
    return done;
}

//Get info
auth_ip *AuthenticationService::GetAuth(IPAddress ip, const char *sessionID)
{
    auth_ip *current = _head;
    while (current) {
        if (ip == current->ip) {
            if (strcmp(sessionID, current->sessionID) == 0) {
                //found
                return current;
            }
        }
        //previous = current;
        current = current->_next;
    }
    return NULL;
}

//Get time left for specific session
uint32_t AuthenticationService::getSessionRemaining(const char *sessionID)
{
    auth_ip *current = _head;
    if ((sessionID == nullptr) || (strlen(sessionID) == 0)) {
        return 0;
    }
    while (current) {
        if (strcmp(sessionID, current->sessionID) == 0) {
            //found
            uint32_t now = millis();
            if ((now - current->last_time) > _sessionTimeout) {
                return 0;
            }
            return _sessionTimeout - (now - current->last_time);
        }
        //previous = current;
        current = current->_next;
    }
    return 0;
}

//Review all IP to reset timers
level_authenticate_type AuthenticationService::ResetAuthIP(IPAddress ip, const char *sessionID)
{
    auth_ip *current = _head;
    auth_ip *previous = NULL;
    //get time
    //uint32_t now = millis();
    while (current) {
        //if time out is reached and time out is not disabled
        //if IP is not current one and time out is disabled
        if ((((millis() - current->last_time) > _sessionTimeout) && (_sessionTimeout != 0)) || ((ip != current->ip) && (_sessionTimeout == 0))) {
            //remove
            if (current == _head) {
                _head = current->_next;
                _current_nb_ip--;
                delete current;
                current = _head;
            } else {
                previous->_next = current->_next;
                _current_nb_ip--;
                delete current;
                current = previous->_next;
            }
        } else {
            if (ip == current->ip) {
                if (strcmp(sessionID, current->sessionID) == 0) {
                    //reset time
                    current->last_time = millis();
                    return (level_authenticate_type)current->level;
                }
            }
            previous = current;
            current = current->_next;
        }
    }
    return LEVEL_GUEST;
}
#endif //HTTP_FEATURE

#endif //AUTHENTICATION_FEATURE
