/**
 * RFC 2136 Dynamic DNS Provider Adapter
 *
 * Manages DNS records via RFC 2136 dynamic updates using the nsupdate CLI tool.
 * Compatible with BIND, PowerDNS, Windows DNS, and any RFC 2136-compliant server.
 *
 * Capabilities: create-record, delete-record, resolve, credentials
 * Not supported: logs, restart, update-check, list-records, zones
 */

const { execFile } = require('child_process');
const { promisify } = require('util');
const dns = require('dns');
const os = require('os');
const path = require('path');
const fs = require('fs');

const execFileAsync = promisify(execFile);

const BaseDNSProvider = require('./base');

const CAPABILITIES = ['create-record', 'delete-record', 'resolve', 'credentials'];

const DEFAULT_PORT = 53;
const DEFAULT_TSIG_ALGORITHM = 'hmac-sha256';
const NSUPDATE_TIMEOUT_MS = 15000;

class RFC2136Provider extends BaseDNSProvider {
  static providerId = 'rfc2136';
  static displayName = 'RFC 2136 (Dynamic DNS)';

  constructor(config, ctx) {
    super(config, ctx);

    this.providerId = 'rfc2136';
    this.displayName = 'RFC 2136 (Dynamic DNS)';

    // Core config
    this.server = config.server || null;
    this.port = config.port || DEFAULT_PORT;
    this.zone = config.zone || null;

    // TSIG authentication
    this.tsigAlgorithm = config.tsigAlgorithm || DEFAULT_TSIG_ALGORITHM;
    this.tsigKeyName = config.tsigKeyName || null;
    this.tsigSecret = config.tsigSecret || null;

    // Resolve credentials from credential manager if available
    if (ctx && ctx.credentialManager) {
      if (!this.tsigKeyName && ctx.credentialManager.get) {
        this.tsigKeyName = ctx.credentialManager.get('rfc2136_tsigKeyName') || null;
      }
      if (!this.tsigSecret && ctx.credentialManager.get) {
        this.tsigSecret = ctx.credentialManager.get('rfc2136_tsigSecret') || null;
      }
    }

    // Logger shorthand
    this._log = ctx && ctx.log ? ctx.ctx : null;
  }

  // ── Logging helper ────────────────────────────────────────────────────────

  _log(level, message, meta) {
    if (this.ctx && this.ctx.log && typeof this.ctx.log[level] === 'function') {
      this.ctx.log[level](`[rfc2136] ${message}`, meta || {});
    }
  }

  // ── Capabilities ──────────────────────────────────────────────────────────

  supportsCapability(cap) {
    return CAPABILITIES.includes(cap);
  }

  getCapabilities() {
    return [...CAPABILITIES];
  }

  // ── Config validation ─────────────────────────────────────────────────────

  validateConfig() {
    const errors = [];
    if (!this.server) errors.push('Missing required config: server');
    if (!this.zone) errors.push('Missing required config: zone');
    return { valid: errors.length === 0, errors };
  }

  // ── Helpers ───────────────────────────────────────────────────────────────

  /**
   * Ensure a domain name ends with a trailing dot (FQDN for nsupdate).
   */
  _ensureFqdn(domain) {
    if (!domain) return domain;
    return domain.endsWith('.') ? domain : `${domain}.`;
  }

  /**
   * Build the common nsupdate header lines (server, zone, key).
   */
  _buildHeader() {
    const lines = [];
    lines.push(`server ${this.server} ${this.port}`);
    lines.push(`zone ${this.zone}`);

    if (this.tsigKeyName && this.tsigSecret) {
      lines.push(`key ${this.tsigAlgorithm}:${this.tsigKeyName} ${this.tsigSecret}`);
    }

    return lines;
  }

