import { randomUUID } from 'crypto';
import ldap from 'ldapjs';

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 = attr.values[0];
      return v != null ? String(v).trim() : '';
    }
  }
  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', () => 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 insertLog(db, row) {
  db.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,
  );
}

/**
 * @param {import('node:sqlite').DatabaseSync} db
 * @param {() => object} loadIntegrations
 * @param {'manual' | 'automatic'} trigger
 */
export async function performLdapSync(db, 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;
  let client = null;

  try {
    const config = loadIntegrations().ldap || {};
    const serverUrl = String(config.serverUrl || '').trim();
    const searchBase = String(config.searchBase || '').trim();
    const filter = String(
      config.userSearchFilter || config.userFilter || '',
    ).trim();
    const usernameAttr = String(
      config.usernameAttribute || 'sAMAccountName',
    ).trim();

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

    client = ldap.createClient({
      url: serverUrl,
      timeout: 120000,
      connectTimeout: 20000,
      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],
    });

    await unbindAsync(client);
    client = null;

    db.exec('BEGIN');
    try {
      let n = 0;
      for (const entry of entries) {
        let dn;
        try {
          dn = entry.objectName.toString();
        } catch {
          continue;
        }
        const un = getAttr(entry, usernameAttr).toLowerCase().trim();
        if (!un) continue;

        const existing = db
          .prepare('SELECT id, source FROM users WHERE username = ?')
          .get(un);
        if (existing) {
          if (existing.source === 'local') continue;
          db.prepare(
            `UPDATE users SET ldap_dn = ?, updated_at = datetime('now') WHERE id = ?`,
          ).run(dn, existing.id);
          n += 1;
        } else {
          const id = randomUUID();
          db.prepare(
            `INSERT INTO users (id, username, password_hash, role, source, ldap_dn, active, updated_at)
             VALUES (?, ?, NULL, 'user', 'ldap', ?, 1, datetime('now'))`,
          ).run(id, un, dn);
          n += 1;
        }
      }
      db.exec('COMMIT');
      usersSynced = n;
    } catch (e) {
      try {
        db.exec('ROLLBACK');
      } catch {
        /* ignore */
      }
      throw e;
    }
  } 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 status = errorMessage ? 'error' : 'success';
      insertLog(db, {
        id: logId,
        startedAt,
        finishedAt,
        triggerType: trigger,
        status,
        usersSynced: errorMessage ? 0 : usersSynced,
        errorMessage,
      });
    } finally {
      syncRunning = false;
    }
  }

  if (errorMessage) {
    return { ok: false, usersSynced: 0, error: errorMessage };
  }
  return { ok: true, usersSynced };
}

/**
 * @param {import('node:sqlite').DatabaseSync} db
 */
export function getSyncStatus(db) {
  const last = db
    .prepare(
      `SELECT finished_at FROM ldap_sync_log ORDER BY finished_at DESC LIMIT 1`,
    )
    .get();
  const entries = db
    .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: last?.finished_at ?? null,
    entries,
  };
}
