import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import {
  DnsProviderDoc,
  DomainDoc,
  DnsRecordDoc,
} from '../db/documents/index.js';
import type { IDcRouterOptions } from '../classes.dcrouter.js';
import type { IDnsProviderClient, IProviderRecord } from './providers/interfaces.js';
import { createDnsProvider } from './providers/factory.js';
import type {
  TDnsRecordType,
  TDnsRecordSource,
} from '../../ts_interfaces/data/dns-record.js';
import type {
  TDnsProviderType,
  TDnsProviderCredentials,
  IDnsProviderPublic,
  IProviderDomainListing,
} from '../../ts_interfaces/data/dns-provider.js';

/**
 * DnsManager — owns runtime DNS state on top of the embedded DnsServer.
 *
 * Responsibilities:
 *  - Load Domain/DnsRecord docs from the DB on start
 *  - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
 *  - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
 *  - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
 *    smartdns, provider domains hit the provider API)
 *  - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
 *
 * Provider-managed domains are NEVER served from the embedded DnsServer — the
 * provider stays authoritative. We only mirror their records locally for the UI
 * and to track providerRecordIds for updates / deletes.
 */
export class DnsManager {
  /**
   * Reference to the active smartdns DnsServer (set by DcRouter once it exists).
   * May be undefined if dnsScopes/dnsNsDomains aren't configured.
   */
  public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;

  /**
   * Cached provider clients, keyed by DnsProviderDoc.id.
   * Created lazily when a provider is first needed.
   */
  private providerClients = new Map<string, IDnsProviderClient>();

  constructor(private options: IDcRouterOptions) {}

  // ==========================================================================
  // Lifecycle
  // ==========================================================================

  /**
   * Called from DcRouter after DcRouterDb is up. Performs first-boot seeding
   * from legacy constructor config if (and only if) the DB is empty.
   */
  public async start(): Promise<void> {
    logger.log('info', 'DnsManager: starting');
    await this.seedFromConstructorConfigIfEmpty();
  }

  public async stop(): Promise<void> {
    this.providerClients.clear();
    this.dnsServer = undefined;
  }

