import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type {
  IDiscoveryCandidate,
  IDiscoveryContext,
  IDiscoveryMatch,
  IDiscoveryMatcher,
  IDiscoveryProbe,
  IDiscoveryProbeResult,
  IDiscoveryValidator,
} from '../../core/types.js';
import type { IWizManualEntry, IWizMdnsRecord, IWizUdpDiscoveryRecord, IWizUdpResponse } from './wiz.types.js';
import { wizDefaultPort } from './wiz.types.js';

const wizMacPrefixes = ['a8bb50', 'd8a011', '444f8e', '6c2990'];
const wizUdpMethods = new Set(['getPilot', 'setPilot', 'syncPilot', 'syncSystemConfig', 'getSystemConfig', 'registration']);

export class WizUdpDiscoveryProbe implements IDiscoveryProbe {
  public id = 'wiz-udp-discovery-probe';
  public source = 'custom' as const;
  public description = 'Discover WiZ devices by UDP registration broadcast on port 38899.';

  public async probe(contextArg: IDiscoveryContext): Promise<IDiscoveryProbeResult> {
    if (contextArg.abortSignal?.aborted) {
      return { candidates: [] };
    }
    return { candidates: await this.discover(1200) };
  }

  private async discover(timeoutMsArg: number): Promise<IDiscoveryCandidate[]> {
    const { createSocket } = await import('node:dgram');
    const message = Buffer.from(JSON.stringify({
      method: 'registration',
      params: {
        phoneMac: 'AAAAAAAAAAAA',
        register: false,
        phoneIp: '1.2.3.4',
        id: '1',
      },
    }));
    const matcher = new WizUdpMatcher();
    const candidates: IDiscoveryCandidate[] = [];

    return new Promise((resolve) => {
      const socket = createSocket({ type: 'udp4', reuseAddr: true });
      const timer = setTimeout(() => {
        try {
          socket.close();
        } catch {
          // The discovery socket may already be closed after an OS error.
        }
        resolve(candidates);
      }, timeoutMsArg);

      socket.on('message', async (dataArg, remoteArg) => {
        let response: IWizUdpResponse<Record<string, unknown>> | undefined;
        try {
          response = JSON.parse(dataArg.toString('utf8')) as IWizUdpResponse<Record<string, unknown>>;
        } catch {
          return;
        }
        const match = await matcher.matches({ host: remoteArg.address, port: remoteArg.port, response });
        if (match.matched && match.candidate && !candidates.some((candidateArg) => candidateArg.id === match.candidate?.id || candidateArg.host === match.candidate?.host)) {
          candidates.push(match.candidate);
        }
      });
      socket.on('error', () => {
        clearTimeout(timer);
        try {
          socket.close();
        } catch {
          // Ignore discovery socket close races.
        }
        resolve(candidates);
      });
      socket.bind(() => {
        socket.setBroadcast(true);
        socket.send(message, wizDefaultPort, '255.255.255.255');
        setTimeout(() => socket.send(message, wizDefaultPort, '255.255.255.255'), 500);
      });
    });
  }
}

export class WizMdnsMatcher implements IDiscoveryMatcher<IWizMdnsRecord> {
  public id = 'wiz-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize WiZ mDNS or hostname advertisements.';

