const ldap = require('ldapjs');
const axios = require('axios');

// Configuration
const LDAP_PORT = 3893;
const KEYCLOAK_URL = '';
const KEYCLOAK_REALM = '';
const KEYCLOAK_CLIENT_ID = '';
const KEYCLOAK_CLIENT_SECRET = '';
const BASE_DN = '';

// Create LDAP server
const server = ldap.createServer();

// Helper function to parse DN and extract username
function extractUsername(dn) {
    try {
        const parsed = ldap.parseDN(dn);
        // rdns is an array in some versions, need to handle it properly
        if (parsed.rdns && Array.isArray(parsed.rdns)) {
            for (const rdn of parsed.rdns) {
                if (rdn.attrs) {
                    for (const attr of rdn.attrs) {
                        if (attr.type === 'cn' || attr.type === 'uid') {
                            return attr.value;
                        }
                    }
                }
            }
        }
        // Fallback: try to extract from string
        const match = dn.match(/(?:cn|uid)=([^,]+)/i);
        return match ? match[1] : null;
    } catch (error) {
        console.error('Error parsing DN:', error);
        // Fallback: simple regex extraction
        const match = dn.toString().match(/(?:cn|uid)=([^,]+)/i);
        return match ? match[1] : null;
    }
}

// Authenticate against Keycloak using Resource Owner Password Credentials flow
async function authenticateWithKeycloak(username, password) {
    try {
        const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`;
        
        const params = new URLSearchParams();
        params.append('grant_type', 'password');
        params.append('client_id', KEYCLOAK_CLIENT_ID);
        params.append('client_secret', KEYCLOAK_CLIENT_SECRET);
        params.append('username', username);
        params.append('password', password);
        
        const response = await axios.post(tokenUrl, params, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        });
        
        return response.status === 200 && response.data.access_token;
    } catch (error) {
        console.error(`Auth failed for ${username}:`, error.response?.data || error.message);
        return false;
    }
}

// Handle BIND requests (authentication)
server.bind(BASE_DN, async (req, res, next) => {
    const username = extractUsername(req.dn.toString());
    const password = req.credentials;
    
    console.log(`Bind attempt for: ${username}`);
    
    if (!username || !password) {
        console.log('Missing username or password');
        return next(new ldap.InvalidCredentialsError());
    }
    
    const authenticated = await authenticateWithKeycloak(username, password);
    
    if (authenticated) {
        console.log(`Successfully authenticated: ${username}`);
        res.end();
        return next();
    } else {
        console.log(`Authentication failed for: ${username}`);
        return next(new ldap.InvalidCredentialsError());
    }
});

// Handle SEARCH requests (required by some LDAP clients)
server.search(BASE_DN, (req, res, next) => {
    console.log(`Search request: ${req.dn.toString()}, filter: ${req.filter.toString()}`);
    
    // Return empty results - we only care about authentication
    res.end();
    return next();
});

// Handle UNBIND
server.unbind((req, res, next) => {
    res.end();
    return next();
});

// Start server
server.listen(LDAP_PORT, '0.0.0.0', () => {
    console.log(`LDAP-OAuth2 bridge listening on port ${LDAP_PORT}`);
    console.log(`Keycloak URL: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}`);
});

// Error handling
server.on('error', (err) => {
    console.error('Server error:', err);
});
