/*******************************************************************************
 * 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.
 *******************************************************************************/

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <structmember.h>

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

/* =========================================================================
 *  PyClientConnection – wraps authdb::client::ClientConnection
 * ========================================================================= */

typedef struct {
    PyObject_HEAD
    authdb::client::ClientConnection *con;
} PyClientConnection;

static void PyClientConnection_dealloc(PyClientConnection *self) {
    delete self->con;
    Py_TYPE(self)->tp_free(reinterpret_cast<PyObject *>(self));
}

static int PyClientConnection_init(PyClientConnection *self,
                                   PyObject * /*args*/, PyObject * /*kwds*/) {
    try {
        self->con = new authdb::client::ClientConnection();
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return -1;
    }
    return 0;
}

static PyObject *PyClientConnection_setUrl(PyClientConnection *self, PyObject *args) {
    const char *url = nullptr;
    if (!PyArg_ParseTuple(args, "s", &url)) return nullptr;
    try { self->con->setUrl(std::string(url)); }
    catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return nullptr; }
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_setClientName(PyClientConnection *self, PyObject *args) {
    const char *v = nullptr;
    if (!PyArg_ParseTuple(args, "s", &v)) return nullptr;
    self->con->setClientName(v);
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_setClientSecret(PyClientConnection *self, PyObject *args) {
    const char *v = nullptr;
    if (!PyArg_ParseTuple(args, "s", &v)) return nullptr;
    self->con->setClientSecret(v);
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_setUsername(PyClientConnection *self, PyObject *args) {
    const char *v = nullptr;
    if (!PyArg_ParseTuple(args, "s", &v)) return nullptr;
    self->con->setUsername(v);
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_setUserPassword(PyClientConnection *self, PyObject *args) {
    const char *v = nullptr;
    if (!PyArg_ParseTuple(args, "s", &v)) return nullptr;
    self->con->setUserPassword(v);
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_getClientId(PyClientConnection *self, PyObject * /*args*/) {
    try {
        const uuid::uuid &cid = self->con->getClientId();
        return PyUnicode_FromString(cid.c_str());
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

static PyObject *PyClientConnection_getSessionID(PyClientConnection *self, PyObject * /*args*/) {
    try {
        const uuid::uuid &sid = self->con->getSessionID();
        return PyUnicode_FromString(sid.c_str());
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

static PyObject *PyClientConnection_setSessionID(PyClientConnection *self, PyObject *args) {
    const char *v = nullptr;
    if (!PyArg_ParseTuple(args, "s", &v)) return nullptr;
    try {
        uuid::uuid sid(v);
        self->con->setSessionID(sid);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

static PyObject *PyClientConnection_reConnect(PyClientConnection *self, PyObject * /*args*/) {
    try { self->con->reConnect(); }
    catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return nullptr; }
    Py_RETURN_NONE;
}

static PyMethodDef PyClientConnection_methods[] = {
    {"set_url",           (PyCFunction)PyClientConnection_setUrl,          METH_VARARGS, "Set the server URL"},
    {"set_client_name",   (PyCFunction)PyClientConnection_setClientName,   METH_VARARGS, "Set the client name"},
    {"set_client_secret", (PyCFunction)PyClientConnection_setClientSecret, METH_VARARGS, "Set the client secret"},
    {"set_username",      (PyCFunction)PyClientConnection_setUsername,      METH_VARARGS, "Set the username"},
    {"set_user_password", (PyCFunction)PyClientConnection_setUserPassword, METH_VARARGS, "Set the user password"},
    {"get_client_id",     (PyCFunction)PyClientConnection_getClientId,     METH_NOARGS,  "Get the client connection ID"},
    {"get_session_id",    (PyCFunction)PyClientConnection_getSessionID,    METH_NOARGS,  "Get the session ID"},
    {"set_session_id",    (PyCFunction)PyClientConnection_setSessionID,    METH_VARARGS, "Set the session ID"},
    {"reconnect",         (PyCFunction)PyClientConnection_reConnect,       METH_NOARGS,  "Reconnect to the server"},
    {nullptr, nullptr, 0, nullptr}
};

static PyTypeObject PyClientConnectionType = {
    .ob_base = PyVarObject_HEAD_INIT(nullptr, 0)
    .tp_name = "authdb.ClientConnection",
    .tp_basicsize = sizeof(PyClientConnection),
    .tp_itemsize = 0,
    .tp_dealloc = (destructor)PyClientConnection_dealloc,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_doc = "AuthDB client connection",
    .tp_methods = PyClientConnection_methods,
    .tp_init = (initproc)PyClientConnection_init,
    .tp_new = PyType_GenericNew,
};

/* =========================================================================
 *  PyClient – wraps authdb::client::Client
 * ========================================================================= */

typedef struct {
    PyObject_HEAD
    authdb::client::Client *client;
    PyClientConnection     *con_ref;   /* prevent GC of the connection */
} PyClient;

static void PyClient_dealloc(PyClient *self) {
    delete self->client;
    Py_XDECREF(self->con_ref);
    Py_TYPE(self)->tp_free(reinterpret_cast<PyObject *>(self));
}

static int PyClient_init(PyClient *self, PyObject *args, PyObject * /*kwds*/) {
    PyObject *conObj = nullptr;
    if (!PyArg_ParseTuple(args, "O!", &PyClientConnectionType, &conObj)) return -1;

    PyClientConnection *pyCon = reinterpret_cast<PyClientConnection *>(conObj);
    try {
        self->client = new authdb::client::Client(*pyCon->con);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return -1;
    }
    self->con_ref = pyCon;
    Py_INCREF(pyCon);
    return 0;
}

/* -- user_auth() -> bool -- */
static PyObject *PyClient_userAuth(PyClient *self, PyObject * /*args*/) {
    try {
        bool ok = self->client->UserAuth();
        return PyBool_FromLong(ok ? 1 : 0);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- client_auth() -> bool -- */
static PyObject *PyClient_clientAuth(PyClient *self, PyObject * /*args*/) {
    try {
        bool ok = self->client->ClientAuth();
        return PyBool_FromLong(ok ? 1 : 0);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- add_connection(name, secret) -> str -- */
static PyObject *PyClient_addConnection(PyClient *self, PyObject *args) {
    const char *name = nullptr, *secret = nullptr;
    if (!PyArg_ParseTuple(args, "ss", &name, &secret)) return nullptr;
    try {
        std::string cid = self->client->addConnection(name, secret);
        return PyUnicode_FromString(cid.c_str());
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- rem_connection(client_id) -- */
static PyObject *PyClient_remConnection(PyClient *self, PyObject *args) {
    const char *cid = nullptr;
    if (!PyArg_ParseTuple(args, "s", &cid)) return nullptr;
    try {
        uuid::uuid uid(cid);
        self->client->remConnection(uid);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/* -- edit_connection(client_id, name, secret) -- */
static PyObject *PyClient_editConnection(PyClient *self, PyObject *args) {
    const char *cid = nullptr, *name = nullptr, *secret = nullptr;
    if (!PyArg_ParseTuple(args, "sss", &cid, &name, &secret)) return nullptr;
    try {
        uuid::uuid uid(cid);
        self->client->editConnection(uid, name, secret);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

/* -- session_info() -> dict -- */
static PyObject *PyClient_sessionInfo(PyClient *self, PyObject * /*args*/) {
    try {
        authdb::SessionData sdat;
        self->client->SessionInfo(sdat);

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

        PyObject *dict = PyDict_New();
        PyDict_SetItemString(dict, "session_id", PyUnicode_FromString(sid.c_str()));
        PyDict_SetItemString(dict, "user_id",    PyUnicode_FromString(uid.c_str()));
        PyDict_SetItemString(dict, "username",   PyUnicode_FromString(sdat.getUsername()));
        PyDict_SetItemString(dict, "domain_id",  PyUnicode_FromString(did.c_str()));
        return dict;
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- rem_session() -> bool -- */
static PyObject *PyClient_remSession(PyClient *self, PyObject * /*args*/) {
    try {
        bool ok = self->client->remSession();
        return PyBool_FromLong(ok ? 1 : 0);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- gpo_check(gpo_id) -> bool -- */
static PyObject *PyClient_gpoCheck(PyClient *self, PyObject *args) {
    const char *gid = nullptr;
    if (!PyArg_ParseTuple(args, "s", &gid)) return nullptr;
    try {
        uuid::uuid gpoid(gid);
        bool ok = self->client->GPOcheck(gpoid);
        return PyBool_FromLong(ok ? 1 : 0);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- gpo_exists(gpo_id) -> bool -- */
static PyObject *PyClient_gpoExists(PyClient *self, PyObject *args) {
    const char *gid = nullptr;
    if (!PyArg_ParseTuple(args, "s", &gid)) return nullptr;
    try {
        uuid::uuid gpoid(gid);
        bool ok = self->client->GPOexits(gpoid);
        return PyBool_FromLong(ok ? 1 : 0);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- gpo_add(gpo_id, name, desc, gpo_default, members) -> str -- */
static PyObject *PyClient_gpoAdd(PyClient *self, PyObject *args) {
    const char *gpoId = nullptr, *name = nullptr, *desc = nullptr;
    int gpoDefault = 0;
    PyObject *pyMembers = nullptr;
    if (!PyArg_ParseTuple(args, "ssspO!", &gpoId, &name, &desc, &gpoDefault, &PyList_Type, &pyMembers))
        return nullptr;
    try {
        uuid::uuid gpoid(gpoId);
        std::vector<std::string> members;
        Py_ssize_t len = PyList_Size(pyMembers);
        for (Py_ssize_t i = 0; i < len; ++i) {
            PyObject *item = PyList_GetItem(pyMembers, i);
            if (PyUnicode_Check(item)) {
                members.emplace_back(PyUnicode_AsUTF8(item));
            }
        }
        std::string result = self->client->GPOadd(gpoid, name, desc, gpoDefault != 0, members);
        return PyUnicode_FromString(result.c_str());
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
}

/* -- change_user_pw(old_pw, new_pw) -- */
static PyObject *PyClient_changeUserPw(PyClient *self, PyObject *args) {
    const char *oldpw = nullptr, *newpw = nullptr;
    if (!PyArg_ParseTuple(args, "ss", &oldpw, &newpw)) return nullptr;
    try {
        self->client->changeUserPw(oldpw, newpw);
    } catch (const std::exception &e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return nullptr;
    }
    Py_RETURN_NONE;
}

static PyMethodDef PyClient_methods[] = {
    {"user_auth",        (PyCFunction)PyClient_userAuth,       METH_NOARGS,  "Authenticate as user, returns bool"},
    {"client_auth",      (PyCFunction)PyClient_clientAuth,     METH_NOARGS,  "Authenticate as client, returns bool"},
    {"add_connection",   (PyCFunction)PyClient_addConnection,  METH_VARARGS, "Add a client connection (name, secret) -> client_db_id"},
    {"rem_connection",   (PyCFunction)PyClient_remConnection,  METH_VARARGS, "Remove a client connection (client_id)"},
    {"edit_connection",  (PyCFunction)PyClient_editConnection, METH_VARARGS, "Edit a client connection (client_id, name, secret)"},
    {"session_info",     (PyCFunction)PyClient_sessionInfo,    METH_NOARGS,  "Get session info -> dict"},
    {"rem_session",      (PyCFunction)PyClient_remSession,     METH_NOARGS,  "Remove session, returns bool"},
    {"gpo_check",        (PyCFunction)PyClient_gpoCheck,       METH_VARARGS, "Check GPO for current session (gpo_id) -> bool"},
    {"gpo_exists",       (PyCFunction)PyClient_gpoExists,      METH_VARARGS, "Check if GPO exists (gpo_id) -> bool"},
    {"gpo_add",          (PyCFunction)PyClient_gpoAdd,          METH_VARARGS, "Add a GPO (gpo_id, name, desc, default, members[]) -> gpo_id"},
    {"change_user_pw",   (PyCFunction)PyClient_changeUserPw,   METH_VARARGS, "Change user password (old_pw, new_pw)"},
    {nullptr, nullptr, 0, nullptr}
};

static PyTypeObject PyClientType = {
    .ob_base = PyVarObject_HEAD_INIT(nullptr, 0)
    .tp_name = "authdb.Client",
    .tp_basicsize = sizeof(PyClient),
    .tp_itemsize = 0,
    .tp_dealloc = (destructor)PyClient_dealloc,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_doc = "AuthDB client",
    .tp_methods = PyClient_methods,
    .tp_init = (initproc)PyClient_init,
    .tp_new = PyType_GenericNew,
};

/* =========================================================================
 *  Module definition
 * ========================================================================= */

static PyMethodDef module_methods[] = {
    {nullptr, nullptr, 0, nullptr}
};

static struct PyModuleDef authdb_module = {
    PyModuleDef_HEAD_INIT,
    "authdb",                        /* module name */
    "AuthDB Python client bindings", /* docstring */
    -1,                              /* per-interpreter state size (-1 = global) */
    module_methods
};

PyMODINIT_FUNC PyInit_authdb(void) {
    if (PyType_Ready(&PyClientConnectionType) < 0) return nullptr;
    if (PyType_Ready(&PyClientType) < 0) return nullptr;

    PyObject *m = PyModule_Create(&authdb_module);
    if (!m) return nullptr;

    Py_INCREF(&PyClientConnectionType);
    if (PyModule_AddObject(m, "ClientConnection",
                           reinterpret_cast<PyObject *>(&PyClientConnectionType)) < 0) {
        Py_DECREF(&PyClientConnectionType);
        Py_DECREF(m);
        return nullptr;
    }

    Py_INCREF(&PyClientType);
    if (PyModule_AddObject(m, "Client",
                           reinterpret_cast<PyObject *>(&PyClientType)) < 0) {
        Py_DECREF(&PyClientType);
        Py_DECREF(m);
        return nullptr;
    }

    return m;
}