  public async matches(recordArg: IWizMdnsRecord): Promise<IDiscoveryMatch> {
    const txt = recordArg.txt || recordArg.properties || {};
    const type = this.normalizeType(recordArg.type);
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const mac = this.normalizeMac(this.txt(txt, 'mac') || this.txt(txt, 'macaddress') || this.txt(txt, 'mac_address'));
    const name = this.name(recordArg, txt);
    const text = [type, recordArg.name, recordArg.hostname, host, name, this.txt(txt, 'manufacturer'), this.txt(txt, 'model')]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const matched = Boolean(mac && this.isWizMac(mac)) || /(^|[._-])wiz([._-]|$)/i.test(text) || text.includes('wizconnected');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a WiZ advertisement.' };
    }
    return {
      matched: true,
      confidence: mac ? 'certain' : host ? 'high' : 'medium',
      reason: mac ? 'mDNS record contains a WiZ MAC address.' : 'mDNS record contains WiZ hostname or TXT metadata.',
      normalizedDeviceId: mac || recordArg.name || host,
      candidate: {
        source: 'mdns',
        integrationDomain: 'wiz',
        id: mac || recordArg.name || host,
        host,
        port: recordArg.port || wizDefaultPort,
        name: name || 'WiZ',
        manufacturer: 'WiZ',
        model: this.txt(txt, 'model') || this.txt(txt, 'moduleName') || 'WiZ device',
        macAddress: mac,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt,
        },
      },
    };
  }

  private txt(txtArg: Record<string, string | undefined>, keyArg: string): string | undefined {
    return txtArg[keyArg] || txtArg[keyArg.toUpperCase()];
  }

  private name(recordArg: IWizMdnsRecord, txtArg: Record<string, string | undefined>): string | undefined {
    return this.txt(txtArg, 'name') || this.txt(txtArg, 'friendly_name') || recordArg.name?.split('._', 1)[0] || recordArg.hostname?.replace(/\.local\.?$/i, '');
  }

  private normalizeType(valueArg?: string): string {
    return (valueArg || '').toLowerCase().replace(/\.$/, '');
  }

  private normalizeMac(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : valueArg.toLowerCase();
  }

  private isWizMac(valueArg: string): boolean {
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return wizMacPrefixes.some((prefixArg) => compact.startsWith(prefixArg));
  }
}

export class WizUdpMatcher implements IDiscoveryMatcher<IWizUdpDiscoveryRecord> {
  public id = 'wiz-udp-match';
  public source = 'custom' as const;
  public description = 'Recognize WiZ UDP JSON discovery responses.';

  public async matches(recordArg: IWizUdpDiscoveryRecord): Promise<IDiscoveryMatch> {
    const response = recordArg.response;
    const result = response?.result || recordArg.result || {};
    const method = response?.method || recordArg.method;
    const host = recordArg.host || recordArg.address || recordArg.ip || recordArg.ip_address;
    const mac = this.normalizeMac(recordArg.mac || recordArg.mac_address || this.stringValue(result.mac));
    const matched = Boolean(mac) || Boolean(method && wizUdpMethods.has(method)) || recordArg.metadata?.wiz === true;
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'UDP record is not a WiZ JSON response.' };
    }
    return {
      matched: true,
      confidence: mac && host ? 'certain' : mac || host ? 'high' : 'medium',
      reason: mac ? 'UDP response contains a WiZ MAC address.' : 'UDP response method matches WiZ JSON protocol.',
      normalizedDeviceId: mac || recordArg.name || host,
      candidate: {
        source: 'custom',
        integrationDomain: 'wiz',
        id: mac || recordArg.name || host,
        host,
        port: recordArg.port || wizDefaultPort,
        name: recordArg.name || (mac ? `WiZ ${this.shortMac(mac)}` : 'WiZ'),
        manufacturer: 'WiZ',
        model: 'WiZ UDP JSON device',
        macAddress: mac,
        metadata: {
          ...recordArg.metadata,
          discoveryProtocol: 'udp',
          method,
          result,
        },
      },
    };
  }

  private stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
  }

  private shortMac(valueArg?: string): string {
    return (valueArg || '').replace(/[^a-fA-F0-9]/g, '').slice(-6).toUpperCase();
  }

  private normalizeMac(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : valueArg.toLowerCase();
  }
}

export class WizManualMatcher implements IDiscoveryMatcher<IWizManualEntry> {
  public id = 'wiz-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual WiZ setup entries.';

