/*******************************************************************************
 * Copyright (c) 2026, 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.
 *******************************************************************************/

#include <iostream>
#include <cstring>
#include <algorithm>
#include <sstream>
#include <memory>

#include <netplus/socket.h>
#include <netplus/connection.h>
#include <netplus/eventapi.h>

#include "ad.h"
#include "authdb.h"
#include "backend.h"
#include "user.h"
#include "group.h"
#include "client.h"
#include "service.h"
#include "domain.h"
#include "types.h"
#include "exception.h"
#include "hash/sha512.h"

namespace authdb {

/******************************************************************************
 * Helpers
 *****************************************************************************/
namespace {
    std::string toLower(const std::string &s) {
        std::string r = s;
        std::transform(r.begin(), r.end(), r.begin(),
                       [](unsigned char c){ return std::tolower(c); });
        return r;
    }

    std::string toUpper(const std::string &s) {
        std::string r = s;
        std::transform(r.begin(), r.end(), r.begin(),
                       [](unsigned char c){ return std::toupper(c); });
        return r;
    }

    /* Split string by delimiter */
    std::vector<std::string> split(const std::string &s, char delim) {
        std::vector<std::string> tokens;
        std::istringstream iss(s);
        std::string tok;
        while (std::getline(iss, tok, delim))
            tokens.push_back(tok);
        return tokens;
    }

    /* Trim leading/trailing whitespace */
    std::string trim(const std::string &s) {
        size_t start = s.find_first_not_of(" \t\r\n");
        if (start == std::string::npos) return "";
        size_t end = s.find_last_not_of(" \t\r\n");
        return s.substr(start, end - start + 1);
    }
}

/******************************************************************************
 * LdapBer — BER Encoding
 *****************************************************************************/
void LdapBer::encodeLength(std::vector<uint8_t> &buf, size_t len) {
    if (len < 0x80) {
        buf.push_back(static_cast<uint8_t>(len));
    } else if (len <= 0xFF) {
        buf.push_back(0x81);
        buf.push_back(static_cast<uint8_t>(len));
    } else if (len <= 0xFFFF) {
        buf.push_back(0x82);
        buf.push_back(static_cast<uint8_t>(len >> 8));
        buf.push_back(static_cast<uint8_t>(len & 0xFF));
    } else if (len <= 0xFFFFFF) {
        buf.push_back(0x83);
        buf.push_back(static_cast<uint8_t>(len >> 16));
        buf.push_back(static_cast<uint8_t>((len >> 8) & 0xFF));
        buf.push_back(static_cast<uint8_t>(len & 0xFF));
    } else {
        buf.push_back(0x84);
        buf.push_back(static_cast<uint8_t>(len >> 24));
        buf.push_back(static_cast<uint8_t>((len >> 16) & 0xFF));
        buf.push_back(static_cast<uint8_t>((len >> 8) & 0xFF));
        buf.push_back(static_cast<uint8_t>(len & 0xFF));
    }
}

void LdapBer::encodeTLV(std::vector<uint8_t> &buf, uint8_t tag,
                         const std::vector<uint8_t> &value) {
    buf.push_back(tag);
    encodeLength(buf, value.size());
    buf.insert(buf.end(), value.begin(), value.end());
}

std::vector<uint8_t> LdapBer::encodeInteger(int32_t val) {
    std::vector<uint8_t> content;
    if (val >= -128 && val <= 127) {
        content.push_back(static_cast<uint8_t>(val & 0xFF));
    } else if (val >= -32768 && val <= 32767) {
        content.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
        content.push_back(static_cast<uint8_t>(val & 0xFF));
    } else if (val >= -8388608 && val <= 8388607) {
        content.push_back(static_cast<uint8_t>((val >> 16) & 0xFF));
        content.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
        content.push_back(static_cast<uint8_t>(val & 0xFF));
    } else {
        content.push_back(static_cast<uint8_t>((val >> 24) & 0xFF));
        content.push_back(static_cast<uint8_t>((val >> 16) & 0xFF));
        content.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
        content.push_back(static_cast<uint8_t>(val & 0xFF));
    }
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_INTEGER, content);
    return out;
}

std::vector<uint8_t> LdapBer::encodeEnumerated(int32_t val) {
    std::vector<uint8_t> content;
    content.push_back(static_cast<uint8_t>(val & 0xFF));
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_ENUMERATED, content);
    return out;
}

std::vector<uint8_t> LdapBer::encodeBoolean(bool val) {
    std::vector<uint8_t> content;
    content.push_back(val ? 0xFF : 0x00);
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_BOOLEAN, content);
    return out;
}

std::vector<uint8_t> LdapBer::encodeOctetString(const std::string &s) {
    std::vector<uint8_t> content(s.begin(), s.end());
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_OCTET_STRING, content);
    return out;
}

std::vector<uint8_t> LdapBer::encodeOctetString(const std::vector<uint8_t> &data) {
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_OCTET_STRING, data);
    return out;
}

std::vector<uint8_t> LdapBer::encodeSequence(const std::vector<uint8_t> &contents) {
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_SEQUENCE, contents);
    return out;
}

std::vector<uint8_t> LdapBer::encodeSet(const std::vector<uint8_t> &contents) {
    std::vector<uint8_t> out;
    encodeTLV(out, TAG_SET, contents);
    return out;
}

/******************************************************************************
 * LdapBer — BER Decoding
 *****************************************************************************/
bool LdapBer::parseTLV(const uint8_t *data, size_t len, size_t &off,
                        uint8_t &tag, const uint8_t *&content, size_t &contentLen) {
    if (off >= len) return false;

    tag = data[off++];

    if (off >= len) return false;
    uint8_t lb = data[off++];
    size_t clen = 0;

    if (lb < 0x80) {
        clen = lb;
    } else {
        int numBytes = lb & 0x7F;
        if (numBytes > 4 || off + numBytes > len) return false;
        for (int i = 0; i < numBytes; i++) {
            clen = (clen << 8) | data[off++];
        }
    }

    if (off + clen > len) return false;
    content = data + off;
    contentLen = clen;
    off += clen;
    return true;
}

