/**
 * Technitium DNS Server Provider Adapter
 *
 * Wraps Technitium-specific DNS logic into the standard adapter interface.
 * Uses the Technitium HTTP API (default port 5380) for all operations.
 */
const BaseDNSProvider = require('./base');

const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24-hour token lifetime

class TechnitiumDNSProvider extends BaseDNSProvider {
  constructor(config, ctx) {
    super(config, ctx);
    this.providerId = 'technitium';
    this.displayName = 'Technitium DNS Server';

    this.serverIp = config.serverIp;
    this.serverPort = config.serverPort || 5380;
    this.dnsId = config.dnsId || null;

    // Token state
    this.token = null;
    this.tokenExpiry = null;
  }

  // ---------------------------------------------------------------------------
  // Capabilities
  // ---------------------------------------------------------------------------

  static CAPABILITIES = [
    'create-record',
    'delete-record',
    'resolve',
    'list-records',
    'logs',
    'restart',
    'update-check',
    'credentials',
    'zones'
  ];

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

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

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  /** Build the base URL for this server */
  _baseUrl() {
    return `http://${this.serverIp}:${this.serverPort}`;
  }

  /** Build a full API URL with query-string params */
  _buildUrl(apiPath, params = {}) {
    const qs = new URLSearchParams(params).toString();
    return `${this._baseUrl()}${apiPath}${qs ? '?' + qs : ''}`;
  }

  /** Ensure we have a valid token; throws on failure */
  async _requireToken() {
    // Re-use existing token if still valid
    if (this.token && this.tokenExpiry && new Date() < new Date(this.tokenExpiry)) {
      return this.token;
    }
    const result = await this.authenticate();
    if (!result.success) {
      const err = new Error('No valid DNS token available. ' + (result.error || ''));
      err.statusCode = 401;
      throw err;
    }
    return this.token;
  }

  // ---------------------------------------------------------------------------
  // Authentication
  // ---------------------------------------------------------------------------

  /**
   * Authenticate against the Technitium server.
   * Checks per-server credentials first (dns.{dnsId}.readonly.username),
   * then falls back to global credentials (dns.username).
   *
   * Stores token + expiry on success.
   */
  async authenticate() {
    const { credentialManager, log } = this.ctx;

    // Try per-server credentials first
    if (this.dnsId) {
      for (const role of ['readonly', 'admin']) {
        try {
          const username = await credentialManager.retrieve(`dns.${this.dnsId}.${role}.username`);
          const password = await credentialManager.retrieve(`dns.${this.dnsId}.${role}.password`);
          if (username && password) {
            const result = await this._doLogin(username, password);
            if (result.success) return result;
          }
        } catch (err) {
          log.error('technitium', `Per-server ${role} credential error`, {
            dnsId: this.dnsId,
            error: err.message
          });
        }
      }
    }

    // Fall back to global credentials
    try {
      const username = await credentialManager.retrieve('dns.username');
      const password = await credentialManager.retrieve('dns.password');
      if (username && password) {
        return await this._doLogin(username, password);
      }
    } catch (err) {
      log.error('technitium', 'Global credential error', { error: err.message });
    }

    return {
      success: false,
      error: 'No DNS credentials configured. Please set up credentials via /api/dns/credentials'
    };
  }

  /**
   * Perform the actual login POST to Technitium.
   * Stores token on success.
   */
  async _doLogin(username, password) {
    const { fetchT, log } = this.ctx;

    try {
      const params = new URLSearchParams({
        user: username,
        pass: password,
        includeInfo: 'false'
      });

      const url = `${this._baseUrl()}/api/user/login?${params.toString()}`;
      const response = await fetchT(url, {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      });

      const result = await response.json();

      if (result.status === 'ok' && result.token) {
        this.token = result.token;
        this.tokenExpiry = new Date(Date.now() + SESSION_TTL_MS).toISOString();
        log.info('technitium', 'DNS token obtained', {
          server: this.serverIp,
          expires: this.tokenExpiry
        });
        return { success: true, token: this.token };
      }

      return { success: false, error: result.errorMessage || 'Login failed' };
    } catch (error) {
      log.error('technitium', 'Login error', { error: error.message });
      return { success: false, error: error.message };
    }
  }

  // ---------------------------------------------------------------------------
  // Record Management
  // ---------------------------------------------------------------------------

  /**
   * Create (or overwrite) a DNS record.
   * GET /api/zones/records/add?token=...&domain=...&zone=...&type=...&ipAddress=...&ttl=...&overwrite=...
   */
  async createRecord({ domain, zone, type, value, ttl, overwrite }) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    const params = {
      token,
      domain,
      zone,
      type: type || 'A',
      ipAddress: value,
      ttl: String(ttl || 300),
      overwrite: String(overwrite !== false)
    };

