/**
 * Security Host Registry
 *
 * Tracks every "location" that reports security events into the central
 * DashCaddy instance. A host can be:
 *   - The current DashCaddy node itself ("self")
 *   - Another DashCaddy install (in the future, when we add DCA-agent)
 *   - A service location like a NAS or a remote Docker host
 *   - An IP range / CIDR / domain representing a service that runs elsewhere
 *
 * Each host has:
 *   - id         : stable identifier (slug)
 *   - label      : human-readable name
 *   - type       : "self" | "dashcaddy" | "service" | "agent"
 *   - api_key    : per-host API key for ingest auth (HMAC-signed, stored hashed)
 *   - registered_at, last_seen_at
 *   - meta       : free-form metadata (location, region, tags, etc.)
 *   - enabled    : soft-disable flag (stops accepting events)
 *
 * Persistence: /data/security-hosts.json (atomic write via tmp+rename).
 *
 * Auth on the ingest endpoint: the request must include
 *   Authorization: Bearer <host.api_key>
 * matching a registered, enabled host. We verify by hashing the presented key
 * and comparing to the stored hash.
 */

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const platformPaths = require('../../platform-paths');

const HOSTS_FILE = process.env.SECURITY_HOSTS_FILE
  || path.join(platformPaths.dataDir, 'security-hosts.json');

const KEY_PREFIX = 'dca_'; // DashCaddy Agent key prefix — easy to spot in logs

class HostRegistry {
  constructor(opts = {}) {
    this.filePath = opts.filePath || HOSTS_FILE;
    this.log      = opts.log || console;
    this.hosts    = new Map(); // id -> host record (without raw api_key)
    this._keyHash = new Map(); // api_key_hash -> host_id  (for O(1) ingest auth lookup)
    this._load();
  }

  _load() {
    try {
      if (!fs.existsSync(this.filePath)) {
        // First run — register the self host automatically
        this._registerSelf();
        this._save();
        return;
      }
      const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
      for (const h of raw.hosts || []) {
        this.hosts.set(h.id, h);
        if (h.api_key_hash) this._keyHash.set(h.api_key_hash, h.id);
      }
      this.log.info?.('security', 'host registry loaded', { count: this.hosts.size });
    } catch (e) {
      this.log.error?.('security', 'host registry load failed', { error: e.message });
    }
  }

  _registerSelf() {
    const hostname = require('os').hostname();
    const apiKey = KEY_PREFIX + crypto.randomBytes(24).toString('base64url');
    const hash = this._hashKey(apiKey);
    const host = {
      id: 'self',
      label: hostname,
      type: 'self',
      registered_at: new Date().toISOString(),
      last_seen_at:   new Date().toISOString(),
      meta: { hostname },
      enabled: true,
      api_key_hash: hash,
      // We do NOT persist the raw api_key for self — it's only used for ingest from self.
      // For self-ingest we call _selfKey() at runtime. For other hosts we surface the key
      // exactly once at registration time.
      _raw_key: apiKey,
    };
    this.hosts.set('self', host);
    this._keyHash.set(hash, 'self');
    this.log.info?.('security', 'registered self host', { id: host.id, label: host.label });
  }

  /**
   * HMAC-SHA256 of the api key with a per-install pepper.
   * Pepper is loaded from /data/.security-pepper if present, else a fixed default.
   * The default pepper is NOT secret — it's just to make rainbow-table attacks on the
   * stored hash harder if someone gets the file. Real auth security comes from
   * not leaking the file (file mode 0600, root-only).
   */
  _pepper() {
    const pepperFile = path.join(platformPaths.dataDir, '.security-pepper');
    try {
      if (fs.existsSync(pepperFile)) return fs.readFileSync(pepperFile, 'utf8').trim();
    } catch {}
    return 'dashcaddy-default-pepper';
  }

  _hashKey(key) {
    return crypto.createHmac('sha256', this._pepper()).update(key).digest('hex');
  }

  /**
   * Register a new host. Returns the record and the raw api_key (only chance
   * the caller will see it — they must store it on their side).
   */
  register({ id, label, type = 'service', meta = {}, enabled = true }) {
    if (!id || typeof id !== 'string') throw new Error('id required');
    if (this.hosts.has(id)) throw new Error(`host ${id} already registered`);
    const apiKey = KEY_PREFIX + crypto.randomBytes(24).toString('base64url');
    const hash = this._hashKey(apiKey);
    const host = {
      id,
      label: label || id,
      type,
      registered_at: new Date().toISOString(),
      last_seen_at:   null,
      meta,
      enabled,
      api_key_hash: hash,
    };
    this.hosts.set(id, host);
    this._keyHash.set(hash, id);
    this._save();
    return { host: this._public(host), api_key: apiKey };
  }

  /**
   * Authenticate an incoming ingest request by api key.
   * Returns the host record on success, null on failure.
   * Updates last_seen_at on success.
   */
  authenticate(apiKey) {
    if (!apiKey || typeof apiKey !== 'string') return null;
    const hash = this._hashKey(apiKey);
    const id = this._keyHash.get(hash);
    if (!id) return null;
    const host = this.hosts.get(id);
    if (!host || !host.enabled) return null;
    host.last_seen_at = new Date().toISOString();
    // Don't save on every auth — that's a lot of writes. Persist periodically.
    this._maybeSave();
    return this._public(host);
  }

  /**
   * Look up the self-host's api key for internal use (the DashCaddy process
   * authenticating to itself when emitting events from log parsers, etc.).
   */
  selfApiKey() {
    const self = this.hosts.get('self');
    return self ? self._raw_key : null;
  }

  get(id) {
    const h = this.hosts.get(id);
    return h ? this._public(h) : null;
  }

  list() {
    return Array.from(this.hosts.values()).map(h => this._public(h));
  }

  update(id, patch) {
    const h = this.hosts.get(id);
    if (!h) return null;
    // Allowed mutable fields
    const allowed = ['label', 'type', 'meta', 'enabled'];
    for (const k of allowed) {
      if (k in patch) h[k] = patch[k];
    }
    this._save();
    return this._public(h);
  }

  remove(id) {
    if (id === 'self') throw new Error('cannot remove self host');
    const h = this.hosts.get(id);
    if (!h) return false;
    this.hosts.delete(id);
    if (h.api_key_hash) this._keyHash.delete(h.api_key_hash);
    this._save();
    return true;
  }

  _public(h) {
    // Strip the raw key + hash from anything returned externally
    const { _raw_key, api_key_hash, ...pub } = h;
    return pub;
  }

  _maybeSave() {
    // Coalesce: only save at most once per 5 seconds under auth load
    const now = Date.now();
    if (this._lastSave && (now - this._lastSave) < 5000) return;
    this._lastSave = now;
    this._save();
  }

  _save() {
    const data = { hosts: Array.from(this.hosts.values()) };
    const tmp = this.filePath + '.tmp';
    try {
      fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
      fs.renameSync(tmp, this.filePath);
    } catch (e) {
      this.log.error?.('security', 'host registry save failed', { error: e.message });
    }
  }
}

let _instance = null;
function getRegistry(opts) {
  if (_instance) return _instance;
  _instance = new HostRegistry(opts);
  return _instance;
}

module.exports = { HostRegistry, getRegistry };