import * as plugins from '../plugins.js';
import type { IEmailDomainConfig } from '@push.rocks/smartmta';
import { logger } from '../logger.js';
import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js';
import { DomainDoc } from '../db/documents/classes.domain.doc.js';
import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
import type { DnsManager } from '../dns/manager.dns.js';
import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js';
import { buildEmailDnsRecords } from './email-dns-records.js';

/**
 * EmailDomainManager — orchestrates email domain setup.
 *
 * Wires smartmta's DKIMCreator (key generation) with dcrouter's DnsManager
 * (record creation for dcrouter-hosted and provider-managed zones) to provide
 * a single entry point for setting up an email domain from A to Z.
 */
export class EmailDomainManager {
  private dcRouter: any; // DcRouter — avoids circular import
  private baseEmailDomains: IEmailDomainConfig[] = [];

  constructor(dcRouterRef: any) {
    this.dcRouter = dcRouterRef;
    this.setBaseEmailDomains(this.dcRouter.options?.emailConfig?.domains as IEmailDomainConfig[] | undefined);
  }

  public setBaseEmailDomains(domains: IEmailDomainConfig[] | undefined): void {
    this.baseEmailDomains = (domains || [])
      .map((domainConfig) => JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig);
  }

  private get dnsManager(): DnsManager | undefined {
    return this.dcRouter.dnsManager;
  }

  private get dkimCreator(): any | undefined {
    return this.dcRouter.emailServer?.dkimCreator;
  }

  private get emailHostname(): string {
    return this.dcRouter.options?.emailConfig?.hostname || this.dcRouter.options?.tls?.domain || 'localhost';
  }

  public async start(): Promise<void> {
    await this.syncManagedDomainsToRuntime();
  }

  public async stop(): Promise<void> {}

  // ---------------------------------------------------------------------------
  // CRUD
  // ---------------------------------------------------------------------------

  public async getAll(): Promise<IEmailDomain[]> {
    const docs = await EmailDomainDoc.findAll();
    return docs.map((d) => this.docToInterface(d));
  }

  public async getById(id: string): Promise<IEmailDomain | null> {
    const doc = await EmailDomainDoc.findById(id);
    return doc ? this.docToInterface(doc) : null;
  }

  public async getByDomain(domainName: string): Promise<IEmailDomain | null> {
    const doc = await EmailDomainDoc.findByDomain(domainName);
    return doc ? this.docToInterface(doc) : null;
  }

  public async ensureEmailDomainForDomainName(domainName: string): Promise<IEmailDomain | null> {
    const normalizedDomain = domainName.trim().toLowerCase();
    const existing = await this.getByDomain(normalizedDomain);
    if (existing) return existing;
    if (this.isDomainAlreadyConfigured(normalizedDomain)) return null;

    const linkedDomain = await this.findLinkedDnsDomain(normalizedDomain);
    if (!linkedDomain) {
      throw new Error(`DNS domain not found for email domain: ${normalizedDomain}`);
    }

    const subdomain = normalizedDomain === linkedDomain.name
      ? undefined
      : normalizedDomain.slice(0, -(linkedDomain.name.length + 1));
    return await this.createEmailDomain({
      linkedDomainId: linkedDomain.id,
      subdomain,
    });
  }