    try {
      log.info('technitium', 'Creating DNS record', { domain, type, value });
      const url = this._buildUrl('/api/zones/records/add', params);
      const response = await fetchT(url, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const result = await response.json();

      if (result.status === 'ok') {
        log.info('technitium', 'DNS record created', { domain, type, value });
        return { success: true };
      }

      // If token expired, re-authenticate and retry once
      if (result.errorMessage && result.errorMessage.toLowerCase().includes('token')) {
        log.info('technitium', 'Token expired, re-authenticating');
        this.token = null;
        this.tokenExpiry = null;
        const retryToken = await this._requireToken();
        params.token = retryToken;
        const retryUrl = this._buildUrl('/api/zones/records/add', params);
        const retryResp = await fetchT(retryUrl, {
          method: 'GET',
          headers: { 'Accept': 'application/json' }
        });
        const retryResult = await retryResp.json();
        if (retryResult.status === 'ok') {
          return { success: true };
        }
        throw new Error(retryResult.errorMessage || 'Failed after token refresh');
      }

      throw new Error(result.errorMessage || 'Unknown error');
    } catch (error) {
      throw new Error(`Failed to create DNS record for ${domain}: ${error.message}`);
    }
  }

  /**
   * Delete a DNS record.
   * GET /api/zones/records/delete?token=...&domain=...&type=...  (+ ipAddress if value provided)
   */
  async deleteRecord({ domain, type, value }) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    const params = {
      token,
      domain,
      type: type || 'A'
    };
    if (value) {
      params.ipAddress = value;
    }

    try {
      log.info('technitium', 'Deleting DNS record', { domain, type, value });
      const url = this._buildUrl('/api/zones/records/delete', params);
      const response = await fetchT(url, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const result = await response.json();

      if (result.status === 'ok') {
        log.info('technitium', 'DNS record deleted', { domain, type, value });
        return { success: true };
      }

      throw new Error(result.errorMessage || 'Unknown error');
    } catch (error) {
      throw new Error(`Failed to delete DNS record for ${domain}: ${error.message}`);
    }
  }

  /**
   * Resolve/query records for a domain in a zone.
   * GET /api/zones/records/get?token=...&domain=...&zone=...&listZone=true
   * Filters returned records by type if provided.
   */
  async resolveRecords({ domain, zone, type }) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    const params = {
      token,
      domain,
      zone,
      listZone: 'true'
    };

    try {
      log.info('technitium', 'Resolving records', { domain, zone, type });
      const url = this._buildUrl('/api/zones/records/get', params);
      const response = await fetchT(url, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const result = await response.json();

      if (result.status !== 'ok') {
        throw new Error(result.errorMessage || 'Failed to resolve records');
      }

      let records = (result.response && result.response.records) || [];

      // Filter by type if specified
      if (type) {
        records = records.filter(r => r.type === type);
      }

      return { success: true, records };
    } catch (error) {
      throw new Error(`Failed to resolve records for ${domain}: ${error.message}`);
    }
  }

  /**
   * List all records in a zone.
   * Delegates to resolveRecords with a wildcard domain.
   */
  async listRecords({ zone }) {
    return this.resolveRecords({ domain: zone, zone, type: null });
  }

  // ---------------------------------------------------------------------------
  // Logs
  // ---------------------------------------------------------------------------

  /**
   * Fetch and parse DNS query logs.
   * 1. GET /api/logs/list to discover the latest log file
   * 2. GET /api/logs/download?token=...&fileName=... to download it
   * 3. Parse text format: [timestamp] [client:port] [protocol] QNAME: domain; QTYPE: type; QCLASS: class; RCODE: rcode; ANSWER: [answer]
   */
  async getLogs({ limit, server } = {}) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    const targetIp = server || this.serverIp;
    const targetPort = this.serverPort;
    const baseUrl = `http://${targetIp}:${targetPort}`;

