/**
 * LDAP-Anmeldung analog zu Stundenerfassung (services/ldap-service.js):
 * Service-Bind, Benutzersuche (exakt + Varianten ß/ss), Bind mit Nutzerpasswort.
 */
import ldap from 'ldapjs';

function escapeLDAPFilter(value) {
  if (!value) return '';
  return String(value)
    .replace(/\\/g, '\\5c')
    .replace(/\*/g, '\\2a')
    .replace(/\(/g, '\\28')
    .replace(/\)/g, '\\29')
    .replace(/\0/g, '\\00');
}

function getUsernameSearchVariants(usernameStr) {
  const variants = new Set();
  variants.add(usernameStr);
  variants.add(usernameStr.toLowerCase());
  variants.add(usernameStr.replace(/\u00df/g, 'ss'));
  variants.add(usernameStr.replace(/ss/g, '\u00df'));
  return [...variants];
}

function unescapeLdapDN(dn) {
  if (!dn || typeof dn !== 'string') return dn;
  let result = '';
  const bytes = [];
  let i = 0;
  while (i < dn.length) {
    if (dn[i] === '\\' && i + 2 < dn.length && /^[0-9a-fA-F]{2}$/.test(dn.slice(i + 1, i + 3))) {
      bytes.push(parseInt(dn.slice(i + 1, i + 3), 16));
      i += 3;
    } else {
      if (bytes.length > 0) {
        result += Buffer.from(bytes).toString('utf8');
        bytes.length = 0;
      }
      result += dn[i];
      i++;
    }
  }
  if (bytes.length > 0) {
    result += Buffer.from(bytes).toString('utf8');
  }
  return result;
}

function getAttributeValue(entry, attributeName) {
  const want = String(attributeName || '').toLowerCase();
  for (const attr of entry.attributes || []) {
    if (attr.type.toLowerCase() === want) {
      const value = Array.isArray(attr.values) ? attr.values[0] : attr.values;
      return value != null ? String(value) : null;
    }
  }
  return null;
}

function entryUserDn(entry) {
  try {
    if (entry.dn != null) return entry.dn.toString();
    if (entry.objectName != null) return entry.objectName.toString();
  } catch {
    /* ignore */
  }
  return '';
}

function bindAsync(client, dn, password) {
  return new Promise((resolve, reject) => {
    client.bind(dn || '', password ?? '', (err) => (err ? reject(err) : resolve()));
  });
}

function unbindAsync(client) {
  return new Promise((resolve) => {
    try {
      client.unbind(() => resolve());
    } catch {
      resolve();
    }
  });
}

function searchWithEvents(client, baseDN, options) {
  return new Promise((resolve, reject) => {
    client.search(baseDN, options, (err, res) => {
      if (err) return reject(err);
      const entries = [];
      res.on('searchEntry', (entry) => entries.push(entry));
      res.on('error', reject);
      res.on('end', (result) => {
        if (result && result.status !== 0) {
          return reject(new Error(`LDAP-Suche fehlgeschlagen: ${result.status}`));
        }
        resolve(entries);
      });
    });
  });
}

/**
 * @param {string} usernameInput Roher Benutzername (Trim, kein toLowerCase — für UTF-8/AD)
 * @param {string} password
 * @param {() => object} loadIntegrations
 * @returns {Promise<{ ok: true, canonicalUsername: string } | { ok: false, skipLdap?: true, ldapAttempted?: true, message?: string }>}
 */
export async function authenticateLdap(usernameInput, password, loadIntegrations) {
  const usernameStr = String(usernameInput || '').trim();
  if (!usernameStr) {
    return {
      ok: false,
      ldapAttempted: true,
      message: 'Benutzername darf nicht leer sein',
    };
  }

  const raw = loadIntegrations().ldap || {};
  if (!raw.syncEnabled) {
    return { ok: false, skipLdap: true };
  }

  const url = String(raw.serverUrl || '').trim();
  const baseDN = String(raw.searchBase || '').trim();
  if (!url || !baseDN) {
    return { ok: false, skipLdap: true };
  }

  const usernameAttr = String(raw.usernameAttribute || 'sAMAccountName').trim();

  const client = ldap.createClient({
    url,
    timeout: 10000,
    connectTimeout: 10000,
    reconnect: false,
  });
  client.on('error', () => {});

  try {
    await bindAsync(
      client,
      String(raw.bindDn || '').trim(),
      raw.bindPassword != null ? String(raw.bindPassword) : '',
    );
  } catch (e) {
    await unbindAsync(client);
    return {
      ok: false,
      ldapAttempted: true,
      message: e && e.message ? String(e.message) : String(e),
    };
  }

  const exactFilter = `(${usernameAttr}=${escapeLDAPFilter(usernameStr)})`;
  let userDN = null;
  let canonicalUsername = null;

  const applyEntries = (entries, fallbackName) => {
    for (const entry of entries) {
      const dn = entryUserDn(entry);
      if (dn) {
        userDN = dn;
        canonicalUsername =
          getAttributeValue(entry, usernameAttr) || fallbackName;
      }
    }
  };

  const runVariantSearch = async () => {
    userDN = null;
    canonicalUsername = null;
    const variants = getUsernameSearchVariants(usernameStr);
    const filterParts = variants.map(
      (v) => `(${usernameAttr}=${escapeLDAPFilter(v)})`,
    );
    const searchFilter =
      filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`;
    const entries = await searchWithEvents(client, baseDN, {
      filter: searchFilter,
      scope: 'sub',
      attributes: ['dn', usernameAttr],
    });
    applyEntries(entries, usernameStr);
  };

  try {
    const entries = await searchWithEvents(client, baseDN, {
      filter: exactFilter,
      scope: 'sub',
      attributes: ['dn', usernameAttr],
    });
    applyEntries(entries, usernameStr);
  } catch {
    try {
      await runVariantSearch();
    } catch (e) {
      await unbindAsync(client);
      return {
        ok: false,
        ldapAttempted: true,
        message: e && e.message ? String(e.message) : String(e),
      };
    }
  }

  if (!userDN) {
    try {
      await runVariantSearch();
    } catch (e) {
      await unbindAsync(client);
      return {
        ok: false,
        ldapAttempted: true,
        message: e && e.message ? String(e.message) : String(e),
      };
    }
  }

  if (!userDN) {
    await unbindAsync(client);
    return {
      ok: false,
      ldapAttempted: true,
      message: `Benutzer „${usernameStr}“ nicht gefunden. Prüfen Sie den Benutzernamen (z. B. UTF-8-Zeichen).`,
    };
  }

  await unbindAsync(client);

  const bindDN = unescapeLdapDN(userDN);
  const authClient = ldap.createClient({
    url,
    timeout: 10000,
    connectTimeout: 10000,
    reconnect: false,
  });

  try {
    await new Promise((resolve, reject) => {
      authClient.on('error', (err) => {
        try {
          authClient.unbind();
        } catch {
          /* ignore */
        }
        reject(err);
      });
      authClient.bind(bindDN, password, (err) => {
        if (err) {
          const errorMsg = err.message || String(err);
          try {
            authClient.unbind();
          } catch {
            /* ignore */
          }
          return reject(
            new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`),
          );
        }
        authClient.unbind(() => resolve());
      });
    });
  } catch (e) {
    return {
      ok: false,
      ldapAttempted: true,
      message: e && e.message ? String(e.message) : String(e),
    };
  }

  return {
    ok: true,
    canonicalUsername: canonicalUsername || usernameStr,
  };
}