  public async createEmailDomain(opts: {
    linkedDomainId: string;
    subdomain?: string;
    dkimSelector?: string;
    dkimKeySize?: number;
    rotateKeys?: boolean;
    rotationIntervalDays?: number;
  }): Promise<IEmailDomain> {
    // Resolve the linked DNS domain
    const domainDoc = await DomainDoc.findById(opts.linkedDomainId);
    if (!domainDoc) {
      throw new Error(`DNS domain not found: ${opts.linkedDomainId}`);
    }
    const baseDomain = domainDoc.name;
    const subdomain = opts.subdomain?.trim() || undefined;
    const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;

    // Check for duplicates
    if (this.isDomainAlreadyConfigured(domainName)) {
      throw new Error(`Email domain already configured for ${domainName}`);
    }
    const existing = await EmailDomainDoc.findByDomain(domainName);
    if (existing) {
      throw new Error(`Email domain already exists for ${domainName}`);
    }

    const selector = opts.dkimSelector || 'default';
    const keySize = opts.dkimKeySize || 2048;
    const now = new Date().toISOString();

    // Generate DKIM keys
    let publicKey: string | undefined;
    if (this.dkimCreator) {
      try {
        await this.dkimCreator.handleDKIMKeysForSelector(domainName, selector, keySize);
        const dnsRecord = await this.dkimCreator.getDNSRecordForDomain(domainName, selector);
        // Extract public key from the DNS record value
        const match = dnsRecord?.value?.match(/p=([A-Za-z0-9+/=]+)/);
        publicKey = match ? match[1] : undefined;
        logger.log('info', `DKIM keys generated for ${domainName} (selector: ${selector})`);
      } catch (err: unknown) {
        logger.log('warn', `DKIM key generation failed for ${domainName}: ${(err as Error).message}`);
      }
    }

    // Create the document
    const doc = new EmailDomainDoc();
    doc.id = plugins.smartunique.shortId();
    doc.domain = domainName.toLowerCase();
    doc.linkedDomainId = opts.linkedDomainId;
    doc.subdomain = subdomain;
    doc.dkim = {
      selector,
      keySize,
      publicKey,
      rotateKeys: opts.rotateKeys ?? false,
      rotationIntervalDays: opts.rotationIntervalDays ?? 90,
    };
    doc.dnsStatus = {
      mx: 'unchecked',
      spf: 'unchecked',
      dkim: 'unchecked',
      dmarc: 'unchecked',
    };
    doc.createdAt = now;
    doc.updatedAt = now;
    await doc.save();
    await this.syncManagedDomainsToRuntime();

    logger.log('info', `Email domain created: ${domainName}`);
    return this.docToInterface(doc);
  }

  public async updateEmailDomain(
    id: string,
    changes: {
      rotateKeys?: boolean;
      rotationIntervalDays?: number;
      rateLimits?: IEmailDomain['rateLimits'];
    },
  ): Promise<void> {
    const doc = await EmailDomainDoc.findById(id);
    if (!doc) throw new Error(`Email domain not found: ${id}`);

    if (changes.rotateKeys !== undefined) doc.dkim.rotateKeys = changes.rotateKeys;
    if (changes.rotationIntervalDays !== undefined) doc.dkim.rotationIntervalDays = changes.rotationIntervalDays;
    if (changes.rateLimits !== undefined) doc.rateLimits = changes.rateLimits;
    doc.updatedAt = new Date().toISOString();
    await doc.save();
    await this.syncManagedDomainsToRuntime();
  }

  public async deleteEmailDomain(id: string): Promise<void> {
    const doc = await EmailDomainDoc.findById(id);
    if (!doc) throw new Error(`Email domain not found: ${id}`);
    await doc.delete();
    await this.syncManagedDomainsToRuntime();
    logger.log('info', `Email domain deleted: ${doc.domain}`);
  }

  // ---------------------------------------------------------------------------
  // DNS record computation
  // ---------------------------------------------------------------------------

  /**
   * Compute the 4 required DNS records for an email domain.
   */
  public async getRequiredDnsRecords(id: string): Promise<IEmailDnsRecord[]> {
    const doc = await EmailDomainDoc.findById(id);
    if (!doc) throw new Error(`Email domain not found: ${id}`);

    const domain = doc.domain;
    const selector = doc.dkim.selector;
    const hostname = this.emailHostname;
    let dkimValue = `v=DKIM1; h=sha256; k=rsa; p=${doc.dkim.publicKey || ''}`;

    if (this.dkimCreator) {
      try {
        const dnsRecord = await this.dkimCreator.getDNSRecordForDomain(domain, selector);
        dkimValue = dnsRecord.value;
      } catch (err: unknown) {
        logger.log('warn', `Failed to load DKIM DNS record for ${domain}: ${(err as Error).message}`);
      }
    }

    return buildEmailDnsRecords({
      domain,
      hostname,
      selector,
      dkimValue,
      statuses: doc.dnsStatus,
    });
  }