bool LdapBer::parseInteger(const uint8_t *data, size_t len, int32_t &val) {
    if (len == 0 || len > 4) return false;
    val = (data[0] & 0x80) ? -1 : 0;
    for (size_t i = 0; i < len; i++)
        val = (val << 8) | data[i];
    return true;
}

bool LdapBer::parseOctetString(const uint8_t *data, size_t len, std::string &out) {
    out.assign(reinterpret_cast<const char*>(data), len);
    return true;
}

bool LdapBer::parseBoolean(const uint8_t *data, size_t len, bool &val) {
    if (len != 1) return false;
    val = (data[0] != 0);
    return true;
}

bool LdapBer::parseEnumerated(const uint8_t *data, size_t len, int32_t &val) {
    return parseInteger(data, len, val);
}

/******************************************************************************
 * LdapEntry
 *****************************************************************************/
void LdapEntry::addAttribute(const std::string &type, const std::string &value) {
    for (auto &attr : attributes) {
        if (toLower(attr.type) == toLower(type)) {
            attr.values.push_back(value);
            return;
        }
    }
    LdapAttribute a;
    a.type = type;
    a.values.push_back(value);
    attributes.push_back(std::move(a));
}

void LdapEntry::addAttribute(const std::string &type, const std::vector<std::string> &values) {
    for (auto &v : values)
        addAttribute(type, v);
}

/******************************************************************************
 * LdapFilter
 *****************************************************************************/
LdapFilter::LdapFilter() : tag(LDAP_FILTER_PRESENT) {}

bool LdapFilter::matches(const LdapEntry &entry) const {
    switch (tag) {
    case LDAP_FILTER_PRESENT: {
        /* attribute must exist */
        std::string lattr = toLower(attrType);
        for (auto &a : entry.attributes) {
            if (toLower(a.type) == lattr && !a.values.empty())
                return true;
        }
        return false;
    }
    case LDAP_FILTER_EQUALITY: {
        std::string lattr = toLower(attrType);
        std::string lval = toLower(attrValue);
        for (auto &a : entry.attributes) {
            if (toLower(a.type) == lattr) {
                for (auto &v : a.values) {
                    if (toLower(v) == lval)
                        return true;
                }
            }
        }
        return false;
    }
    case LDAP_FILTER_AND: {
        for (auto &child : children) {
            if (!child->matches(entry))
                return false;
        }
        return true;
    }
    case LDAP_FILTER_OR: {
        for (auto &child : children) {
            if (child->matches(entry))
                return true;
        }
        return children.empty();
    }
    case LDAP_FILTER_NOT: {
        if (children.empty()) return true;
        return !children[0]->matches(entry);
    }
    default:
        /* Unsupported filter types: match everything */
        return true;
    }
}

/******************************************************************************
 * netplus event proxy (same pattern as KdcEvent in krb5 plugin)
 *****************************************************************************/
namespace {
    class AdEvent : public netplus::event {
    public:
        AdEvent(std::vector<netplus::socket*> sockets, ActiveDirectory *ad)
            : netplus::event(sockets), _ad(ad) {}

        void RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override {
            _ad->RequestEvent(curcon, tid, args);
        }
        void ResponseEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override {
            _ad->ResponseEvent(curcon, tid, args);
        }
        void ConnectEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override {
            _ad->ConnectEvent(curcon, tid, args);
        }
        void DisconnectEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override {
            _ad->DisconnectEvent(curcon, tid, args);
        }
        void CreateConnection(std::shared_ptr<netplus::con> &res) override {
            _ad->CreateConnection(res);
        }
    private:
        ActiveDirectory *_ad;
    };
}

/******************************************************************************
 * ActiveDirectory — Construction / Destruction
 *****************************************************************************/
ActiveDirectory::ActiveDirectory(AuthBackend &adminBackend,
                                 const std::string &bindAddr,
                                 int port, int maxconn)
    : _AdminBackend(adminBackend),
      _BindAddr(bindAddr), _Port(port), _MaxConn(maxconn),
      _Event(nullptr)
{
    loadDomains();

    netplus::tcp *ssock = new netplus::tcp(bindAddr.c_str(), port, maxconn, 0);
    _ServerSockets.push_back(ssock);

    std::cerr << "AD/LDAP plugin listening on " << bindAddr << ":" << port << std::endl;
}

void ActiveDirectory::init() {
    for (auto *s : _ServerSockets) {
        s->bind();
        s->setNonBlock();
        s->listen();
    }
}

ActiveDirectory::~ActiveDirectory() {
    delete static_cast<AdEvent*>(_Event);
    for (auto *s : _ServerSockets)
        delete s;
}

void ActiveDirectory::run() {
    auto *ev = new AdEvent(_ServerSockets, this);
    _Event = ev;
    ev->runEventloop(0);
}

/******************************************************************************
 * Domain Enumeration
 *****************************************************************************/
void ActiveDirectory::loadDomains() {
    std::lock_guard<std::mutex> lock(_Lock);
    _Domains.clear();

    Domain domain;
    size_t rd = sizeof(authdb::AuthHeader), end = _AdminBackend.end();

    while (rd < end) {
        std::shared_ptr<authdb::AuthData::Record> cur =
            std::make_shared<authdb::AuthData::Record>();
        cur->type = EmptyData;
        _AdminBackend.setPos(rd);
        _AdminBackend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
        rd = _AdminBackend.getPos() + cur->datasize;

        if (cur->type == DataType::DomainData &&
            strcmp(cur->fieldname, "domainname") == 0) {
            class DomainData ddat(cur->ruid);
            try {
                size_t dpos = sizeof(authdb::AuthHeader);
                domain.info(_AdminBackend, ddat, dpos);

                DomainInfo di;
                di.domainId = uuid::uuid(cur->ruid);
                di.domainName = ddat.getDomainName();
                di.baseDN = domainToBaseDN(di.domainName);
                di.storageType = ddat.getStorageType();
                di.storageOptions = ddat.getStorageOptions();

                if (di.domainName.empty())
                    continue;

                std::cerr << "AD: domain '" << di.domainName
                          << "' → " << di.baseDN << std::endl;
                _Domains.push_back(std::move(di));
            } catch (AuthBackendError &e) {
                std::cerr << "AD: domain load error: " << e.what() << std::endl;
            }
        }
    }

    /* Always include admin.local */
    DomainInfo admin;
    admin.domainName = "admin.local";
    admin.baseDN = domainToBaseDN("admin.local");
    admin.storageType = -1; /* sentinel: use admin backend directly */
    _Domains.push_back(std::move(admin));
}

