import { randomUUID } from 'crypto';
import ldap from 'ldapjs';
import { hashPassword } from './password.js';

let syncRunning = false;

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

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

function searchAsync(client, base, options) {
  return new Promise((resolve, reject) => {
    const results = [];
    client.search(base, options, (err, res) => {
      if (err) return reject(err);
      res.on('searchEntry', (entry) => results.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(results);
      });
    });
  });
}

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 recordLdapLastSync(dbSync) {
  const iso = new Date().toISOString();
  dbSync.prepare(
    `INSERT INTO app_settings (key, value) VALUES ('ldap_last_sync_at', ?)
     ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
  ).run(iso);
}

function insertLog(dbSync, row) {
  dbSync.prepare(
    `INSERT INTO ldap_sync_log (id, started_at, finished_at, trigger_type, status, users_synced, error_message)
     VALUES (?, ?, ?, ?, ?, ?, ?)`,
  ).run(
    row.id,
    row.startedAt,
    row.finishedAt,
    row.triggerType,
    row.status,
    row.usersSynced,
    row.errorMessage,
  );
}

/**
 * Entspricht der Synchronisation in Stundenerfassung (ldap-service.js):
 * Vor-/Nachname aus LDAP, nur Einträge mit Benutzername + Vor- + Nachname,
 * case-insensitive Abgleich, Standard-Passwort-Hash für neu angelegte Konten.
 *
 * @param {import('node:sqlite').DatabaseSync} dbSync
 * @param {() => object} loadIntegrations
 * @param {'manual' | 'automatic'} trigger
 */
export async function performLdapSync(dbSync, loadIntegrations, trigger) {
  if (syncRunning) {
    return { skipped: true, message: 'Synchronisation läuft bereits.' };
  }
  syncRunning = true;
  const logId = randomUUID();
  const startedAt = new Date().toISOString();
  let usersSynced = 0;
  let errorMessage = null;
  /** @type {string[]} */
  const rowErrors = [];
  let client = null;

  try {
    const config = loadIntegrations().ldap || {};
    if (!config.syncEnabled) {
      throw new Error('LDAP-Synchronisation ist nicht aktiviert');
    }

    const serverUrl = String(config.serverUrl || '').trim();
    const searchBase = String(config.searchBase || '').trim();
    const filterRaw = String(
      config.userSearchFilter || config.userFilter || '',
    ).trim();
    const filter = filterRaw || '(objectClass=person)';
    const usernameAttr = String(
      config.usernameAttribute || 'sAMAccountName',
    ).trim();
    const firstAttr = String(
      config.firstNameAttribute || 'givenName',
    ).trim();
    const lastAttr = String(config.lastNameAttribute || 'sn').trim();

    if (!serverUrl) throw new Error('LDAP-Server URL fehlt.');
    if (!searchBase) throw new Error('Base DN fehlt.');

    client = ldap.createClient({
      url: serverUrl,
      timeout: 10000,
      connectTimeout: 10000,
      reconnect: false,
    });

    client.on('error', () => {});

    const bindDn = String(config.bindDn || '').trim();
    const bindPassword =
      config.bindPassword != null ? String(config.bindPassword) : '';

    await bindAsync(client, bindDn, bindPassword);

    const entries = await searchAsync(client, searchBase, {
      scope: 'sub',
      filter,
      attributes: [usernameAttr, firstAttr, lastAttr],
    });

    await unbindAsync(client);
    client = null;

    const defaultPwHash = await hashPassword('changeme123');

    /** @type {{ username: string, firstname: string, lastname: string, dn: string }[]} */
    const ldapUsers = [];
    for (const entry of entries) {
      const dn = entryDn(entry);
      const rawUser = getAttr(entry, usernameAttr);
      const firstname = getAttr(entry, firstAttr);
      const lastname = getAttr(entry, lastAttr);
      if (!rawUser || !firstname || !lastname) continue;
      const username = String(rawUser).trim().toLowerCase();
      if (!username) continue;
      ldapUsers.push({ username, firstname, lastname, dn });
    }

    dbSync.exec('BEGIN');
    try {
      for (const u of ldapUsers) {
        try {
          const existing = dbSync
            .prepare(
              'SELECT id, source FROM users WHERE username = ? COLLATE NOCASE',
            )
            .get(u.username);
          if (existing) {
            if (existing.source === 'ldap') {
              dbSync
                .prepare(
                  `UPDATE users SET firstname = ?, lastname = ?, ldap_dn = ?, updated_at = datetime('now')
                   WHERE id = ?`,
                )
                .run(u.firstname, u.lastname, u.dn || null, existing.id);
            } else {
              dbSync
                .prepare(
                  `UPDATE users SET firstname = ?, lastname = ?, updated_at = datetime('now')
                   WHERE id = ?`,
                )
                .run(u.firstname, u.lastname, existing.id);
            }
            usersSynced += 1;
          } else {
            const id = randomUUID();
            dbSync
              .prepare(
                `INSERT INTO users (id, username, password_hash, role, source, ldap_dn, firstname, lastname, active, updated_at)
                 VALUES (?, ?, ?, 'after_sales', 'ldap', ?, ?, ?, 1, datetime('now'))`,
              )
              .run(
                id,
                u.username,
                defaultPwHash,
                u.dn || null,
                u.firstname,
                u.lastname,
              );
            usersSynced += 1;
          }
        } catch (e) {
          const msg =
            e && e.message ? String(e.message) : String(e);
          rowErrors.push(`Fehler bei ${u.username}: ${msg}`);
        }
      }
      dbSync.exec('COMMIT');
      recordLdapLastSync(dbSync);
    } catch (e) {
      try {
        dbSync.exec('ROLLBACK');
      } catch {
        /* ignore */
      }
      throw e;
    }

    if (rowErrors.length > 0) {
      errorMessage = rowErrors.join('; ');
    }
  } catch (e) {
    errorMessage = e && e.message ? String(e.message) : String(e);
    if (client) {
      try {
        await unbindAsync(client);
      } catch {
        /* ignore */
      }
      client = null;
    }
  } finally {
    try {
      const finishedAt = new Date().toISOString();
      const hasRowErr = rowErrors.length > 0;
      const status = errorMessage || hasRowErr ? 'error' : 'success';
      const logErr =
        errorMessage || (hasRowErr ? rowErrors.join('; ') : null);
      insertLog(dbSync, {
        id: logId,
        startedAt,
        finishedAt,
        triggerType: trigger,
        status,
        usersSynced,
        errorMessage: logErr,
      });
    } finally {
      syncRunning = false;
    }
  }

  const hasRowErr = rowErrors.length > 0;
  if (errorMessage && !hasRowErr) {
    return { ok: false, usersSynced: 0, error: errorMessage, errors: [] };
  }
  if (hasRowErr) {
    return {
      ok: false,
      usersSynced,
      error: errorMessage || rowErrors.join('; '),
      errors: rowErrors,
    };
  }
  return { ok: true, usersSynced, errors: [] };
}

/**
 * @param {import('node:sqlite').DatabaseSync} dbSync
 */
export function getSyncStatus(dbSync) {
  const lastSetting = dbSync
    .prepare("SELECT value FROM app_settings WHERE key = 'ldap_last_sync_at'")
    .get();
  const lastLog = dbSync
    .prepare(
      `SELECT finished_at FROM ldap_sync_log ORDER BY finished_at DESC LIMIT 1`,
    )
    .get();
  const lastSyncAt = lastSetting?.value ?? lastLog?.finished_at ?? null;
  const entries = dbSync
    .prepare(
      `SELECT finished_at AS finishedAt, trigger_type AS triggerType, status, users_synced AS usersSynced, error_message AS errorMessage
       FROM ldap_sync_log ORDER BY finished_at DESC LIMIT 10`,
    )
    .all();
  return {
    lastSyncAt,
    entries,
  };
}