  // ---------------------------------------------------------------------------
  // DNS provisioning
  // ---------------------------------------------------------------------------

  /**
   * Auto-create missing DNS records via the linked domain's DNS path.
   */
  public async provisionDnsRecords(id: string): Promise<number> {
    const doc = await EmailDomainDoc.findById(id);
    if (!doc) throw new Error(`Email domain not found: ${id}`);
    if (!this.dnsManager) throw new Error('DnsManager not available');

    const requiredRecords = await this.getRequiredDnsRecords(id);
    const domainId = doc.linkedDomainId;

    // Get existing DNS records for the linked domain
    const existingRecords = await DnsRecordDoc.findByDomainId(domainId);
    let provisioned = 0;

    for (const required of requiredRecords) {
      // Check if a matching record already exists
      const exists = existingRecords.some((r) => this.recordMatchesRequired(r, required));

      if (!exists) {
        try {
          await this.dnsManager.createRecord({
            domainId,
            name: required.name,
            type: required.type as any,
            value: required.value,
            ttl: 3600,
            createdBy: 'email-domain-manager',
          });
          provisioned++;
          logger.log('info', `Provisioned ${required.type} record for ${required.name}`);
        } catch (err: unknown) {
          logger.log('warn', `Failed to provision ${required.type} for ${required.name}: ${(err as Error).message}`);
        }
      }
    }

    // Re-validate after provisioning
    await this.validateDns(id);

    return provisioned;
  }

  // ---------------------------------------------------------------------------
  // DNS validation
  // ---------------------------------------------------------------------------

  /**
   * Validate DNS records via live lookups.
   */
  public async validateDns(id: string): Promise<IEmailDnsRecord[]> {
    const doc = await EmailDomainDoc.findById(id);
    if (!doc) throw new Error(`Email domain not found: ${id}`);

    const domain = doc.domain;
    const selector = doc.dkim.selector;
    const resolver = new plugins.dns.promises.Resolver();

    // MX check
    const requiredRecords = await this.getRequiredDnsRecords(id);

    const mxRecord = requiredRecords.find((record) => record.type === 'MX');
    const spfRecord = requiredRecords.find((record) => record.name === domain && record.value.startsWith('v=spf1'));
    const dkimRecord = requiredRecords.find((record) => record.name === `${selector}._domainkey.${domain}`);
    const dmarcRecord = requiredRecords.find((record) => record.name === `_dmarc.${domain}`);

    doc.dnsStatus.mx = await this.checkMx(resolver, domain, mxRecord?.value);

    // SPF check
    doc.dnsStatus.spf = await this.checkTxtRecord(resolver, domain, spfRecord?.value);

    // DKIM check
    doc.dnsStatus.dkim = await this.checkTxtRecord(resolver, `${selector}._domainkey.${domain}`, dkimRecord?.value);

    // DMARC check
    doc.dnsStatus.dmarc = await this.checkTxtRecord(resolver, `_dmarc.${domain}`, dmarcRecord?.value);

    doc.dnsStatus.lastCheckedAt = new Date().toISOString();
    doc.updatedAt = new Date().toISOString();
    await doc.save();

    return this.getRequiredDnsRecords(id);
  }

  private recordMatchesRequired(record: DnsRecordDoc, required: IEmailDnsRecord): boolean {
    if (record.type !== required.type || record.name.toLowerCase() !== required.name.toLowerCase()) {
      return false;
    }
    return record.value.trim() === required.value.trim();
  }