/******************************************************************************
 * DN / Domain Utilities
 *****************************************************************************/
std::string ActiveDirectory::domainToBaseDN(const std::string &domain) {
    auto parts = split(domain, '.');
    std::string dn;
    for (size_t i = 0; i < parts.size(); i++) {
        if (i > 0) dn += ",";
        dn += "DC=" + parts[i];
    }
    return dn;
}

std::string ActiveDirectory::normalizeDN(const std::string &dn) {
    return toLower(dn);
}

std::string ActiveDirectory::baseDN(const std::string &domain) const {
    return domainToBaseDN(domain);
}

AuthBackend *ActiveDirectory::resolveBackendFromDN(const std::string &dn,
                                                   std::string &domainName) {
    std::string ndn = normalizeDN(dn);

    for (auto &di : _Domains) {
        std::string nbase = normalizeDN(di.baseDN);
        /* Check if dn ends with the base DN */
        if (ndn == nbase ||
            (ndn.size() > nbase.size() &&
             ndn.substr(ndn.size() - nbase.size()) == nbase &&
             ndn[ndn.size() - nbase.size() - 1] == ',')) {
            domainName = di.domainName;
            if (di.storageType == -1) {
                /* admin.local */
                return &_AdminBackend;
            }
            /* Open the domain backend */
            DomainBackend dbackend(_AdminBackend);
            AuthBackend *bck = dbackend.data(di.domainId);
            return bck;
        }
    }
    return nullptr;
}

/******************************************************************************
 * netplus event callbacks
 *****************************************************************************/
void ActiveDirectory::CreateConnection(std::shared_ptr<netplus::con> &res) {
    res = std::make_shared<netplus::con>(this);
}

void ActiveDirectory::ConnectEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /*args*/) {
    std::lock_guard<std::mutex> lock(_Lock);
    _Sessions[&curcon] = LdapSession();
}

void ActiveDirectory::DisconnectEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /*args*/) {
    std::lock_guard<std::mutex> lock(_Lock);
    _Sessions.erase(&curcon);
}

void ActiveDirectory::ResponseEvent(netplus::con &/*curcon*/, const int /*tid*/, ULONG_PTR /*args*/) {
    /* nothing */
}

void ActiveDirectory::RequestEvent(netplus::con &curcon, const int /*tid*/, ULONG_PTR /*args*/) {
    /*
     * LDAP uses raw BER over TCP — no length prefix like Kerberos.
     * Each LDAPMessage is a BER SEQUENCE.  We parse the outer envelope,
     * dispatch, and accumulate response(s).
     */
    if (curcon.RecvData.empty())
        return;

    LdapSession *session = nullptr;
    {
        std::lock_guard<std::mutex> lock(_Lock);
        auto it = _Sessions.find(&curcon);
        if (it != _Sessions.end())
            session = &it->second;
    }
    if (!session) return;

    const uint8_t *raw = reinterpret_cast<const uint8_t*>(curcon.RecvData.data());
    size_t rawLen = curcon.RecvData.size();
    size_t consumed = 0;

    while (consumed < rawLen) {
        /* Parse outer SEQUENCE envelope */
        size_t off = consumed;
        uint8_t outerTag = 0;
        const uint8_t *msgContent = nullptr;
        size_t msgLen = 0;

        if (!LdapBer::parseTLV(raw, rawLen, off, outerTag, msgContent, msgLen))
            break;

        if (outerTag != LdapBer::TAG_SEQUENCE)
            break;

        std::vector<uint8_t> response = handleMessage(msgContent, msgLen, *session);
        if (!response.empty()) {
            curcon.SendData.insert(curcon.SendData.end(), response.begin(), response.end());
        }
        consumed = off;
    }

    if (consumed > 0) {
        curcon.RecvData.erase(curcon.RecvData.begin(),
                              curcon.RecvData.begin() + consumed);
    }
}

/******************************************************************************
 * LDAP Message Dispatch
 *****************************************************************************/
std::vector<uint8_t> ActiveDirectory::handleMessage(const uint8_t *data, size_t len,
                                                    LdapSession &session) {
    size_t off = 0;
    uint8_t tag;
    const uint8_t *content;
    size_t contentLen;

    /* messageID (INTEGER) */
    if (!LdapBer::parseTLV(data, len, off, tag, content, contentLen))
        return {};
    int32_t msgId = 0;
    if (tag != LdapBer::TAG_INTEGER || !LdapBer::parseInteger(content, contentLen, msgId))
        return {};

    /* protocolOp (APPLICATION tag) */
    if (!LdapBer::parseTLV(data, len, off, tag, content, contentLen))
        return {};

    uint8_t appTag = tag & 0x1F; /* strip class + constructed bits */

    switch (appTag) {
    case LDAP_BIND_REQUEST:
        return handleBindRequest(content, contentLen, msgId, session);

    case LDAP_UNBIND_REQUEST:
        return handleUnbindRequest(msgId, session);

    case LDAP_SEARCH_REQUEST:
        return handleSearchRequest(content, contentLen, msgId, session);

    case LDAP_ABANDON_REQUEST:
        /* silently ignore abandons */
        return {};

    default:
        /* Unsupported operation */
        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_SEARCH_RESULT_DONE,
            buildLdapResult(LDAP_UNWILLING_TO_PERFORM, "", "Operation not supported"));
    }
}

/******************************************************************************
 * LDAP Bind Request
 *****************************************************************************/
