/*******************************************************************************
 * Copyright (c) 2025, Jan Koester jan.koester@gmx.net
 * 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 the <organization> 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 <COPYRIGHT HOLDER> 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.
 *******************************************************************************/

extern "C" {
#include <php.h>
#include <zend_exceptions.h>
#include <zend_API.h>
#include <ext/standard/info.h>
}

#include "../client/client.h"

/* =========================================================================
 *  Helpers
 * ========================================================================= */

#define AUTHDB_THROW(msg) \
    zend_throw_exception(zend_ce_exception, msg, 0)

/* =========================================================================
 *  AuthDBConnection class
 * ========================================================================= */

static zend_class_entry *ce_AuthDBConnection = nullptr;
static zend_object_handlers authdb_connection_handlers;

struct php_authdb_connection {
    authdb::client::ClientConnection *con;
    zend_object                       std;
};

static inline php_authdb_connection *php_authdb_connection_from_obj(zend_object *obj) {
    return reinterpret_cast<php_authdb_connection *>(
        reinterpret_cast<char *>(obj) - XtOffsetOf(php_authdb_connection, std));
}

static zend_object *php_authdb_connection_create(zend_class_entry *ce) {
    auto *intern = static_cast<php_authdb_connection *>(
        ecalloc(1, sizeof(php_authdb_connection) + zend_object_properties_size(ce)));
    intern->con = nullptr;
    zend_object_std_init(&intern->std, ce);
    object_properties_init(&intern->std, ce);
    intern->std.handlers = &authdb_connection_handlers;
    return &intern->std;
}

static void php_authdb_connection_free(zend_object *obj) {
    php_authdb_connection *intern = php_authdb_connection_from_obj(obj);
    delete intern->con;
    intern->con = nullptr;
    zend_object_std_dtor(obj);
}

/* __construct() */
PHP_METHOD(AuthDBConnection, __construct) {
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        intern->con = new authdb::client::ClientConnection();
    } catch (const std::exception &e) {
        AUTHDB_THROW(e.what());
    }
}

/* setUrl(string $url): void */
PHP_METHOD(AuthDBConnection, setUrl) {
    char *url = nullptr; size_t url_len = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(url, url_len) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try { intern->con->setUrl(std::string(url, url_len)); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* setClientName(string $name): void */
PHP_METHOD(AuthDBConnection, setClientName) {
    char *v = nullptr; size_t vl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(v, vl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS))->con->setClientName(v);
}

/* setClientSecret(string $secret): void */
PHP_METHOD(AuthDBConnection, setClientSecret) {
    char *v = nullptr; size_t vl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(v, vl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS))->con->setClientSecret(v);
}

/* setUsername(string $username): void */
PHP_METHOD(AuthDBConnection, setUsername) {
    char *v = nullptr; size_t vl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(v, vl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS))->con->setUsername(v);
}

/* setUserPassword(string $password): void */
PHP_METHOD(AuthDBConnection, setUserPassword) {
    char *v = nullptr; size_t vl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(v, vl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS))->con->setUserPassword(v);
}