  /**
   * Execute an nsupdate script and return { stdout, stderr }.
   * Writes commands to a temporary file and runs `nsupdate <file>`.
   */
  async _runNsupdate(commands) {
    const script = commands.join('\n') + '\n';
    const tmpFile = path.join(os.tmpdir(), `nsupdate-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.cmd`);

    try {
      await fs.promises.writeFile(tmpFile, script, { mode: 0o600 });
      this._log('debug', `Executing nsupdate script`, { script: script.trim() });

      const { stdout, stderr } = await execFileAsync('nsupdate', [tmpFile], {
        timeout: NSUPDATE_TIMEOUT_MS,
        maxBuffer: 1024 * 1024,
      });

      this._log('debug', 'nsupdate completed', { stdout: (stdout || '').trim(), stderr: (stderr || '').trim() });

      if (stderr && stderr.toLowerCase().includes('refused')) {
        throw new Error(`nsupdate refused: ${stderr.trim()}`);
      }
      if (stderr && stderr.toLowerCase().includes('failed')) {
        throw new Error(`nsupdate failed: ${stderr.trim()}`);
      }

      return { stdout: (stdout || '').trim(), stderr: (stderr || '').trim() };
    } catch (err) {
      if (err.code === 'ENOENT') {
        throw new Error('nsupdate command not found. Install bind9utils (Debian/Ubuntu) or bind-utils (RHEL/CentOS).');
      }
      throw err;
    } finally {
      try { await fs.promises.unlink(tmpFile); } catch (_) { /* ignore */ }
    }
  }

  // ── Authenticate ──────────────────────────────────────────────────────────

  /**
   * Verify nsupdate is available and optionally test connectivity.
   * Runs a minimal nsupdate with just "show" (no-op) to confirm the tool works.
   */
  async authenticate() {
    const validation = this.validateConfig();
    if (!validation.valid) {
      throw new Error(`RFC 2136 config invalid: ${validation.errors.join('; ')}`);
    }

    // Check nsupdate binary is available with a dry-run command set
    const commands = [
      ...this._buildHeader(),
      'show',
    ];

    try {
      const { stdout } = await this._runNsupdate(commands);
      this._log('info', 'Authenticated to RFC 2136 server', { server: this.server, port: this.port });
      return { success: true, server: this.server, port: this.port };
    } catch (err) {
      this._log('error', 'Authentication test failed', { error: err.message });
      // If nsupdate is missing, rethrow immediately
      if (err.message.includes('not found')) throw err;
      // Otherwise, the server might be unreachable but the tool works — return partial
      return { success: false, error: err.message, server: this.server };
    }
  }

  // ── Create Record ─────────────────────────────────────────────────────────

  /**
   * Create (add) a DNS record via RFC 2136 UPDATE.
   *
   * @param {Object} params
   * @param {string} params.domain  - Record name (e.g. "www.example.com")
   * @param {string} params.zone    - Zone name (overrides constructor zone)
   * @param {string} params.type    - Record type (A, AAAA, CNAME, TXT, etc.)
   * @param {string} params.value   - Record value
   * @param {number} [params.ttl=300] - TTL in seconds
   */
  async createRecord({ domain, zone, type, value, ttl }) {
    const effectiveZone = zone || this.zone;
    const effectiveTtl = ttl || 300;
    const fqdn = this._ensureFqdn(domain);

    const commands = [
      `server ${this.server} ${this.port}`,
      `zone ${effectiveZone}`,
    ];

    if (this.tsigKeyName && this.tsigSecret) {
      commands.push(`key ${this.tsigAlgorithm}:${this.tsigKeyName} ${this.tsigSecret}`);
    }

    commands.push(`update add ${fqdn} ${effectiveTtl} ${type} ${value}`);
    commands.push('show');
    commands.push('send');

    this._log('info', 'Creating DNS record', { domain: fqdn, type, value, ttl: effectiveTtl });

    const result = await this._runNsupdate(commands);

    return {
      success: true,
      action: 'create-record',
      domain: fqdn,
      type,
      value,
      ttl: effectiveTtl,
      zone: effectiveZone,
      raw: result.stdout,
    };
  }

  // ── Delete Record ─────────────────────────────────────────────────────────