  public async matches(inputArg: IWizManualEntry): Promise<IDiscoveryMatch> {
    const mac = this.normalizeMac(inputArg.mac || inputArg.macAddress || inputArg.deviceInfo?.mac);
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, inputArg.deviceInfo?.manufacturer, inputArg.deviceInfo?.model, inputArg.deviceInfo?.moduleName]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const matched = Boolean(inputArg.host || mac || inputArg.metadata?.wiz || text.includes('wiz'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain WiZ setup hints.' };
    }
    return {
      matched: true,
      confidence: inputArg.host && mac ? 'certain' : inputArg.host || mac ? 'high' : 'medium',
      reason: 'Manual entry can start WiZ setup.',
      normalizedDeviceId: mac || inputArg.id || inputArg.host,
      candidate: {
        source: 'manual',
        integrationDomain: 'wiz',
        id: inputArg.id || mac || inputArg.host,
        host: inputArg.host,
        port: inputArg.port || wizDefaultPort,
        name: inputArg.name || inputArg.deviceInfo?.name || (mac ? `WiZ ${this.shortMac(mac)}` : 'WiZ'),
        manufacturer: inputArg.manufacturer || inputArg.deviceInfo?.manufacturer || 'WiZ',
        model: inputArg.model || inputArg.deviceInfo?.model || inputArg.deviceInfo?.moduleName || 'WiZ device',
        macAddress: mac,
        metadata: {
          ...inputArg.metadata,
          discoveryProtocol: 'manual',
          deviceInfo: inputArg.deviceInfo,
          pilot: inputArg.pilot,
          snapshot: inputArg.snapshot,
        },
      },
    };
  }

  private shortMac(valueArg?: string): string {
    return (valueArg || '').replace(/[^a-fA-F0-9]/g, '').slice(-6).toUpperCase();
  }

  private normalizeMac(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : valueArg.toLowerCase();
  }
}

export class WizCandidateValidator implements IDiscoveryValidator {
  public id = 'wiz-candidate-validator';
  public description = 'Validate WiZ candidates from mDNS, UDP, DHCP, and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const mac = this.normalizeMac(candidateArg.macAddress);
    const compactMac = (mac || '').replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    const name = (candidateArg.name || '').toLowerCase();
    const model = (candidateArg.model || '').toLowerCase();
    const manufacturer = (candidateArg.manufacturer || '').toLowerCase();
    const mdnsType = typeof metadata.mdnsType === 'string' ? metadata.mdnsType.toLowerCase() : '';
    const discoveryProtocol = typeof metadata.discoveryProtocol === 'string' ? metadata.discoveryProtocol : undefined;
    const macMatched = wizMacPrefixes.some((prefixArg) => compactMac.startsWith(prefixArg));
    const hostMatched = Boolean(candidateArg.host && /^wiz[_-]/i.test(candidateArg.host));
    const textMatched = name.includes('wiz') || model.includes('wiz') || manufacturer.includes('wiz') || mdnsType.includes('wiz');
    const matched = candidateArg.integrationDomain === 'wiz'
      || macMatched
      || hostMatched
      || textMatched
      || candidateArg.port === wizDefaultPort
      || metadata.wiz === true
      || discoveryProtocol === 'udp'
      || discoveryProtocol === 'manual';
    return {
      matched,
      confidence: matched && (candidateArg.integrationDomain === 'wiz' || macMatched) && candidateArg.host ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has WiZ metadata or local UDP port information.' : 'Candidate is not WiZ.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: mac || candidateArg.id || candidateArg.host,
      metadata: matched ? { macMatched, hostMatched, discoveryProtocol } : undefined,
    };
  }

  private normalizeMac(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : valueArg.toLowerCase();
  }
}

export const createWizDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'wiz', displayName: 'WiZ' })
    .addProbe(new WizUdpDiscoveryProbe())
    .addMatcher(new WizMdnsMatcher())
    .addMatcher(new WizUdpMatcher())
    .addMatcher(new WizManualMatcher())
    .addValidator(new WizCandidateValidator());
};