/* getClientId(): string */
PHP_METHOD(AuthDBConnection, getClientId) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        const uuid::uuid &cid = intern->con->getClientId();
        RETURN_STRING(cid.c_str());
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* getSessionID(): string */
PHP_METHOD(AuthDBConnection, getSessionID) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        const uuid::uuid &sid = intern->con->getSessionID();
        RETURN_STRING(sid.c_str());
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* setSessionID(string $sid): void */
PHP_METHOD(AuthDBConnection, setSessionID) {
    char *v = nullptr; size_t vl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(v, vl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid sid(v);
        intern->con->setSessionID(sid);
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* reConnect(): void */
PHP_METHOD(AuthDBConnection, reConnect) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_connection *intern = php_authdb_connection_from_obj(Z_OBJ_P(ZEND_THIS));
    try { intern->con->reConnect(); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

ZEND_BEGIN_ARG_INFO_EX(arginfo_none, 0, 0, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_one_string, 0, 0, 1)
    ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0)
ZEND_END_ARG_INFO()

static const zend_function_entry authdb_connection_methods[] = {
    PHP_ME(AuthDBConnection, __construct,     arginfo_none,       ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    PHP_ME(AuthDBConnection, setUrl,          arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, setClientName,   arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, setClientSecret, arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, setUsername,      arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, setUserPassword, arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, getClientId,     arginfo_none,       ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, getSessionID,    arginfo_none,       ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, setSessionID,    arginfo_one_string, ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBConnection, reConnect,       arginfo_none,       ZEND_ACC_PUBLIC)
    PHP_FE_END
};

/* =========================================================================
 *  AuthDBClient class
 * ========================================================================= */

static zend_class_entry *ce_AuthDBClient = nullptr;
static zend_object_handlers authdb_client_handlers;

struct php_authdb_client {
    authdb::client::Client *client;
    zend_object             std;
};

static inline php_authdb_client *php_authdb_client_from_obj(zend_object *obj) {
    return reinterpret_cast<php_authdb_client *>(
        reinterpret_cast<char *>(obj) - XtOffsetOf(php_authdb_client, std));
}

static zend_object *php_authdb_client_create(zend_class_entry *ce) {
    auto *intern = static_cast<php_authdb_client *>(
        ecalloc(1, sizeof(php_authdb_client) + zend_object_properties_size(ce)));
    intern->client = nullptr;
    zend_object_std_init(&intern->std, ce);
    object_properties_init(&intern->std, ce);
    intern->std.handlers = &authdb_client_handlers;
    return &intern->std;
}

static void php_authdb_client_free(zend_object *obj) {
    php_authdb_client *intern = php_authdb_client_from_obj(obj);
    delete intern->client;
    intern->client = nullptr;
    zend_object_std_dtor(obj);
}

ZEND_BEGIN_ARG_INFO_EX(arginfo_client_construct, 0, 0, 1)
    ZEND_ARG_OBJ_INFO(0, connection, AuthDBConnection, 0)
ZEND_END_ARG_INFO()

/* __construct(AuthDBConnection $connection) */
PHP_METHOD(AuthDBClient, __construct) {
    zval *zcon = nullptr;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_OBJECT_OF_CLASS(zcon, ce_AuthDBConnection) ZEND_PARSE_PARAMETERS_END();

    php_authdb_connection *pcon = php_authdb_connection_from_obj(Z_OBJ_P(zcon));
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        intern->client = new authdb::client::Client(*pcon->con);
    } catch (const std::exception &e) {
        AUTHDB_THROW(e.what());
    }
}

/* userAuth(): bool */
PHP_METHOD(AuthDBClient, userAuth) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try { RETURN_BOOL(intern->client->UserAuth()); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* clientAuth(): bool */
PHP_METHOD(AuthDBClient, clientAuth) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try { RETURN_BOOL(intern->client->ClientAuth()); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

ZEND_BEGIN_ARG_INFO_EX(arginfo_two_strings, 0, 0, 2)
    ZEND_ARG_TYPE_INFO(0, arg1, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, arg2, IS_STRING, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_three_strings, 0, 0, 3)
    ZEND_ARG_TYPE_INFO(0, arg1, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, arg2, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, arg3, IS_STRING, 0)
ZEND_END_ARG_INFO()

/* addConnection(string $name, string $secret): string */
PHP_METHOD(AuthDBClient, addConnection) {
    char *name = nullptr, *secret = nullptr;
    size_t nl = 0, sl = 0;
    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STRING(name, nl)
        Z_PARAM_STRING(secret, sl)
    ZEND_PARSE_PARAMETERS_END();

    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        std::string cid = intern->client->addConnection(name, secret);
        RETURN_STRINGL(cid.c_str(), cid.size());
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* remConnection(string $clientId): void */
PHP_METHOD(AuthDBClient, remConnection) {
    char *cid = nullptr; size_t cl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(cid, cl) ZEND_PARSE_PARAMETERS_END();

    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid uid(cid);
        intern->client->remConnection(uid);
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* editConnection(string $clientId, string $name, string $secret): void */
PHP_METHOD(AuthDBClient, editConnection) {
    char *cid = nullptr, *name = nullptr, *secret = nullptr;
    size_t cl = 0, nl = 0, sl = 0;
    ZEND_PARSE_PARAMETERS_START(3, 3)
        Z_PARAM_STRING(cid, cl)
        Z_PARAM_STRING(name, nl)
        Z_PARAM_STRING(secret, sl)
    ZEND_PARSE_PARAMETERS_END();

    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid uid(cid);
        intern->client->editConnection(uid, name, secret);
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* sessionInfo(): array */
PHP_METHOD(AuthDBClient, sessionInfo) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        authdb::SessionData sdat;
        intern->client->SessionInfo(sdat);

        uuid::uuid sid, uid, did;
        sdat.getSid(sid);
        sdat.getUid(uid);
        sdat.getDid(did);

        array_init(return_value);
        add_assoc_string(return_value, "session_id", const_cast<char *>(sid.c_str()));
        add_assoc_string(return_value, "user_id",    const_cast<char *>(uid.c_str()));
        add_assoc_string(return_value, "username",   const_cast<char *>(sdat.getUsername()));
        add_assoc_string(return_value, "domain_id",  const_cast<char *>(did.c_str()));
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* remSession(): bool */
PHP_METHOD(AuthDBClient, remSession) {
    ZEND_PARSE_PARAMETERS_NONE();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try { RETURN_BOOL(intern->client->remSession()); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* gpoCheck(string $gpoId): bool */
PHP_METHOD(AuthDBClient, gpoCheck) {
    char *gid = nullptr; size_t gl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(gid, gl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid gpoid(gid);
        RETURN_BOOL(intern->client->GPOcheck(gpoid));
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* gpoExists(string $gpoId): bool */
PHP_METHOD(AuthDBClient, gpoExists) {
    char *gid = nullptr; size_t gl = 0;
    ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(gid, gl) ZEND_PARSE_PARAMETERS_END();
    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid gpoid(gid);
        RETURN_BOOL(intern->client->GPOexits(gpoid));
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

ZEND_BEGIN_ARG_INFO_EX(arginfo_addgpo, 0, 0, 5)
    ZEND_ARG_TYPE_INFO(0, gpoId, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, desc, IS_STRING, 0)
    ZEND_ARG_TYPE_INFO(0, gpoDefault, _IS_BOOL, 0)
    ZEND_ARG_TYPE_INFO(0, members, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

/* gpoAdd(string $gpoId, string $name, string $desc, bool $gpoDefault, array $members): string */
PHP_METHOD(AuthDBClient, gpoAdd) {
    char *gpoId = nullptr, *name = nullptr, *desc = nullptr;
    size_t gl = 0, nl = 0, dl = 0;
    zend_bool gpoDefault = 0;
    zval *zmembers = nullptr;
    ZEND_PARSE_PARAMETERS_START(5, 5)
        Z_PARAM_STRING(gpoId, gl)
        Z_PARAM_STRING(name, nl)
        Z_PARAM_STRING(desc, dl)
        Z_PARAM_BOOL(gpoDefault)
        Z_PARAM_ARRAY(zmembers)
    ZEND_PARSE_PARAMETERS_END();

    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try {
        uuid::uuid gpoid(gpoId);
        std::vector<std::string> members;
        HashTable *ht = Z_ARRVAL_P(zmembers);
        zval *entry;
        ZEND_HASH_FOREACH_VAL(ht, entry) {
            if (Z_TYPE_P(entry) == IS_STRING) {
                members.emplace_back(Z_STRVAL_P(entry), Z_STRLEN_P(entry));
            }
        } ZEND_HASH_FOREACH_END();
        std::string result = intern->client->GPOadd(gpoid, name, desc, gpoDefault != 0, members);
        RETURN_STRINGL(result.c_str(), result.size());
    } catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

/* changeUserPw(string $oldPw, string $newPw): void */
PHP_METHOD(AuthDBClient, changeUserPw) {
    char *oldpw = nullptr, *newpw = nullptr;
    size_t ol = 0, nl = 0;
    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STRING(oldpw, ol)
        Z_PARAM_STRING(newpw, nl)
    ZEND_PARSE_PARAMETERS_END();

    php_authdb_client *intern = php_authdb_client_from_obj(Z_OBJ_P(ZEND_THIS));
    try { intern->client->changeUserPw(oldpw, newpw); }
    catch (const std::exception &e) { AUTHDB_THROW(e.what()); }
}

static const zend_function_entry authdb_client_methods[] = {
    PHP_ME(AuthDBClient, __construct,    arginfo_client_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    PHP_ME(AuthDBClient, userAuth,       arginfo_none,             ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, clientAuth,     arginfo_none,             ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, addConnection,  arginfo_two_strings,      ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, remConnection,  arginfo_one_string,       ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, editConnection, arginfo_three_strings,    ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, sessionInfo,    arginfo_none,             ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, remSession,     arginfo_none,             ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, gpoCheck,       arginfo_one_string,       ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, gpoExists,      arginfo_one_string,       ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, gpoAdd,         arginfo_addgpo,           ZEND_ACC_PUBLIC)
    PHP_ME(AuthDBClient, changeUserPw,   arginfo_two_strings,      ZEND_ACC_PUBLIC)
    PHP_FE_END
};

/* =========================================================================
 *  Module lifecycle
 * ========================================================================= */

static PHP_MINIT_FUNCTION(authdb) {
    /* AuthDBConnection */
    zend_class_entry tmp_con;
    INIT_CLASS_ENTRY(tmp_con, "AuthDBConnection", authdb_connection_methods);
    ce_AuthDBConnection = zend_register_internal_class(&tmp_con);
    ce_AuthDBConnection->create_object = php_authdb_connection_create;
    memcpy(&authdb_connection_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
    authdb_connection_handlers.offset = XtOffsetOf(php_authdb_connection, std);
    authdb_connection_handlers.free_obj = php_authdb_connection_free;

    /* AuthDBClient */
    zend_class_entry tmp_cli;
    INIT_CLASS_ENTRY(tmp_cli, "AuthDBClient", authdb_client_methods);
    ce_AuthDBClient = zend_register_internal_class(&tmp_cli);
    ce_AuthDBClient->create_object = php_authdb_client_create;
    memcpy(&authdb_client_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
    authdb_client_handlers.offset = XtOffsetOf(php_authdb_client, std);
    authdb_client_handlers.free_obj = php_authdb_client_free;

    return SUCCESS;
}

static PHP_MINFO_FUNCTION(authdb) {
    php_info_print_table_start();
    php_info_print_table_row(2, "AuthDB support", "enabled");
    php_info_print_table_row(2, "Version", "1.0.0");
    php_info_print_table_end();
}

/* Module entry – no request/shutdown hooks needed */
zend_module_entry authdb_module_entry = {
    STANDARD_MODULE_HEADER,
    "authdb",               /* name */
    nullptr,                /* module-level functions (none) */
    PHP_MINIT(authdb),      /* MINIT */
    nullptr,                /* MSHUTDOWN */
    nullptr,                /* RINIT */
    nullptr,                /* RSHUTDOWN */
    PHP_MINFO(authdb),      /* MINFO */
    "1.0.0",                /* version */
    STANDARD_MODULE_PROPERTIES
};

extern "C" {
ZEND_GET_MODULE(authdb)
}