  /**
   * Wire the embedded DnsServer instance after it has been created by
   * DcRouter.setupDnsWithSocketHandler(). After this, local records on
   * dcrouter-hosted domains loaded from the DB are registered with the server.
   */
  public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
    this.dnsServer = dnsServer;
    await this.applyDcrouterDomainsToDnsServer();
  }

  // ==========================================================================
  // First-boot seeding
  // ==========================================================================

  /**
   * If no DomainDocs exist yet but the constructor has legacy DNS fields,
   * seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
   * local (`record.source: 'local'`) records. On subsequent boots (DB has
   * entries), constructor config is ignored with a warning.
   */
  private async seedFromConstructorConfigIfEmpty(): Promise<void> {
    const existingDomains = await DomainDoc.findAll();
    const hasLegacyConfig =
      (this.options.dnsScopes && this.options.dnsScopes.length > 0) ||
      (this.options.dnsRecords && this.options.dnsRecords.length > 0);

    if (existingDomains.length > 0) {
      if (hasLegacyConfig) {
        logger.log(
          'warn',
          'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords constructor config. ' +
            'dnsNsDomains is still required for nameserver and DoH bootstrap unless that moves into DB-backed config.',
        );
      }
      return;
    }

    if (!hasLegacyConfig) {
      return;
    }

    logger.log('info', 'DnsManager: seeding DB from legacy constructor DNS config');

    const now = Date.now();
    const seededDomains = new Map<string, DomainDoc>();

    // Create one DomainDoc per dnsScope (these are the authoritative zones)
    for (const scope of this.options.dnsScopes ?? []) {
      const domain = new DomainDoc();
      domain.id = plugins.uuid.v4();
      domain.name = scope.toLowerCase();
      domain.source = 'dcrouter';
      domain.authoritative = true;
      domain.createdAt = now;
      domain.updatedAt = now;
      domain.createdBy = 'seed';
      await domain.save();
      seededDomains.set(domain.name, domain);
      logger.log('info', `DnsManager: seeded DomainDoc for ${domain.name}`);
    }

    // Map each legacy dnsRecord to its parent DomainDoc
    for (const rec of this.options.dnsRecords ?? []) {
      const parent = this.findParentDomain(rec.name, seededDomains);
      if (!parent) {
        logger.log(
          'warn',
          `DnsManager: legacy dnsRecord '${rec.name}' has no matching dnsScope — skipping seed`,
        );
        continue;
      }
      const record = new DnsRecordDoc();
      record.id = plugins.uuid.v4();
      record.domainId = parent.id;
      record.name = rec.name.toLowerCase();
      record.type = rec.type as TDnsRecordType;
      record.value = rec.value;
      record.ttl = rec.ttl ?? 300;
      record.source = 'local';
      record.createdAt = now;
      record.updatedAt = now;
      record.createdBy = 'seed';
      await record.save();
    }

    logger.log(
      'info',
      `DnsManager: seeded ${seededDomains.size} domain(s) and ${this.options.dnsRecords?.length ?? 0} record(s) from legacy config`,
    );
  }

  private findParentDomain(
    recordName: string,
    domains: Map<string, DomainDoc>,
  ): DomainDoc | null {
    const lower = recordName.toLowerCase().replace(/^\*\./, '');
    let candidate: DomainDoc | null = null;
    for (const [name, doc] of domains) {
      if (lower === name || lower.endsWith(`.${name}`)) {
        if (!candidate || name.length > candidate.name.length) {
          candidate = doc;
        }
      }
    }
    return candidate;
  }

  // ==========================================================================
  // DcRouter-hosted domain DnsServer wiring
  // ==========================================================================

  /**
   * Register all records from dcrouter-hosted domains in the DB with the
   * embedded DnsServer. Called once after attachDnsServer().
   */
  private async applyDcrouterDomainsToDnsServer(): Promise<void> {
    if (!this.dnsServer) {
      return;
    }
    const allDomains = await DomainDoc.findAll();
    const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
    let registered = 0;
    for (const domain of dcrouterDomains) {
      const records = await DnsRecordDoc.findByDomainId(domain.id);
      for (const rec of records) {
        this.registerRecordWithDnsServer(rec);
        registered++;
      }
    }
    logger.log(
      'info',
      `DnsManager: registered ${registered} dcrouter-hosted DNS record(s) from DB`,
    );
  }

  /**
   * Register a single record with the embedded DnsServer. The handler closure
   * captures the record fields, so updates require a re-register cycle.
   */
  private registerRecordWithDnsServer(rec: DnsRecordDoc): void {
    if (!this.dnsServer) return;
    this.dnsServer.registerHandler(rec.name, [rec.type], (question) => {
      if (question.name.toLowerCase() === rec.name.toLowerCase() && question.type.toUpperCase() === rec.type) {
        return {
          name: question.name,
          type: rec.type,
          class: 'IN',
          ttl: rec.ttl,
          data: this.parseRecordData(rec.type, rec.value),
        };
      }
      return null;
    });
  }

  private parseRecordData(type: TDnsRecordType, value: string): any {
    switch (type) {
      case 'A':
      case 'AAAA':
      case 'CNAME':
      case 'TXT':
      case 'NS':
      case 'CAA':
        return value;
      case 'MX': {
        const [priorityStr, exchange] = value.split(' ');
        return { priority: parseInt(priorityStr, 10), exchange };
      }
      case 'SOA': {
        const parts = value.split(' ');
        return {
          mname: parts[0],
          rname: parts[1],
          serial: parseInt(parts[2], 10),
          refresh: parseInt(parts[3], 10),
          retry: parseInt(parts[4], 10),
          expire: parseInt(parts[5], 10),
          minimum: parseInt(parts[6], 10),
        };
      }
      default:
        return value;
    }
  }

  // ==========================================================================
  // Provider lookup (used by ACME DNS-01 + record CRUD)
  // ==========================================================================

  /**
   * Get the provider client for a given DnsProviderDoc id, instantiating
   * (and caching) it on first use.
   */
  public async getProviderClientById(providerId: string): Promise<IDnsProviderClient | null> {
    const cached = this.providerClients.get(providerId);
    if (cached) return cached;
    const doc = await DnsProviderDoc.findById(providerId);
    if (!doc) return null;
    const client = createDnsProvider(doc.type, doc.credentials);
    this.providerClients.set(providerId, client);
    return client;
  }

  /**
   * Find the IDnsProviderClient that owns the given FQDN (by walking up its
   * labels to find a matching DomainDoc with `source === 'provider'`).
   * Returns null if no provider claims this FQDN.
   *
   * Used by:
   *  - SmartAcme DNS-01 wiring in setupSmartProxy()
   *  - DnsRecordHandler when creating provider records
   */
  public async getProviderClientForDomain(fqdn: string): Promise<IDnsProviderClient | null> {
    const lower = fqdn.toLowerCase().replace(/^\*\./, '').replace(/\.$/, '');
    const allDomains = await DomainDoc.findAll();
    const providerDomains = allDomains
      .filter((d) => d.source === 'provider' && d.providerId)
      // longest-match wins
      .sort((a, b) => b.name.length - a.name.length);

    for (const domain of providerDomains) {
      if (lower === domain.name || lower.endsWith(`.${domain.name}`)) {
        return this.getProviderClientById(domain.providerId!);
      }
    }
    return null;
  }

  /**
   * Find the DomainDoc that covers a given FQDN, regardless of source
   * (dcrouter-hosted or provider-managed). Uses longest-suffix match.
   */
  public async findDomainForFqdn(fqdn: string): Promise<DomainDoc | null> {
    const lower = fqdn.toLowerCase().replace(/^\*\./, '').replace(/\.$/, '');
    const allDomains = await DomainDoc.findAll();
    // Sort by name length descending for longest-match-wins
    allDomains.sort((a, b) => b.name.length - a.name.length);
    for (const domain of allDomains) {
      if (lower === domain.name || lower.endsWith(`.${domain.name}`)) {
        return domain;
      }
    }
    return null;
  }

  /**
   * Delete DNS records matching a name and type under a domain.
   * When value is provided, only that exact record is removed so parallel ACME
   * challenges for the same host can coexist.
   */
  public async deleteRecordsByNameAndType(
    domainId: string,
    name: string,
    type: TDnsRecordType,
    value?: string,
  ): Promise<void> {
    const records = await DnsRecordDoc.findByDomainId(domainId);
    for (const rec of records) {
      if (
        rec.name.toLowerCase() === name.toLowerCase()
        && rec.type === type
        && (value === undefined || rec.value === value)
      ) {
        await this.deleteRecord(rec.id);
      }
    }
  }

  /**
   * True if any domain is under management (dcrouter-hosted or provider-managed).
   * Used by setupSmartProxy() to decide whether to wire SmartAcme with a DNS-01 handler.
   */
  public async hasAnyManagedDomain(): Promise<boolean> {
    const domains = await DomainDoc.findAll();
    return domains.length > 0;
  }

  /**
   * Build an IConvenientDnsProvider that routes ACME DNS-01 challenges through
   * the DnsManager abstraction. Challenges are dispatched via createRecord() /
   * deleteRecord(), which transparently handle both dcrouter-hosted zones
   * (embedded DnsServer) and provider-managed zones (e.g. Cloudflare API).
   *
   * Only domains under management (with a DomainDoc in DB) are supported —
   * this acts as the management gate for certificate issuance.
   */
  public buildAcmeConvenientDnsProvider(): plugins.tsclass.network.IConvenientDnsProvider {
    const self = this;
    const adapter = {
      async acmeSetDnsChallenge(dnsChallenge: { hostName: string; challenge: string }) {
        const domainDoc = await self.findDomainForFqdn(dnsChallenge.hostName);
        if (!domainDoc) {
          throw new Error(
            `DnsManager: no managed domain found for ${dnsChallenge.hostName}. ` +
              'Add the domain in Domains before issuing certificates.',
          );
        }
        // Clean only the same challenge value. Exact + wildcard SAN orders can
        // legitimately need multiple TXT records at the same name.
        try {
          await self.deleteRecordsByNameAndType(
            domainDoc.id,
            dnsChallenge.hostName,
            'TXT',
            dnsChallenge.challenge,
          );
        } catch (err: unknown) {
          logger.log('warn', `DnsManager: failed to clean existing TXT for ${dnsChallenge.hostName}: ${(err as Error).message}`);
        }
        // Create the challenge TXT record via the unified path
        await self.createRecord({
          domainId: domainDoc.id,
          name: dnsChallenge.hostName,
          type: 'TXT',
          value: dnsChallenge.challenge,
          ttl: 120,
          createdBy: 'acme-dns01',
        });
      },
      async acmeRemoveDnsChallenge(dnsChallenge: { hostName: string; challenge: string }) {
        const domainDoc = await self.findDomainForFqdn(dnsChallenge.hostName);
        if (!domainDoc) {
          // The domain may have been removed; nothing to clean up.
          return;
        }
        try {
          await self.deleteRecordsByNameAndType(
            domainDoc.id,
            dnsChallenge.hostName,
            'TXT',
            dnsChallenge.challenge,
          );
        } catch (err: unknown) {
          logger.log('warn', `DnsManager: failed to remove TXT for ${dnsChallenge.hostName}: ${(err as Error).message}`);
        }
      },
      async isDomainSupported(domain: string): Promise<boolean> {
        const domainDoc = await self.findDomainForFqdn(domain);
        return !!domainDoc;
      },
    };
    return { convenience: adapter } as plugins.tsclass.network.IConvenientDnsProvider;
  }

  // ==========================================================================
  // Provider CRUD (used by DnsProviderHandler)
  // ==========================================================================

  public async listProviders(): Promise<IDnsProviderPublic[]> {
    const docs = await DnsProviderDoc.findAll();
    return docs.map((d) => this.toPublicProvider(d));
  }

  public async getProvider(id: string): Promise<IDnsProviderPublic | null> {
    const doc = await DnsProviderDoc.findById(id);
    return doc ? this.toPublicProvider(doc) : null;
  }

  public async createProvider(args: {
    name: string;
    type: TDnsProviderType;
    credentials: TDnsProviderCredentials;
    createdBy: string;
  }): Promise<string> {
    if (args.type === 'dcrouter') {
      throw new Error(
        'createProvider: cannot create a DnsProviderDoc with type "dcrouter" — ' +
          'that type is reserved for the built-in pseudo-provider surfaced at read time.',
      );
    }
    const now = Date.now();
    const doc = new DnsProviderDoc();
    doc.id = plugins.uuid.v4();
    doc.name = args.name;
    doc.type = args.type;
    doc.credentials = args.credentials;
    doc.status = 'untested';
    doc.createdAt = now;
    doc.updatedAt = now;
    doc.createdBy = args.createdBy;
    await doc.save();
    return doc.id;
  }

  public async updateProvider(
    id: string,
    args: { name?: string; credentials?: TDnsProviderCredentials },
  ): Promise<boolean> {
    const doc = await DnsProviderDoc.findById(id);
    if (!doc) return false;
    if (args.name !== undefined) doc.name = args.name;
    if (args.credentials !== undefined) {
      doc.credentials = args.credentials;
      doc.status = 'untested';
      doc.lastError = undefined;
      // Invalidate cached client so the next use re-instantiates with the new credentials.
      this.providerClients.delete(id);
    }
    doc.updatedAt = Date.now();
    await doc.save();
    return true;
  }

  public async deleteProvider(id: string, force: boolean): Promise<{ success: boolean; message?: string }> {
    const doc = await DnsProviderDoc.findById(id);
    if (!doc) return { success: false, message: 'Provider not found' };
    const linkedDomains = await DomainDoc.findByProviderId(id);
    if (linkedDomains.length > 0 && !force) {
      return {
        success: false,
        message: `Provider is referenced by ${linkedDomains.length} domain(s). Pass force: true to delete anyway.`,
      };
    }
    // If forcing, also delete the linked domains and their records.
    if (force) {
      for (const domain of linkedDomains) {
        await this.deleteDomain(domain.id);
      }
    }
    await doc.delete();
    this.providerClients.delete(id);
    return { success: true };
  }

  public async testProvider(id: string): Promise<{ ok: boolean; error?: string; testedAt: number }> {
    const doc = await DnsProviderDoc.findById(id);
    if (!doc) {
      return { ok: false, error: 'Provider not found', testedAt: Date.now() };
    }
    const client = createDnsProvider(doc.type, doc.credentials);
    const result = await client.testConnection();
    doc.status = result.ok ? 'ok' : 'error';
    doc.lastTestedAt = Date.now();
    doc.lastError = result.ok ? undefined : result.error;
    await doc.save();
    if (result.ok) {
      this.providerClients.set(id, client); // cache the working client
    }
    return { ok: result.ok, error: result.error, testedAt: doc.lastTestedAt };
  }

  public async listProviderDomains(providerId: string): Promise<IProviderDomainListing[]> {
    const client = await this.getProviderClientById(providerId);
    if (!client) {
      throw new Error('Provider not found');
    }
    return await client.listDomains();
  }

  // ==========================================================================
  // Domain CRUD (used by DomainHandler)
  // ==========================================================================

  public async listDomains(): Promise<DomainDoc[]> {
    return await DomainDoc.findAll();
  }

  public async getDomain(id: string): Promise<DomainDoc | null> {
    return await DomainDoc.findById(id);
  }

  /**
   * Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
   * DNS records for this domain via the embedded smartdns.DnsServer.
   */
  public async createDcrouterDomain(args: {
    name: string;
    description?: string;
    createdBy: string;
  }): Promise<string> {
    const now = Date.now();
    const doc = new DomainDoc();
    doc.id = plugins.uuid.v4();
    doc.name = args.name.toLowerCase();
    doc.source = 'dcrouter';
    doc.authoritative = true;
    doc.description = args.description;
    doc.createdAt = now;
    doc.updatedAt = now;
    doc.createdBy = args.createdBy;
    await doc.save();
    return doc.id;
  }

  /**
   * Import one or more domains from a provider, pulling all of their DNS
   * records into local DnsRecordDocs.
   */
  public async importDomainsFromProvider(args: {
    providerId: string;
    domainNames: string[];
    createdBy: string;
  }): Promise<string[]> {
    const provider = await DnsProviderDoc.findById(args.providerId);
    if (!provider) {
      throw new Error('Provider not found');
    }
    const client = await this.getProviderClientById(args.providerId);
    if (!client) {
      throw new Error('Failed to instantiate provider client');
    }
    const allProviderDomains = await client.listDomains();
    const importedIds: string[] = [];
    const now = Date.now();

    for (const wantedName of args.domainNames) {
      const lower = wantedName.toLowerCase();
      const listing = allProviderDomains.find((d) => d.name.toLowerCase() === lower);
      if (!listing) {
        logger.log('warn', `DnsManager: import skipped — provider does not list domain ${wantedName}`);
        continue;
      }
      // Skip if already imported
      const existing = await DomainDoc.findByName(lower);
      if (existing) {
        logger.log('warn', `DnsManager: domain ${wantedName} already imported — skipping`);
        continue;
      }

      const domain = new DomainDoc();
      domain.id = plugins.uuid.v4();
      domain.name = lower;
      domain.source = 'provider';
      domain.providerId = args.providerId;
      domain.authoritative = false;
      domain.nameservers = listing.nameservers;
      domain.externalZoneId = listing.externalId;
      domain.lastSyncedAt = now;
      domain.createdAt = now;
      domain.updatedAt = now;
      domain.createdBy = args.createdBy;
      await domain.save();
      importedIds.push(domain.id);

      // Pull records for the imported domain
      try {
        const providerRecords = await client.listRecords(lower);
        for (const pr of providerRecords) {
          await this.createSyncedRecord(domain.id, pr, args.createdBy);
        }
        logger.log('info', `DnsManager: imported ${providerRecords.length} record(s) for ${lower}`);
      } catch (err: unknown) {
        logger.log('warn', `DnsManager: failed to import records for ${lower}: ${(err as Error).message}`);
      }
    }
    return importedIds;
  }

  public async updateDomain(id: string, args: { description?: string }): Promise<boolean> {
    const doc = await DomainDoc.findById(id);
    if (!doc) return false;
    if (args.description !== undefined) doc.description = args.description;
    doc.updatedAt = Date.now();
    await doc.save();
    return true;
  }

  /**
   * Delete a domain and all of its DNS records. For provider domains, only
   * removes the local mirror — does NOT touch the provider.
   * For dcrouter-hosted domains, also unregisters records from the embedded
   * DnsServer.
   *
   * Note: smartdns has no public unregister-by-name API in the version pinned
   * here, so local record deletes only take effect after a restart. The DB
   * is the source of truth and the next start will not register the deleted
   * record.
   */
  public async deleteDomain(id: string): Promise<boolean> {
    const doc = await DomainDoc.findById(id);
    if (!doc) return false;
    const records = await DnsRecordDoc.findByDomainId(id);
    for (const r of records) {
      await r.delete();
    }
    await doc.delete();
    return true;
  }

  /**
   * Force-resync a provider-managed domain: re-pull all records from the
   * provider API, replacing the cached DnsRecordDocs.
   */
  public async syncDomain(id: string): Promise<{ success: boolean; recordCount?: number; message?: string }> {
    const doc = await DomainDoc.findById(id);
    if (!doc) return { success: false, message: 'Domain not found' };
    if (doc.source !== 'provider' || !doc.providerId) {
      return { success: false, message: 'Domain is not provider-managed' };
    }
    const client = await this.getProviderClientById(doc.providerId);
    if (!client) {
      return { success: false, message: 'Provider client unavailable' };
    }
    const providerRecords = await client.listRecords(doc.name);

    // Drop existing records and replace
    const existing = await DnsRecordDoc.findByDomainId(id);
    for (const r of existing) {
      await r.delete();
    }
    for (const pr of providerRecords) {
      await this.createSyncedRecord(id, pr, doc.createdBy);
    }
    doc.lastSyncedAt = Date.now();
    doc.updatedAt = doc.lastSyncedAt;
    await doc.save();
    return { success: true, recordCount: providerRecords.length };
  }

  // ==========================================================================
  // Record CRUD (used by DnsRecordHandler)
  // ==========================================================================

  public async listRecordsForDomain(domainId: string): Promise<DnsRecordDoc[]> {
    return await DnsRecordDoc.findByDomainId(domainId);
  }

  public async getRecord(id: string): Promise<DnsRecordDoc | null> {
    return await DnsRecordDoc.findById(id);
  }

  // ==========================================================================
  // Domain migration
  // ==========================================================================

  /**
   * Migrate a domain between dcrouter-hosted and provider-managed.
   * Transfers all records to the target and updates domain metadata.
   */
  public async migrateDomain(args: {
    id: string;
    targetSource: 'dcrouter' | 'provider';
    targetProviderId?: string;
    deleteExistingProviderRecords?: boolean;
  }): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
    const domain = await DomainDoc.findById(args.id);
    if (!domain) return { success: false, message: 'Domain not found' };

    if (domain.source === args.targetSource && domain.providerId === args.targetProviderId) {
      return { success: false, message: 'Domain is already in the target configuration' };
    }

    const records = await DnsRecordDoc.findByDomainId(domain.id);

    if (args.targetSource === 'provider') {
      return this.migrateToDnsProvider(domain, records, args.targetProviderId!, args.deleteExistingProviderRecords ?? false);
    } else {
      return this.migrateToDcrouter(domain, records);
    }
  }

  /**
   * Migrate domain from dcrouter-hosted (or another provider) to an external DNS provider.
   */
  private async migrateToDnsProvider(
    domain: DomainDoc,
    records: DnsRecordDoc[],
    targetProviderId: string,
    deleteExistingProviderRecords: boolean,
  ): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
    // Validate the target provider exists
    const client = await this.getProviderClientById(targetProviderId);
    if (!client) {
      return { success: false, message: 'Target DNS provider not found' };
    }

    // Find the zone at the provider
    const providerDomains = await client.listDomains();
    const zone = providerDomains.find(
      (z) => z.name.toLowerCase() === domain.name.toLowerCase(),
    );
    if (!zone) {
      return { success: false, message: `Zone "${domain.name}" not found at the target provider` };
    }

    // Optionally delete existing records at the provider
    if (deleteExistingProviderRecords) {
      try {
        const existingProviderRecords = await client.listRecords(domain.name);
        for (const pr of existingProviderRecords) {
          await client.deleteRecord(domain.name, pr.providerRecordId).catch(() => {});
        }
        logger.log('info', `Deleted ${existingProviderRecords.length} existing records at provider for ${domain.name}`);
      } catch (err: unknown) {
        logger.log('warn', `Failed to clean existing provider records for ${domain.name}: ${(err as Error).message}`);
      }
    }

    // Push each local record to the provider
    let migrated = 0;
    for (const rec of records) {
      try {
        const providerRecord = await client.createRecord(domain.name, {
          name: rec.name,
          type: rec.type as any,
          value: rec.value,
          ttl: rec.ttl,
        });
        // Unregister from embedded DnsServer if it was dcrouter-hosted
        if (domain.source === 'dcrouter') {
          this.unregisterRecordFromDnsServer(rec);
        }
        // Update the record doc to synced
        rec.source = 'synced' as TDnsRecordSource;
        rec.providerRecordId = providerRecord.providerRecordId;
        await rec.save();
        migrated++;
      } catch (err: unknown) {
        logger.log('warn', `Failed to migrate record ${rec.name} ${rec.type} to provider: ${(err as Error).message}`);
      }
    }

    // Update domain metadata
    domain.source = 'provider';
    domain.authoritative = false;
    domain.providerId = targetProviderId;
    domain.externalZoneId = zone.externalId;
    domain.nameservers = zone.nameservers;
    domain.lastSyncedAt = Date.now();
    domain.updatedAt = Date.now();
    await domain.save();

    logger.log('info', `Domain ${domain.name} migrated to provider (${migrated} records)`);
    return { success: true, recordsMigrated: migrated };
  }

  /**
   * Migrate domain from provider-managed to dcrouter-hosted (authoritative).
   */
  private async migrateToDcrouter(
    domain: DomainDoc,
    records: DnsRecordDoc[],
  ): Promise<{ success: boolean; recordsMigrated?: number; message?: string }> {
    // Register each record with the embedded DnsServer
    let migrated = 0;
    for (const rec of records) {
      try {
        this.registerRecordWithDnsServer(rec);
        // Update the record doc to local
        rec.source = 'local' as TDnsRecordSource;
        rec.providerRecordId = undefined;
        await rec.save();
        migrated++;
      } catch (err: unknown) {
        logger.log('warn', `Failed to register record ${rec.name} ${rec.type} with DnsServer: ${(err as Error).message}`);
      }
    }

    // Update domain metadata
    domain.source = 'dcrouter';
    domain.authoritative = true;
    domain.providerId = undefined;
    domain.externalZoneId = undefined;
    domain.nameservers = undefined;
    domain.lastSyncedAt = undefined;
    domain.updatedAt = Date.now();
    await domain.save();

    logger.log('info', `Domain ${domain.name} migrated to dcrouter (${migrated} records)`);
    return { success: true, recordsMigrated: migrated };
  }

  // ==========================================================================
  // Record CRUD
  // ==========================================================================

  public async createRecord(args: {
    domainId: string;
    name: string;
    type: TDnsRecordType;
    value: string;
    ttl?: number;
    proxied?: boolean;
    createdBy: string;
  }): Promise<{ success: boolean; id?: string; message?: string }> {
    const domain = await DomainDoc.findById(args.domainId);
    if (!domain) return { success: false, message: 'Domain not found' };

    const now = Date.now();
    const doc = new DnsRecordDoc();
    doc.id = plugins.uuid.v4();
    doc.domainId = args.domainId;
    doc.name = args.name.toLowerCase();
    doc.type = args.type;
    doc.value = args.value;
    doc.ttl = args.ttl ?? 300;
    if (args.proxied !== undefined) doc.proxied = args.proxied;
    doc.source = 'local';
    doc.createdAt = now;
    doc.updatedAt = now;
    doc.createdBy = args.createdBy;

    if (domain.source === 'provider') {
      // Push to provider first; only persist locally on success
      if (!domain.providerId) {
        return { success: false, message: 'Provider domain has no providerId' };
      }
      const client = await this.getProviderClientById(domain.providerId);
      if (!client) return { success: false, message: 'Provider client unavailable' };
      try {
        const created = await client.createRecord(domain.name, {
          name: doc.name,
          type: doc.type,
          value: doc.value,
          ttl: doc.ttl,
          proxied: doc.proxied,
        });
        doc.providerRecordId = created.providerRecordId;
        doc.source = 'synced';
      } catch (err: unknown) {
        return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
      }
    } else {
      // dcrouter-hosted / authoritative — register with embedded DnsServer immediately
      this.registerRecordWithDnsServer(doc);
    }

    await doc.save();
    return { success: true, id: doc.id };
  }

  public async updateRecord(args: {
    id: string;
    name?: string;
    value?: string;
    ttl?: number;
    proxied?: boolean;
  }): Promise<{ success: boolean; message?: string }> {
    const doc = await DnsRecordDoc.findById(args.id);
    if (!doc) return { success: false, message: 'Record not found' };
    const domain = await DomainDoc.findById(doc.domainId);
    if (!domain) return { success: false, message: 'Parent domain not found' };

    if (args.name !== undefined) doc.name = args.name.toLowerCase();
    if (args.value !== undefined) doc.value = args.value;
    if (args.ttl !== undefined) doc.ttl = args.ttl;
    if (args.proxied !== undefined) doc.proxied = args.proxied;
    doc.updatedAt = Date.now();

    if (domain.source === 'provider') {
      if (!domain.providerId || !doc.providerRecordId) {
        return { success: false, message: 'Provider record metadata missing' };
      }
      const client = await this.getProviderClientById(domain.providerId);
      if (!client) return { success: false, message: 'Provider client unavailable' };
      try {
        await client.updateRecord(domain.name, doc.providerRecordId, {
          name: doc.name,
          type: doc.type,
          value: doc.value,
          ttl: doc.ttl,
          proxied: doc.proxied,
        });
      } catch (err: unknown) {
        return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
      }
    } else {
      // Re-register the local record so the new closure picks up the updated fields
      this.registerRecordWithDnsServer(doc);
    }

    await doc.save();
    return { success: true };
  }

  public async deleteRecord(id: string): Promise<{ success: boolean; message?: string }> {
    const doc = await DnsRecordDoc.findById(id);
    if (!doc) return { success: false, message: 'Record not found' };
    const domain = await DomainDoc.findById(doc.domainId);
    if (!domain) return { success: false, message: 'Parent domain not found' };

    if (domain.source === 'provider') {
      if (domain.providerId && doc.providerRecordId) {
        const client = await this.getProviderClientById(domain.providerId);
        if (client) {
          try {
            await client.deleteRecord(domain.name, doc.providerRecordId);
          } catch (err: unknown) {
            return { success: false, message: `Provider rejected delete: ${(err as Error).message}` };
          }
        }
      }
    }
    // For dcrouter-hosted records: unregister the handler from the embedded DnsServer
    // so the record stops being served immediately (not just after restart).
    if (domain.source === 'dcrouter' && this.dnsServer) {
      this.unregisterRecordFromDnsServer(doc);
    }

    await doc.delete();
    return { success: true };
  }

  /**
   * Unregister a record's handler from the embedded DnsServer.
   */
  public unregisterRecordFromDnsServer(rec: DnsRecordDoc): void {
    if (!this.dnsServer) return;
    this.dnsServer.unregisterHandler(rec.name, [rec.type]);
  }

  // ==========================================================================
  // Internal helpers
  // ==========================================================================

  private async createSyncedRecord(
    domainId: string,
    pr: IProviderRecord,
    createdBy: string,
  ): Promise<void> {
    const now = Date.now();
    const doc = new DnsRecordDoc();
    doc.id = plugins.uuid.v4();
    doc.domainId = domainId;
    doc.name = pr.name.toLowerCase();
    doc.type = pr.type;
    doc.value = pr.value;
    doc.ttl = pr.ttl;
    if (pr.proxied !== undefined) doc.proxied = pr.proxied;
    doc.source = 'synced';
    doc.providerRecordId = pr.providerRecordId;
    doc.createdAt = now;
    doc.updatedAt = now;
    doc.createdBy = createdBy;
    await doc.save();
  }

  /**
   * Convert a DnsProviderDoc to its public, secret-stripped representation
   * for the OpsServer API.
   */
  public toPublicProvider(doc: DnsProviderDoc): IDnsProviderPublic {
    return {
      id: doc.id,
      name: doc.name,
      type: doc.type,
      status: doc.status,
      lastTestedAt: doc.lastTestedAt,
      lastError: doc.lastError,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
      createdBy: doc.createdBy,
      hasCredentials: !!doc.credentials,
    };
  }

  /**
   * Convert a DomainDoc to its plain interface representation.
   */
  public toPublicDomain(doc: DomainDoc): {
    id: string;
    name: string;
    source: 'dcrouter' | 'provider';
    providerId?: string;
    authoritative: boolean;
    nameservers?: string[];
    externalZoneId?: string;
    lastSyncedAt?: number;
    description?: string;
    createdAt: number;
    updatedAt: number;
    createdBy: string;
  } {
    return {
      id: doc.id,
      name: doc.name,
      source: doc.source,
      providerId: doc.providerId,
      authoritative: doc.authoritative,
      nameservers: doc.nameservers,
      externalZoneId: doc.externalZoneId,
      lastSyncedAt: doc.lastSyncedAt,
      description: doc.description,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
      createdBy: doc.createdBy,
    };
  }

  /**
   * Convert a DnsRecordDoc to its plain interface representation.
   */
  public toPublicRecord(doc: DnsRecordDoc): {
    id: string;
    domainId: string;
    name: string;
    type: TDnsRecordType;
    value: string;
    ttl: number;
    proxied?: boolean;
    source: TDnsRecordSource;
    providerRecordId?: string;
    createdAt: number;
    updatedAt: number;
    createdBy: string;
  } {
    return {
      id: doc.id,
      domainId: doc.domainId,
      name: doc.name,
      type: doc.type,
      value: doc.value,
      ttl: doc.ttl,
      proxied: doc.proxied,
      source: doc.source,
      providerRecordId: doc.providerRecordId,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
      createdBy: doc.createdBy,
    };
  }
}