  /**
   * Delete a DNS record via RFC 2136 UPDATE.
   *
   * @param {Object} params
   * @param {string} params.domain - Record name
   * @param {string} params.type   - Record type
   * @param {string} [params.value] - Optional specific value to match
   */
  async deleteRecord({ domain, type, value }) {
    const effectiveZone = this.zone;
    const fqdn = this._ensureFqdn(domain);

    const commands = [
      `server ${this.server} ${this.port}`,
      `zone ${effectiveZone}`,
    ];

    if (this.tsigKeyName && this.tsigSecret) {
      commands.push(`key ${this.tsigAlgorithm}:${this.tsigKeyName} ${this.tsigSecret}`);
    }

    // "update delete" with value removes that specific RR;
    // without value it removes all RRs of that type for the name.
    const deleteClause = value
      ? `update delete ${fqdn} ${type} ${value}`
      : `update delete ${fqdn} ${type}`;

    commands.push(deleteClause);
    commands.push('show');
    commands.push('send');

    this._log('info', 'Deleting DNS record', { domain: fqdn, type, value: value || '(all)' });

    const result = await this._runNsupdate(commands);

    return {
      success: true,
      action: 'delete-record',
      domain: fqdn,
      type,
      value: value || null,
      zone: effectiveZone,
      raw: result.stdout,
    };
  }

  // ── Resolve Records ───────────────────────────────────────────────────────

  /**
   * Resolve DNS records for a domain.
   * First attempts dig against the configured server, then falls back to Node dns module.
   *
   * @param {Object} params
   * @param {string} params.domain - Domain to resolve
   * @param {string} [params.zone] - Zone (unused for resolution, kept for interface consistency)
   * @param {string} [params.type='A'] - Record type to query
   */
  async resolveRecords({ domain, zone, type }) {
    const queryType = type || 'A';
    const fqdn = domain.endsWith('.') ? domain : domain;

    // Strategy 1: Use dig against the configured RFC 2136 server
    try {
      const { stdout } = await execFileAsync('dig', [
        `@${this.server}`,
        '-p', String(this.port),
        fqdn,
        queryType,
        '+short',
        '+time=5',
        '+tries=1',
      ], { timeout: 10000 });

      const records = stdout
        .split('\n')
        .map(line => line.trim())
        .filter(Boolean);

      if (records.length > 0) {
        this._log('debug', `Resolved ${fqdn} ${queryType} via dig`, { records });
        return {
          domain: fqdn,
          type: queryType,
          records: records.map(r => ({ value: r, type: queryType })),
          source: 'dig',
          server: this.server,
        };
      }
    } catch (err) {
      this._log('warn', 'dig resolution failed, falling back to Node dns', { error: err.message });
    }

    // Strategy 2: Fallback to Node.js built-in resolver
    try {
      const resolver = new dns.Resolver();
      resolver.setServers([this.server]);

      const resolveMethod = this._getResolveMethod(queryType);
      const resolveAsync = promisify(resolver[resolveMethod]).bind(resolver);

      const results = await resolveAsync(fqdn);
      const records = Array.isArray(results) ? results : [results];

      this._log('debug', `Resolved ${fqdn} ${queryType} via Node dns`, { records });

      return {
        domain: fqdn,
        type: queryType,
        records: records.map(r => ({ value: String(r), type: queryType })),
        source: 'node-dns',
        server: this.server,
      };
    } catch (err) {
      this._log('warn', 'Node dns resolution also failed', { error: err.message });
      return {
        domain: fqdn,
        type: queryType,
        records: [],
        source: 'none',
        server: this.server,
        error: err.message,
      };
    }
  }

  /**
   * Map record type to the Node dns resolver method name.
   */
  _getResolveMethod(type) {
    const map = {
      A: 'resolve4',
      AAAA: 'resolve6',
      CNAME: 'resolveCname',
      MX: 'resolveMx',
      TXT: 'resolveTxt',
      NS: 'resolveNs',
      SOA: 'resolveSoa',
      SRV: 'resolveSrv',
      PTR: 'reverse',
    };
    return map[(type || '').toUpperCase()] || 'resolve4';
  }

  // ── Shutdown ──────────────────────────────────────────────────────────────

  async shutdown() {
    this._log('info', 'RFC 2136 provider shutting down');
  }
}

// Expose providerId on the prototype so the registry's auto-discover can detect it
RFC2136Provider.prototype.providerId = 'rfc2136';

module.exports = RFC2136Provider;