std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content, size_t len,
                                                        int32_t msgId,
                                                        LdapSession &session) {
    size_t off = 0;
    uint8_t tag;
    const uint8_t *data;
    size_t dataLen;

    /* version (INTEGER) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
            buildLdapResult(LDAP_PROTOCOL_ERROR, "", "Cannot parse version"));
    int32_t version = 0;
    LdapBer::parseInteger(data, dataLen, version);
    if (version != 3) {
        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
            buildLdapResult(LDAP_PROTOCOL_ERROR, "", "Only LDAPv3 is supported"));
    }

    /* name (OCTET STRING) — the bind DN */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
            buildLdapResult(LDAP_PROTOCOL_ERROR, "", "Cannot parse bind DN"));
    std::string bindDN;
    LdapBer::parseOctetString(data, dataLen, bindDN);

    /* authentication choice */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
            buildLdapResult(LDAP_PROTOCOL_ERROR, "", "Cannot parse authentication"));

    uint8_t authChoice = tag & 0x1F;

    if (authChoice == 0) {
        /* Simple authentication (context [0]) */
        std::string password;
        LdapBer::parseOctetString(data, dataLen, password);

        if (bindDN.empty() && password.empty()) {
            /* Anonymous bind — allowed for RootDSE queries */
            session.authenticated = false;
            session.bindDN = "";
            return buildLdapMessage(msgId,
                LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
                buildLdapResult(LDAP_SUCCESS, "", ""));
        }

        if (authenticateSimple(bindDN, password, session)) {
            return buildLdapMessage(msgId,
                LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
                buildLdapResult(LDAP_SUCCESS, "", ""));
        } else {
            return buildLdapMessage(msgId,
                LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
                buildLdapResult(LDAP_INVALID_CREDENTIALS, "", "Invalid credentials"));
        }
    } else if (authChoice == 3) {
        /* SASL authentication (context [3]) — depends on krb5 plugin */
        std::string mechanism;
        if (dataLen > 0) {
            size_t saslOff = 0;
            uint8_t saslTag;
            const uint8_t *saslData;
            size_t saslLen;
            if (LdapBer::parseTLV(data, dataLen, saslOff, saslTag, saslData, saslLen)) {
                LdapBer::parseOctetString(saslData, saslLen, mechanism);
            }
        }

        if (toLower(mechanism) == "gssapi") {
            /* GSSAPI requires the krb5 plugin to be loaded */
            if (!g_KdcPlugin) {
                return buildLdapMessage(msgId,
                    LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
                    buildLdapResult(LDAP_AUTH_METHOD_NOT_SUPPORTED, "",
                                    "GSSAPI not available (krb5 plugin not loaded)"));
            }

            /* TODO: Full GSSAPI/SPNEGO token exchange with krb5 plugin
             * For now, return that the mechanism is recognized but not yet implemented */
            return buildLdapMessage(msgId,
                LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
                buildLdapResult(LDAP_AUTH_METHOD_NOT_SUPPORTED, "",
                                "SASL GSSAPI: negotiation not yet fully implemented"));
        }

        return buildLdapMessage(msgId,
            LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
            buildLdapResult(LDAP_AUTH_METHOD_NOT_SUPPORTED, "",
                            "Unsupported SASL mechanism: " + mechanism));
    }

    return buildLdapMessage(msgId,
        LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_BIND_RESPONSE,
        buildLdapResult(LDAP_AUTH_METHOD_NOT_SUPPORTED, "",
                        "Unsupported authentication method"));
}

/******************************************************************************
 * Simple Bind Authentication
 *****************************************************************************/
bool ActiveDirectory::authenticateSimple(const std::string &bindDN,
                                         const std::string &password,
                                         LdapSession &session) {
    /*
     * Parse the DN to extract the CN (username) and base DN (domain).
     * Expected format: CN=<username>,CN=Users,DC=example,DC=com
     */
    auto parts = split(bindDN, ',');
    if (parts.empty()) return false;

    std::string username;
    std::string baseDNStr;

    /* Extract CN= value from first component */
    std::string first = trim(parts[0]);
    if (first.size() > 3 && toLower(first.substr(0, 3)) == "cn=") {
        username = first.substr(3);
    } else {
        return false;
    }

    /* Reconstruct the base DN from DC= components */
    std::string domainBaseDN;
    for (auto &p : parts) {
        std::string tp = trim(p);
        if (toLower(tp.substr(0, 3)) == "dc=") {
            if (!domainBaseDN.empty()) domainBaseDN += ",";
            domainBaseDN += tp;
        }
    }

    if (username.empty() || domainBaseDN.empty()) return false;

    /* Hash the password with SHA-512 */
    sha512 hash;
    std::string pwhash;
    hash.hash(password, pwhash);

    /* Find matching domain */
    for (auto &di : _Domains) {
        if (normalizeDN(di.baseDN) != normalizeDN(domainBaseDN))
            continue;

        AuthBackend *backend = nullptr;
        std::unique_ptr<DomainBackend> dbackendPtr;

        if (di.storageType == -1) {
            backend = &_AdminBackend;
        } else {
            dbackendPtr = std::make_unique<DomainBackend>(_AdminBackend);
            backend = dbackendPtr->data(di.domainId);
        }
        if (!backend) continue;

        /* Scan for the user */
        User user;
        size_t rd = sizeof(authdb::AuthHeader), end = backend->end();
        std::shared_ptr<authdb::AuthData::Record> cur =
            std::make_shared<authdb::AuthData::Record>();

        while (rd < end) {
            cur->type = EmptyData;
            backend->setPos(rd);
            backend->read((unsigned char*)cur.get(), sizeof(AuthData::Record));
            rd = backend->getPos() + cur->datasize;

            if (cur->type == UserData && strcmp(cur->fieldname, "username") == 0) {
                class UserData udat(cur->ruid);
                try {
                    size_t upos = sizeof(authdb::AuthHeader);
                    user.info(*backend, udat, upos);

                    if (toLower(username) == toLower(udat.getUsername()) &&
                        pwhash == udat.getPwHash()) {
                        session.authenticated = true;
                        session.bindDN = bindDN;
                        session.userUid = uuid::uuid(cur->ruid);
                        session.realm = di.domainName;
                        return true;
                    }
                } catch (...) {}
            }
        }
    }

    return false;
}

/******************************************************************************
 * LDAP Unbind
 *****************************************************************************/
std::vector<uint8_t> ActiveDirectory::handleUnbindRequest(int32_t /*msgId*/,
                                                          LdapSession &session) {
    session.authenticated = false;
    session.bindDN = "";
    /* UnbindRequest has no response */
    return {};
}

/******************************************************************************
 * LDAP Search Request
 *****************************************************************************/
