/**
 * Provider-aware DNS Context
 * Replaces the Technitium-only context with a provider-agnostic layer.
 * Delegates to the active DNS provider adapter based on config.
 *
 * Falls back to legacy Technitium context for backward compatibility
 * when no provider is explicitly configured.
 */
const { createCache, CACHE_CONFIGS } = require('../utilities/cache-config');
const { TIMEOUTS, SESSION_TTL, CADDY } = require('../utilities/constants');
const registry = require('../../dns-providers/registry');

// Per-server token cache (legacy Technitium)
const dnsServerTokens = createCache(CACHE_CONFIGS.dnsTokens);
let dnsToken = '';
let dnsTokenExpiry = null;

/**
 * Create a provider-aware DNS context.
 * This wraps both the new provider system and the legacy Technitium context
 * for seamless migration.
 */
function createProviderDnsContext(siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log, DNS_CREDENTIALS_FILE) {
  /** Resolve the active provider from config */
  function getProviderId() {
    // New explicit provider field
    if (siteConfig.dns?.provider) return siteConfig.dns.provider;
    // Legacy: if dns.ip is set, default to technitium
    if (siteConfig.dnsServerIp || siteConfig.dns?.ip) return 'technitium';
    // No DNS configured
    return 'manual';
  }

  /** Get provider-specific config from site config */
  function getProviderConfig(providerId) {
    const dnsConfig = siteConfig.dns || {};
    const builders = {
      technitium: () => ({
        serverIp: siteConfig.dnsServerIp || dnsConfig.ip || '',
        serverPort: siteConfig.dnsServerPort || dnsConfig.port || '5380',
        dnsServers: siteConfig.dnsServers || {},
        dnsId: Object.keys(siteConfig.dnsServers || {})[0] || 'dns1'
      }),
      cloudflare: () => ({
        apiToken: dnsConfig.apiToken || '',
        zoneId: dnsConfig.zoneId || '',
        domain: siteConfig.domain || ''
      }),
      rfc2136: () => ({
        server: dnsConfig.server || siteConfig.dnsServerIp || '',
        port: dnsConfig.port || 53,
        zone: siteConfig.tld?.replace(/^\./, '') || '',
        tsigAlgorithm: dnsConfig.tsigAlgorithm || 'hmac-sha256',
        tsigKeyName: dnsConfig.tsigKeyName || '',
        tsigSecret: dnsConfig.tsigSecret || ''
      }),
      manual: () => ({})
    };
    const builder = builders[providerId];
    return builder ? builder() : dnsConfig;
  }

  /** Get or create the active provider adapter */
  function getActiveProvider() {
    const providerId = getProviderId();
    const config = getProviderConfig(providerId);
    const ctx = { log, credentialManager, fetchT, httpsAgent };
    return registry.getProvider(providerId, config, ctx);
  }

  // ===== Legacy Technitium helpers (kept for backward compat) =====
  function buildDnsUrl(server, apiPath, params) {
    const protocol = server.match(/^\d+\.\d+\.\d+\.\d+$/) ? 'http' : 'https';
    const port = protocol === 'http' ? `:${CADDY.DEFAULT_DNS_PORT}` : '';
    const qs = params instanceof URLSearchParams ? params.toString() : new URLSearchParams(params).toString();
    return `${protocol}://${server}${port}${apiPath}?${qs}`;
  }

  async function callDns(server, apiPath, params) {
    const url = buildDnsUrl(server, apiPath, params);
    const response = await fetchT(url, {
      method: 'GET',
      headers: { 'Accept': 'application/json' },
      agent: httpsAgent
    }, TIMEOUTS.HTTP_LONG);
    return response.json();
  }

  async function refreshDnsToken(username, password, server) {
    try {
      const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
      const response = await fetchT(
        `http://${server}:5380/api/user/login?${params.toString()}`,
        { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } },
        10000
      );
      const result = await response.json();
      if (result.status === 'ok' && result.token) {
        dnsToken = result.token;
        dnsTokenExpiry = new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString();
        log.info('dns', 'DNS token refreshed', { expires: dnsTokenExpiry });
        return { success: true, token: dnsToken };
      }
      return { success: false, error: result.errorMessage || 'Login failed' };
    } catch (error) {
      log.error('dns', 'DNS token refresh error', { error: error.message });
      return { success: false, error: error.message };
    }
  }

  function dnsIpToDnsId(serverIp) {
    for (const [dnsId, info] of Object.entries(siteConfig.dnsServers || {})) {
      if (info.ip === serverIp) return dnsId;
    }
    return null;
  }

  async function tryServerRoleCredentials(dnsId, role, primaryIp) {
    try {
      const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`);
      const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`);
      if (username && password) return await refreshDnsToken(username, password, primaryIp);
    } catch (err) { /* try next */ }
    return null;
  }

  async function tryGlobalCredentials(primaryIp) {
    try {
      const username = await credentialManager.retrieve('dns.username');
      const password = await credentialManager.retrieve('dns.password');
      const server = await credentialManager.retrieve('dns.server');
      if (username && password) return await refreshDnsToken(username, password, server || primaryIp);
    } catch (err) { /* no global creds */ }
    return null;
  }

  async function ensureValidDnsToken() {
    if (dnsToken && dnsTokenExpiry && new Date() < new Date(dnsTokenExpiry)) {
      return { success: true, token: dnsToken };
    }
    const primaryIp = siteConfig.dnsServerIp;
    if (primaryIp) {
      const dnsId = dnsIpToDnsId(primaryIp);
      if (dnsId) {
        for (const role of ['admin', 'readonly']) {
          const result = await tryServerRoleCredentials(dnsId, role, primaryIp);
          if (result) return result;
        }
      }
    }
    const globalResult = await tryGlobalCredentials(primaryIp);
    if (globalResult) return globalResult;
    return { success: false, error: 'No DNS credentials configured' };
  }

  async function getTokenForServer(targetServer, role = 'readonly') {
    const cacheKey = `${targetServer}:${role}`;
    const cached = dnsServerTokens.get(cacheKey);
    if (cached?.token && cached?.expiry && new Date() < new Date(cached.expiry)) {
      return { success: true, token: cached.token };
    }
    const serverPort = siteConfig.dnsServerPort || '5380';
    async function authToServer(username, password) {
      const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
      const response = await fetchT(
        `http://${targetServer}:${serverPort}/api/user/login?${params.toString()}`,
        { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } }
      );
      const result = await response.json();
      if (result.status === 'ok' && result.token) {
        dnsServerTokens.set(cacheKey, { token: result.token, expiry: new Date(Date.now() + SESSION_TTL.DNS_TOKEN).toISOString() });
        log.info('dns', 'DNS token obtained for server', { server: targetServer, role });
        return { success: true, token: result.token };
      }
      return { success: false, error: result.errorMessage || 'Login failed' };
    }
    const dnsId = dnsIpToDnsId(targetServer);
    if (dnsId) {
      for (const r of [role, role === 'readonly' ? 'admin' : 'readonly']) {
        try {
          const username = await credentialManager.retrieve(`dns.${dnsId}.${r}.username`);
          const password = await credentialManager.retrieve(`dns.${dnsId}.${r}.password`);
          if (username && password) return await authToServer(username, password);
        } catch { /* try next */ }
      }
    }
    try {
      const username = await credentialManager.retrieve('dns.username');
      const password = await credentialManager.retrieve('dns.password');
      if (username && password) return await authToServer(username, password);
    } catch { /* no global creds */ }
    return { success: false, error: 'No DNS credentials configured' };
  }

  async function requireDnsToken(providedToken) {
    if (providedToken) return providedToken;
    const result = await ensureValidDnsToken();
    if (result.success) return result.token;
    const err = new Error('No valid DNS token available. ' + result.error);
    err.statusCode = 401;
    throw err;
  }

  function invalidateTokenForServer(serverIp) {
    dnsServerTokens.delete(`${serverIp}:readonly`);
    dnsServerTokens.delete(`${serverIp}:admin`);
  }

  // ===== Public context API =====
  // This maintains the same interface as the old createDnsContext()
  // but adds provider-aware methods on top.

  return {
    // --- Provider-aware methods ---
    /** Get the active provider ID */
    getProviderId,

    /** Get the active provider adapter instance */
    getActiveProvider,

    /** Get metadata for all available providers */
    getAvailableProviders: () => registry.getProviderMeta(),

    /** Check if the active provider supports a capability */
    supportsCapability: (cap) => {
      try { return getActiveProvider().supportsCapability(cap); }
      catch { return false; }
    },

    // --- Legacy Technitium context (backward compat) ---
    call: callDns,
    buildUrl: buildDnsUrl,
    requireToken: requireDnsToken,
    ensureToken: ensureValidDnsToken,
    getToken: () => dnsToken,
    setToken: (t) => { dnsToken = t; },
    getTokenExpiry: () => dnsTokenExpiry,
    setTokenExpiry: (e) => { dnsTokenExpiry = e; },
    getTokenForServer,
    invalidateTokenForServer,
    refresh: refreshDnsToken,
    credentialsFile: DNS_CREDENTIALS_FILE,

    // --- Universal DNS helpers (provider-agnostic) ---

    /**
     * Create a DNS A record using the active provider.
     * Gracefully handles manual adapters that return instructions instead of performing the action.
     */
    async universalCreateRecord(subdomain, ip) {
      const provider = getActiveProvider();
      const result = await provider.createRecord({
        domain: buildDomain(subdomain),
        zone: siteConfig.tld?.replace(/^\./, '') || '',
        type: 'A',
        value: ip,
        ttl: 300,
        overwrite: true,
      });
      // Manual adapter returns instructions instead of performing the action
      if (result?.manual || result?.instructions) {
        return { success: true, manual: true, instructions: result.instructions || result };
      }
      return result;
    },

    /**
     * Delete a DNS A record using the active provider.
     * Gracefully handles manual adapters that return instructions instead of performing the action.
     */
    async universalDeleteRecord(domain, ip) {
      const provider = getActiveProvider();
      const result = await provider.deleteRecord({
        domain,
        type: 'A',
        value: ip,
      });
      if (result?.manual || result?.instructions) {
        return { success: true, manual: true, instructions: result.instructions || result };
      }
      return result;
    },

    /**
     * Resolve DNS records using the active provider.
     * Returns parsed IP addresses from the result.
     */
    async universalResolveRecord(domain, type) {
      const provider = getActiveProvider();
      const result = await provider.resolveRecords({
        domain,
        zone: siteConfig.tld?.replace(/^\./, '') || '',
        type: type || 'A',
      });
      // Parse IP addresses from the result
      if (Array.isArray(result)) {
        return result;
      }
      if (result?.records) {
        return result.records.map(r => r.ipAddress || r.value || r.address || r).filter(Boolean);
      }
      if (result?.ips) {
        return result.ips;
      }
      return result;
    },
  };
}

module.exports = { createProviderDnsContext };