    try {
      // Step 1: Get log file list
      const listUrl = this._buildUrl('/api/logs/list', { token });
      const listResp = await fetchT(listUrl.replace(this._baseUrl(), baseUrl), {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const listResult = await listResp.json();

      if (listResult.status !== 'ok' || !listResult.response || !listResult.response.length) {
        throw new Error(listResult.errorMessage || 'No log files found');
      }

      // Pick the latest log file (last entry)
      const logFile = listResult.response[listResult.response.length - 1];
      const fileName = logFile.name || logFile.fileName || logFile;

      // Step 2: Download the log file
      const downloadUrl = `${baseUrl}/api/logs/download?${new URLSearchParams({ token, fileName }).toString()}`;
      const downloadResp = await fetchT(downloadUrl, {
        method: 'GET'
      });
      const logText = await downloadResp.text();

      // Step 3: Parse lines
      const parsed = this._parseLogText(logText, limit);
      return { success: true, logs: parsed };
    } catch (error) {
      log.error('technitium', 'Failed to fetch DNS logs', { error: error.message });
      throw new Error(`Failed to get DNS logs: ${error.message}`);
    }
  }

  /**
   * Parse Technitium DNS log text format.
   * Line format: [timestamp] [client:port] [protocol] QNAME: domain; QTYPE: type; QCLASS: class; RCODE: rcode; ANSWER: [answer]
   */
  _parseLogText(text, limit) {
    const lines = text.split('\n').filter(l => l.trim());
    const parsed = [];

    // Process newest first if we need to limit
    const iterable = limit ? lines.slice(-limit).reverse() : lines;

    for (const line of iterable) {
      try {
        const entry = {};

        // Extract timestamp: [2024-01-15 10:30:45]
        const tsMatch = line.match(/\[([^\]]+)\]/);
        if (tsMatch) entry.timestamp = tsMatch[1];

        // Extract client:port: [192.168.1.100:12345]
        const clientMatch = line.match(/\[([^\]]+:\d+)\]/g);
        if (clientMatch && clientMatch.length >= 2) {
          entry.client = clientMatch[1].replace(/\[|\]/g, '');
        }

        // Extract protocol: [UDP] or [TCP]
        const protoMatch = line.match(/\]\s*\[(UDP|TCP|DoH|DoT|DoH2)\]/i);
        if (protoMatch) entry.protocol = protoMatch[1];

        // Extract key-value pairs: QNAME: value; QTYPE: value; etc.
        const kvPattern = /(\w+):\s*([^;]+)/g;
        let match;
        while ((match = kvPattern.exec(line)) !== null) {
          const key = match[1];
          const val = match[2].trim();
          if (['QNAME', 'QTYPE', 'QCLASS', 'RCODE'].includes(key)) {
            entry[key.toLowerCase()] = val;
          } else if (key === 'ANSWER') {
            entry.answer = val;
          }
        }

        entry.raw = line;
        parsed.push(entry);
      } catch {
        // Skip unparseable lines
      }
    }

    return parsed;
  }

  // ---------------------------------------------------------------------------
  // Server Management
  // ---------------------------------------------------------------------------

  /**
   * Restart the DNS server.
   * POST /api/admin/restart?token=...
   * Requires admin credentials.
   */
  async restartServer({ server } = {}) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    try {
      log.info('technitium', 'Restarting DNS server', { server: this.serverIp });
      const url = this._buildUrl('/api/admin/restart', { token });
      const response = await fetchT(url, {
        method: 'POST',
        headers: { 'Accept': 'application/json' }
      });
      const result = await response.json();

      if (result.status === 'ok') {
        log.info('technitium', 'DNS server restart initiated');
        return { success: true, message: 'Server restart initiated' };
      }

      throw new Error(result.errorMessage || 'Restart failed');
    } catch (error) {
      log.error('technitium', 'DNS restart error', { error: error.message });
      throw new Error(`Failed to restart DNS server: ${error.message}`);
    }
  }

  /**
   * Check for DNS server updates.
   * GET /api/user/checkForUpdate?token=...
   */
  async checkUpdate({ server } = {}) {
    const token = await this._requireToken();
    const { fetchT, log } = this.ctx;

    try {
      log.info('technitium', 'Checking for DNS server update', { server: this.serverIp });
      const url = this._buildUrl('/api/user/checkForUpdate', { token });
      const response = await fetchT(url, {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
      });
      const result = await response.json();

      if (result.status === 'ok') {
        return {
          success: true,
          updateAvailable: !!(result.response && result.response.updateAvailable),
          latestVersion: (result.response && result.response.latestVersion) || null,
          currentVersion: (result.response && result.response.currentVersion) || null,
          response: result.response
        };
      }

      throw new Error(result.errorMessage || 'Update check failed');
    } catch (error) {
      log.error('technitium', 'Update check error', { error: error.message });
      throw new Error(`Failed to check for updates: ${error.message}`);
    }
  }

  // ---------------------------------------------------------------------------
  // Config Validation
  // ---------------------------------------------------------------------------

  validateConfig() {
    const errors = [];
    if (!this.serverIp) {
      errors.push('serverIp is required');
    }
    if (this.serverPort && (typeof this.serverPort !== 'number' || this.serverPort < 1 || this.serverPort > 65535)) {
      errors.push('serverPort must be a valid port number (1-65535)');
    }
    return { valid: errors.length === 0, errors };
  }
}

module.exports = TechnitiumDNSProvider;