std::vector<uint8_t> ActiveDirectory::handleSearchRequest(const uint8_t *content,
                                                          size_t len,
                                                          int32_t msgId,
                                                          LdapSession &session) {
    size_t off = 0;
    uint8_t tag;
    const uint8_t *data;
    size_t dataLen;

    /* baseObject (OCTET STRING) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse baseObject");
    std::string baseObject;
    LdapBer::parseOctetString(data, dataLen, baseObject);

    /* scope (ENUMERATED) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse scope");
    int32_t scopeVal = 0;
    LdapBer::parseEnumerated(data, dataLen, scopeVal);
    auto scope = static_cast<LdapSearchScope>(scopeVal);

    /* derefAliases (ENUMERATED) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse derefAliases");
    /* ignored */

    /* sizeLimit (INTEGER) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse sizeLimit");
    int32_t sizeLimit = 0;
    LdapBer::parseInteger(data, dataLen, sizeLimit);

    /* timeLimit (INTEGER) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse timeLimit");
    /* ignored */

    /* typesOnly (BOOLEAN) */
    if (!LdapBer::parseTLV(content, len, off, tag, data, dataLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse typesOnly");
    bool typesOnly = false;
    LdapBer::parseBoolean(data, dataLen, typesOnly);

    /* filter */
    if (off >= len)
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse filter");
    /* Save the filter start position and skip over it */
    size_t filterOff = off;
    uint8_t filterTag;
    const uint8_t *filterContent;
    size_t filterLen;
    if (!LdapBer::parseTLV(content, len, off, filterTag, filterContent, filterLen))
        return buildSearchResultDone(msgId, LDAP_PROTOCOL_ERROR, "Cannot parse filter");

    /* Parse filter from the raw bytes (including the tag) */
    size_t filterTotalLen = off - filterOff;
    auto filter = parseFilter(content + filterOff, filterTotalLen);
    if (!filter) {
        /* If we can't parse the filter, use a match-all filter */
        filter = std::make_shared<LdapFilter>();
        filter->tag = LDAP_FILTER_PRESENT;
        filter->attrType = "objectClass";
    }

    /* attributes (SEQUENCE OF) — requested attributes */
    std::vector<std::string> requestedAttrs;
    if (off < len) {
        if (LdapBer::parseTLV(content, len, off, tag, data, dataLen)) {
            if (tag == LdapBer::TAG_SEQUENCE) {
                size_t aoff = 0;
                while (aoff < dataLen) {
                    uint8_t atag;
                    const uint8_t *acontent;
                    size_t alen;
                    if (!LdapBer::parseTLV(data, dataLen, aoff, atag, acontent, alen))
                        break;
                    std::string attrName;
                    LdapBer::parseOctetString(acontent, alen, attrName);
                    if (!attrName.empty())
                        requestedAttrs.push_back(attrName);
                }
            }
        }
    }

    /* RootDSE query: empty base, scope base */
    if (baseObject.empty() && scope == LDAP_SCOPE_BASE) {
        LdapEntry rootDSE = buildRootDSE();
        if (filter->matches(rootDSE)) {
            std::vector<uint8_t> result;
            auto entry = buildSearchResultEntry(msgId, rootDSE);
            result.insert(result.end(), entry.begin(), entry.end());
            auto done = buildSearchResultDone(msgId, LDAP_SUCCESS);
            result.insert(result.end(), done.begin(), done.end());
            return result;
        }
        return buildSearchResultDone(msgId, LDAP_SUCCESS);
    }

    /* Find matching domain backend */
    std::vector<LdapEntry> results;

    /* For subtree searches, iterate over matching domains */
    for (auto &di : _Domains) {
        std::string nbase = normalizeDN(di.baseDN);
        std::string nobj = normalizeDN(baseObject);

        bool domainMatch = false;
        if (scope == LDAP_SCOPE_SUBTREE) {
            /* baseObject must be the baseDN or a parent/ancestor */
            domainMatch = (nobj == nbase) ||
                          (nbase.size() > nobj.size() &&
                           nbase.substr(nbase.size() - nobj.size()) == nobj &&
                           nbase[nbase.size() - nobj.size() - 1] == ',');
            /* Also match if baseObject is under the baseDN */
            if (!domainMatch) {
                domainMatch = (nobj.size() > nbase.size() &&
                               nobj.substr(nobj.size() - nbase.size()) == nbase &&
                               nobj[nobj.size() - nbase.size() - 1] == ',');
            }
        } else if (scope == LDAP_SCOPE_BASE || scope == LDAP_SCOPE_ONELEVEL) {
            domainMatch = (nobj == nbase) ||
                          (nobj.size() > nbase.size() &&
                           nobj.substr(nobj.size() - nbase.size()) == nbase &&
                           nobj[nobj.size() - nbase.size() - 1] == ',');
        }

        if (!domainMatch) continue;

        AuthBackend *backend = nullptr;
        std::unique_ptr<DomainBackend> dbackendPtr;

        if (di.storageType == -1) {
            backend = &_AdminBackend;
        } else {
            dbackendPtr = std::make_unique<DomainBackend>(_AdminBackend);
            backend = dbackendPtr->data(di.domainId);
        }
        if (!backend) continue;

        /* If base scope and baseObject == baseDN, return the domain entry */
        if (scope == LDAP_SCOPE_BASE && nobj == nbase) {
            LdapEntry domEntry;
            domEntry.dn = di.baseDN;
            domEntry.addAttribute("objectClass", std::vector<std::string>{
                "top", "domain", "domainDNS"});
            domEntry.addAttribute("dc", split(di.domainName, '.')[0]);
            domEntry.addAttribute("distinguishedName", di.baseDN);
            if (filter->matches(domEntry))
                results.push_back(std::move(domEntry));
            continue;
        }

        /* Enumerate containers for onelevel at baseDN */
        if (scope == LDAP_SCOPE_ONELEVEL && nobj == nbase) {
            enumerateContainers(di.baseDN, results);
        }

        /* Enumerate objects */
        enumerateUsers(*backend, di.baseDN, *filter, results);
        enumerateGroups(*backend, di.baseDN, *filter, results);
        enumerateClients(*backend, di.baseDN, *filter, results);
        enumerateServices(*backend, di.baseDN, *filter, results);
    }

    /* Apply size limit */
    if (sizeLimit > 0 && static_cast<int32_t>(results.size()) > sizeLimit)
        results.resize(sizeLimit);

    /* Build response */
    std::vector<uint8_t> response;
    for (auto &entry : results) {
        auto entryMsg = buildSearchResultEntry(msgId, entry);
        response.insert(response.end(), entryMsg.begin(), entryMsg.end());
    }
    auto done = buildSearchResultDone(msgId, LDAP_SUCCESS);
    response.insert(response.end(), done.begin(), done.end());
    return response;
}

/******************************************************************************
 * RootDSE
 *****************************************************************************/
LdapEntry ActiveDirectory::buildRootDSE() {
    LdapEntry entry;
    entry.dn = "";
    entry.addAttribute("objectClass", "top");
    entry.addAttribute("subschemaSubentry", "CN=Aggregate,CN=Schema,CN=Configuration");
    entry.addAttribute("supportedLDAPVersion", "3");
    entry.addAttribute("supportedSASLMechanisms", "GSSAPI");
    entry.addAttribute("vendorName", "authdb");
    entry.addAttribute("vendorVersion", "1.0");

    /* List all domain naming contexts */
    for (auto &di : _Domains) {
        entry.addAttribute("namingContexts", di.baseDN);
    }

    /* AD-specific attributes */
    if (!_Domains.empty()) {
        entry.addAttribute("defaultNamingContext", _Domains[0].baseDN);
        entry.addAttribute("rootDomainNamingContext", _Domains[0].baseDN);
    }
    entry.addAttribute("supportedControl", "1.2.840.113556.1.4.319"); /* paged results */
    entry.addAttribute("supportedCapabilities", "1.2.840.113556.1.4.800"); /* AD LDAP */

    return entry;
}

/******************************************************************************
 * Directory Enumeration — Users
 *****************************************************************************/
void ActiveDirectory::enumerateUsers(AuthBackend &backend, const std::string &baseDN,
                                     const LdapFilter &filter,
                                     std::vector<LdapEntry> &results) {
    User user;
    size_t rd = sizeof(authdb::AuthHeader), end = backend.end();
    std::shared_ptr<authdb::AuthData::Record> cur =
        std::make_shared<authdb::AuthData::Record>();

    while (rd < end) {
        cur->type = EmptyData;
        backend.setPos(rd);
        backend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
        rd = backend.getPos() + cur->datasize;

        if (cur->type == UserData && strcmp(cur->fieldname, "username") == 0) {
            class UserData udat(cur->ruid);
            try {
                size_t upos = sizeof(authdb::AuthHeader);
                user.info(backend, udat, upos);

                std::string username = udat.getUsername();
                std::string dn = "CN=" + username + ",CN=Users," + baseDN;

                LdapEntry entry;
                entry.dn = dn;
                entry.addAttribute("objectClass", std::vector<std::string>{
                    "top", "person", "organizationalPerson", "user"});
                entry.addAttribute("cn", username);
                entry.addAttribute("sAMAccountName", username);
                entry.addAttribute("distinguishedName", dn);
                entry.addAttribute("objectGUID", uuid::uuid(cur->ruid).c_str());
                entry.addAttribute("userPrincipalName",
                    username + "@" + toUpper(baseDN)); /* UPN */

                try { entry.addAttribute("givenName", udat.getFirstname()); } catch (...) {}
                try { entry.addAttribute("sn", udat.getLastname()); } catch (...) {}
                try { entry.addAttribute("mail", udat.getMail()); } catch (...) {}

                entry.addAttribute("objectCategory",
                    "CN=Person,CN=Schema,CN=Configuration," + baseDN);
                entry.addAttribute("instanceType", "4");
                entry.addAttribute("userAccountControl", "512"); /* NORMAL_ACCOUNT */

                if (filter.matches(entry))
                    results.push_back(std::move(entry));
            } catch (...) {}
        }
    }
}

/******************************************************************************
 * Directory Enumeration — Groups
 *****************************************************************************/
void ActiveDirectory::enumerateGroups(AuthBackend &backend, const std::string &baseDN,
                                      const LdapFilter &filter,
                                      std::vector<LdapEntry> &results) {
    size_t rd = sizeof(authdb::AuthHeader), end = backend.end();
    std::shared_ptr<authdb::AuthData::Record> cur =
        std::make_shared<authdb::AuthData::Record>();

    while (rd < end) {
        cur->type = EmptyData;
        backend.setPos(rd);
        backend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
        rd = backend.getPos() + cur->datasize;

        if (cur->type == DataType::GroupData && strcmp(cur->fieldname, "name") == 0) {
            class GroupData gdat(cur->ruid);
            try {
                Group group;
                size_t gpos = sizeof(authdb::AuthHeader);
                group.info(backend, gdat, gpos);

                std::string groupName = gdat.getName();
                std::string dn = "CN=" + groupName + ",CN=Groups," + baseDN;

                LdapEntry entry;
                entry.dn = dn;
                entry.addAttribute("objectClass", std::vector<std::string>{
                    "top", "group"});
                entry.addAttribute("cn", groupName);
                entry.addAttribute("sAMAccountName", groupName);
                entry.addAttribute("distinguishedName", dn);
                entry.addAttribute("objectGUID", uuid::uuid(cur->ruid).c_str());
                entry.addAttribute("objectCategory",
                    "CN=Group,CN=Schema,CN=Configuration," + baseDN);
                entry.addAttribute("groupType", "-2147483646"); /* Global security group */
                entry.addAttribute("instanceType", "4");

                /* Enumerate members */
                std::vector<uuid::uuid> members;
                gdat.getMembers(members);
                for (auto &mid : members) {
                    /* Resolve member username */
                    User user;
                    size_t mrd = sizeof(authdb::AuthHeader);
                    std::shared_ptr<authdb::AuthData::Record> mcur =
                        std::make_shared<authdb::AuthData::Record>();
                    while (mrd < end) {
                        mcur->type = EmptyData;
                        backend.setPos(mrd);
                        backend.read((unsigned char*)mcur.get(), sizeof(AuthData::Record));
                        mrd = backend.getPos() + mcur->datasize;
                        if (mcur->type == UserData &&
                            strcmp(mcur->fieldname, "username") == 0 &&
                            mid == uuid::uuid(mcur->ruid)) {
                            class UserData mudat(mcur->ruid);
                            try {
                                size_t mupos = sizeof(authdb::AuthHeader);
                                user.info(backend, mudat, mupos);
                                entry.addAttribute("member",
                                    "CN=" + mudat.getUsername() + ",CN=Users," + baseDN);
                            } catch (...) {}
                            break;
                        }
                    }
                }

                if (filter.matches(entry))
                    results.push_back(std::move(entry));
            } catch (...) {}
        }
    }
}

/******************************************************************************
 * Directory Enumeration — Clients (Computers)
 *****************************************************************************/
void ActiveDirectory::enumerateClients(AuthBackend &backend, const std::string &baseDN,
                                       const LdapFilter &filter,
                                       std::vector<LdapEntry> &results) {
    size_t rd = sizeof(authdb::AuthHeader), end = backend.end();
    std::shared_ptr<authdb::AuthData::Record> cur =
        std::make_shared<authdb::AuthData::Record>();

    while (rd < end) {
        cur->type = EmptyData;
        backend.setPos(rd);
        backend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
        rd = backend.getPos() + cur->datasize;

        if (cur->type == DataType::ClientData && strcmp(cur->fieldname, "clientname") == 0) {
            class ClientData cdat(cur->ruid);
            try {
                Client client;
                size_t cpos = sizeof(authdb::AuthHeader);
                client.info(backend, cdat, cpos);

                std::string clientName = cdat.getName();
                std::string dn = "CN=" + clientName + ",CN=Computers," + baseDN;

                LdapEntry entry;
                entry.dn = dn;
                entry.addAttribute("objectClass", std::vector<std::string>{
                    "top", "person", "organizationalPerson", "user", "computer"});
                entry.addAttribute("cn", clientName);
                entry.addAttribute("sAMAccountName", clientName + "$");
                entry.addAttribute("distinguishedName", dn);
                entry.addAttribute("objectGUID", uuid::uuid(cur->ruid).c_str());
                entry.addAttribute("dNSHostName", clientName);
                entry.addAttribute("objectCategory",
                    "CN=Computer,CN=Schema,CN=Configuration," + baseDN);
                entry.addAttribute("userAccountControl", "4096"); /* WORKSTATION_TRUST */
                entry.addAttribute("instanceType", "4");

                if (filter.matches(entry))
                    results.push_back(std::move(entry));
            } catch (...) {}
        }
    }
}

/******************************************************************************
 * Directory Enumeration — Services
 *****************************************************************************/
void ActiveDirectory::enumerateServices(AuthBackend &backend, const std::string &baseDN,
                                        const LdapFilter &filter,
                                        std::vector<LdapEntry> &results) {
    size_t rd = sizeof(authdb::AuthHeader), end = backend.end();
    std::shared_ptr<authdb::AuthData::Record> cur =
        std::make_shared<authdb::AuthData::Record>();

    while (rd < end) {
        cur->type = EmptyData;
        backend.setPos(rd);
        backend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
        rd = backend.getPos() + cur->datasize;

        if (cur->type == DataType::ServiceData &&
            strcmp(cur->fieldname, "servicename") == 0) {
            class ServiceData sdat(cur->ruid);
            try {
                ServiceManager svc;
                size_t spos = sizeof(authdb::AuthHeader);
                svc.info(backend, sdat, spos);

                std::string svcName = sdat.getServiceName();
                const char *hostnameRaw = sdat.getHostName();
                std::string hostname = hostnameRaw ? hostnameRaw : "";
                std::string cn = svcName;
                if (!hostname.empty())
                    cn += "/" + hostname;

                std::string dn = "CN=" + cn + ",CN=Services," + baseDN;

                LdapEntry entry;
                entry.dn = dn;
                entry.addAttribute("objectClass", std::vector<std::string>{
                    "top", "serviceConnectionPoint"});
                entry.addAttribute("cn", cn);
                entry.addAttribute("distinguishedName", dn);
                entry.addAttribute("objectGUID", uuid::uuid(cur->ruid).c_str());
                entry.addAttribute("servicePrincipalName",
                    svcName + "/" + hostname);
                entry.addAttribute("objectCategory",
                    "CN=ServiceConnectionPoint,CN=Schema,CN=Configuration," + baseDN);
                entry.addAttribute("instanceType", "4");

                if (filter.matches(entry))
                    results.push_back(std::move(entry));
            } catch (...) {}
        }
    }
}

/******************************************************************************
 * Directory Enumeration — Container entries (CN=Users, CN=Groups, etc.)
 *****************************************************************************/
void ActiveDirectory::enumerateContainers(const std::string &baseDN,
                                          std::vector<LdapEntry> &results) {
    static const char *containers[] = { "Users", "Groups", "Computers", "Services" };
    for (auto &c : containers) {
        LdapEntry entry;
        entry.dn = std::string("CN=") + c + "," + baseDN;
        entry.addAttribute("objectClass", std::vector<std::string>{"top", "container"});
        entry.addAttribute("cn", c);
        entry.addAttribute("distinguishedName", entry.dn);
        results.push_back(std::move(entry));
    }
}

/******************************************************************************
 * Filter Parsing (BER)
 *****************************************************************************/
std::shared_ptr<LdapFilter> ActiveDirectory::parseFilter(const uint8_t *data, size_t len) {
    if (len == 0) return nullptr;

    size_t off = 0;
    uint8_t tag;
    const uint8_t *content;
    size_t contentLen;

    if (!LdapBer::parseTLV(data, len, off, tag, content, contentLen))
        return nullptr;

    auto filter = std::make_shared<LdapFilter>();

    uint8_t tagClass = tag & 0xC0;
    uint8_t tagNum = tag & 0x1F;

    if (tagClass == LdapBer::CLASS_CONTEXT) {
        filter->tag = static_cast<LdapFilterTag>(tagNum);

        switch (filter->tag) {
        case LDAP_FILTER_AND:
        case LDAP_FILTER_OR: {
            /* SET OF Filter */
            size_t coff = 0;
            while (coff < contentLen) {
                size_t childStart = coff;
                /* Find the end of this child TLV */
                uint8_t ctag;
                const uint8_t *ccontent;
                size_t cclen;
                size_t peekOff = coff;
                if (!LdapBer::parseTLV(content, contentLen, peekOff, ctag, ccontent, cclen))
                    break;
                auto child = parseFilter(content + childStart, peekOff - childStart);
                if (child)
                    filter->children.push_back(child);
                coff = peekOff;
            }
            break;
        }
        case LDAP_FILTER_NOT: {
            auto child = parseFilter(content, contentLen);
            if (child)
                filter->children.push_back(child);
            break;
        }
        case LDAP_FILTER_EQUALITY:
        case LDAP_FILTER_GE:
        case LDAP_FILTER_LE:
        case LDAP_FILTER_APPROX: {
            /* AttributeValueAssertion: SEQUENCE { attr, value } */
            size_t aoff = 0;
            uint8_t atag;
            const uint8_t *acontent;
            size_t alen;
            if (LdapBer::parseTLV(content, contentLen, aoff, atag, acontent, alen))
                LdapBer::parseOctetString(acontent, alen, filter->attrType);
            if (LdapBer::parseTLV(content, contentLen, aoff, atag, acontent, alen))
                LdapBer::parseOctetString(acontent, alen, filter->attrValue);
            break;
        }
        case LDAP_FILTER_PRESENT: {
            /* Implicit primitive: content is the attribute name */
            filter->attrType.assign(reinterpret_cast<const char*>(content), contentLen);
            break;
        }
        case LDAP_FILTER_SUBSTRINGS:
        default:
            /* Treat as present(objectClass) for unsupported */
            filter->tag = LDAP_FILTER_PRESENT;
            filter->attrType = "objectClass";
            break;
        }
    } else {
        /* Unexpected tag class — match all */
        filter->tag = LDAP_FILTER_PRESENT;
        filter->attrType = "objectClass";
    }

    return filter;
}

/******************************************************************************
 * LDAP Response Builders
 *****************************************************************************/
std::vector<uint8_t> ActiveDirectory::buildLdapMessage(int32_t msgId, uint8_t appTag,
                                                       const std::vector<uint8_t> &protocolOp) {
    std::vector<uint8_t> body;
    auto msgIdEnc = LdapBer::encodeInteger(msgId);
    body.insert(body.end(), msgIdEnc.begin(), msgIdEnc.end());

    /* protocolOp wrapped in application tag */
    body.push_back(appTag);
    LdapBer::encodeLength(body, protocolOp.size());
    body.insert(body.end(), protocolOp.begin(), protocolOp.end());

    return LdapBer::encodeSequence(body);
}

std::vector<uint8_t> ActiveDirectory::buildLdapResult(LdapResultCode code,
                                                      const std::string &matchedDN,
                                                      const std::string &diagnosticMsg) {
    std::vector<uint8_t> result;
    auto resCode = LdapBer::encodeEnumerated(static_cast<int32_t>(code));
    result.insert(result.end(), resCode.begin(), resCode.end());
    auto dn = LdapBer::encodeOctetString(matchedDN);
    result.insert(result.end(), dn.begin(), dn.end());
    auto msg = LdapBer::encodeOctetString(diagnosticMsg);
    result.insert(result.end(), msg.begin(), msg.end());
    return result;
}

std::vector<uint8_t> ActiveDirectory::buildSearchResultEntry(int32_t msgId,
                                                             const LdapEntry &entry) {
    std::vector<uint8_t> body;

    /* objectName (OCTET STRING) */
    auto dnEnc = LdapBer::encodeOctetString(entry.dn);
    body.insert(body.end(), dnEnc.begin(), dnEnc.end());

    /* attributes (SEQUENCE OF PartialAttribute) */
    std::vector<uint8_t> attrList;
    for (auto &attr : entry.attributes) {
        std::vector<uint8_t> partialAttr;

        /* type (OCTET STRING) */
        auto typeEnc = LdapBer::encodeOctetString(attr.type);
        partialAttr.insert(partialAttr.end(), typeEnc.begin(), typeEnc.end());

        /* vals (SET OF OCTET STRING) */
        std::vector<uint8_t> vals;
        for (auto &v : attr.values) {
            auto vEnc = LdapBer::encodeOctetString(v);
            vals.insert(vals.end(), vEnc.begin(), vEnc.end());
        }
        auto valsEnc = LdapBer::encodeSet(vals);
        partialAttr.insert(partialAttr.end(), valsEnc.begin(), valsEnc.end());

        auto paEnc = LdapBer::encodeSequence(partialAttr);
        attrList.insert(attrList.end(), paEnc.begin(), paEnc.end());
    }
    auto attrSeq = LdapBer::encodeSequence(attrList);
    body.insert(body.end(), attrSeq.begin(), attrSeq.end());

    return buildLdapMessage(msgId,
        LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_SEARCH_RESULT_ENTRY, body);
}

std::vector<uint8_t> ActiveDirectory::buildSearchResultDone(int32_t msgId,
                                                            LdapResultCode code,
                                                            const std::string &msg) {
    return buildLdapMessage(msgId,
        LdapBer::CLASS_APPLICATION | LdapBer::CONSTRUCTED | LDAP_SEARCH_RESULT_DONE,
        buildLdapResult(code, "", msg));
}

} /* namespace authdb */

/******************************************************************************
 * C factory functions for generic plugin loading
 *****************************************************************************/
#include <json-c/json.h>

extern "C" {

authdb::Plugin *authdb_plugin_create(authdb::AuthBackend &backend,
                                      const char *configJson) {
    std::string bindAddr = "0.0.0.0";
    int port = 389;
    int maxconn = 128;

    if (configJson) {
        json_object *jobj = json_tokener_parse(configJson);
        if (jobj) {
            json_object *jbind = json_object_object_get(jobj, "ad_bind");
            json_object *jport = json_object_object_get(jobj, "ad_port");
            json_object *jmax  = json_object_object_get(jobj, "ad_maxconn");
            if (!jbind) jbind = json_object_object_get(jobj, "bind");
            if (jbind) bindAddr = json_object_get_string(jbind);
            if (jport) port     = json_object_get_int(jport);
            if (jmax)  maxconn  = json_object_get_int(jmax);
            json_object_put(jobj);
        }
    }

    return new authdb::ActiveDirectory(backend, bindAddr, port, maxconn);
}

void authdb_plugin_destroy(authdb::Plugin *p) {
    delete p;
}

} /* extern "C" */