  private async checkMx(
    resolver: plugins.dns.promises.Resolver,
    domain: string,
    expectedValue?: string,
  ): Promise<TDnsRecordStatus> {
    try {
      const records = await resolver.resolveMx(domain);
      if (!records || records.length === 0) {
        return 'missing';
      }
      if (!expectedValue) {
        return 'valid';
      }
      const found = records.some((record) => `${record.priority} ${record.exchange}`.trim() === expectedValue.trim());
      return found ? 'valid' : 'invalid';
    } catch {
      return 'missing';
    }
  }

  private async checkTxtRecord(
    resolver: plugins.dns.promises.Resolver,
    name: string,
    expectedValue?: string,
  ): Promise<TDnsRecordStatus> {
    try {
      const records = await resolver.resolveTxt(name);
      const flat = records.map((r) => r.join(''));
      if (flat.length === 0) {
        return 'missing';
      }
      if (!expectedValue) {
        return 'valid';
      }
      const found = flat.some((record) => record.trim() === expectedValue.trim());
      return found ? 'valid' : 'invalid';
    } catch {
      return 'missing';
    }
  }

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

  private docToInterface(doc: EmailDomainDoc): IEmailDomain {
    return {
      id: doc.id,
      domain: doc.domain,
      linkedDomainId: doc.linkedDomainId,
      subdomain: doc.subdomain,
      dkim: doc.dkim,
      rateLimits: doc.rateLimits,
      dnsStatus: doc.dnsStatus,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
    };
  }

  private isDomainAlreadyConfigured(domainName: string): boolean {
    const configuredDomains = ((this.dcRouter.options?.emailConfig?.domains || []) as IEmailDomainConfig[])
      .map((domainConfig) => domainConfig.domain.toLowerCase());
    return configuredDomains.includes(domainName.toLowerCase());
  }

  private async findLinkedDnsDomain(domainName: string): Promise<DomainDoc | null> {
    const domains = await DomainDoc.findAll();
    return domains
      .filter((domainDoc) => domainName === domainDoc.name || domainName.endsWith(`.${domainDoc.name}`))
      .sort((a, b) => b.name.length - a.name.length)[0] || null;
  }

  private async buildManagedDomainConfigs(): Promise<IEmailDomainConfig[]> {
    const docs = await EmailDomainDoc.findAll();
    const managedConfigs: IEmailDomainConfig[] = [];

    for (const doc of docs) {
      const linkedDomain = await DomainDoc.findById(doc.linkedDomainId);
      if (!linkedDomain) {
        logger.log('warn', `Skipping managed email domain ${doc.domain}: linked domain missing`);
        continue;
      }

      managedConfigs.push({
        domain: doc.domain,
        dnsMode: linkedDomain.source === 'dcrouter' ? 'internal-dns' : 'external-dns',
        dkim: {
          selector: doc.dkim.selector,
          keySize: doc.dkim.keySize,
          rotateKeys: doc.dkim.rotateKeys,
          rotationInterval: doc.dkim.rotationIntervalDays,
        },
        rateLimits: doc.rateLimits,
      });
    }

    return managedConfigs;
  }

  public async syncManagedDomainsToRuntime(): Promise<void> {
    if (!this.dcRouter.options?.emailConfig) {
      return;
    }

    const mergedDomains = new Map<string, IEmailDomainConfig>();
    for (const domainConfig of this.baseEmailDomains) {
      mergedDomains.set(domainConfig.domain.toLowerCase(), JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig);
    }

    for (const managedConfig of await this.buildManagedDomainConfigs()) {
      const key = managedConfig.domain.toLowerCase();
      if (mergedDomains.has(key)) {
        logger.log('warn', `Managed email domain ${managedConfig.domain} duplicates a configured domain; keeping the configured definition`);
        continue;
      }
      mergedDomains.set(key, managedConfig);
    }

    const domains = Array.from(mergedDomains.values());
    this.dcRouter.options.emailConfig.domains = domains;
    if (this.dcRouter.emailServer) {
      this.dcRouter.emailServer.updateOptions({ domains });
    }
  }
}
