import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IEsphomeManualEntry, IEsphomeMdnsRecord } from './esphome.types.js';
import { esphomeDefaultNativeApiPort } from './esphome.types.js';

const esphomeMdnsTypes = new Set(['_esphomelib._tcp.local', '_esphome._tcp.local']);

export class EsphomeMdnsMatcher implements IDiscoveryMatcher<IEsphomeMdnsRecord> {
  public id = 'esphome-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize ESPHome native API mDNS advertisements.';

  public async matches(recordArg: IEsphomeMdnsRecord): Promise<IDiscoveryMatch> {
    const type = this.normalizeType(recordArg.type);
    const txt = recordArg.txt || recordArg.properties || {};
    const mac = this.normalizeMac(this.txt(txt, 'mac'));
    const deviceName = this.deviceName(recordArg, txt);
    const matched = esphomeMdnsTypes.has(type) || Boolean(mac || this.txt(txt, 'api_encryption') || this.txt(txt, 'friendly_name'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an ESPHome native API advertisement.' };
    }
    return {
      matched: true,
      confidence: mac ? 'certain' : esphomeMdnsTypes.has(type) ? 'high' : 'medium',
      reason: 'mDNS record matches ESPHome native API metadata.',
      normalizedDeviceId: mac || deviceName,
      candidate: {
        source: 'mdns',
        integrationDomain: 'esphome',
        id: mac || recordArg.name || recordArg.host,
        host: recordArg.host || recordArg.hostname || recordArg.addresses?.[0],
        port: recordArg.port || esphomeDefaultNativeApiPort,
        name: this.txt(txt, 'friendly_name') || deviceName || 'ESPHome',
        manufacturer: 'ESPHome',
        model: 'Native API device',
        macAddress: mac,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt,
          deviceName,
          encryptionRequired: Boolean(this.txt(txt, 'api_encryption')),
        },
      },
    };
  }

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

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

  private deviceName(recordArg: IEsphomeMdnsRecord, txtArg: Record<string, string | undefined>): string | undefined {
    const txtName = this.txt(txtArg, 'name');
    if (txtName) {
      return txtName;
    }
    const hostname = recordArg.hostname || recordArg.name;
    return hostname?.replace(/\._?esphome(?:lib)?\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '');
  }

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

export class EsphomeManualMatcher implements IDiscoveryMatcher<IEsphomeManualEntry> {
  public id = 'esphome-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual ESPHome native API setup entries.';

  public async matches(inputArg: IEsphomeManualEntry): Promise<IDiscoveryMatch> {
    const model = inputArg.model?.toLowerCase() || '';
    const manufacturer = inputArg.manufacturer?.toLowerCase() || '';
    const matched = Boolean(inputArg.host || inputArg.metadata?.esphome || inputArg.metadata?.api_encryption || model.includes('esphome') || manufacturer.includes('esphome'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain ESPHome setup hints.' };
    }
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start ESPHome native API setup.',
      normalizedDeviceId: inputArg.id || inputArg.host,
      candidate: {
        source: 'manual',
        integrationDomain: 'esphome',
        id: inputArg.id || inputArg.host,
        host: inputArg.host,
        port: inputArg.port || esphomeDefaultNativeApiPort,
        name: inputArg.name || inputArg.deviceName || 'ESPHome',
        manufacturer: inputArg.manufacturer || 'ESPHome',
        model: inputArg.model || 'Native API device',
        metadata: {
          ...inputArg.metadata,
          deviceName: inputArg.deviceName,
          password: inputArg.password,
          encryptionKey: inputArg.encryptionKey,
          noisePsk: inputArg.noisePsk,
        },
      },
    };
  }
}

export class EsphomeCandidateValidator implements IDiscoveryValidator {
  public id = 'esphome-candidate-validator';
  public description = 'Validate ESPHome native API candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const model = candidateArg.model?.toLowerCase() || '';
    const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
    const mdnsType = typeof candidateArg.metadata?.mdnsType === 'string' ? candidateArg.metadata.mdnsType.toLowerCase().replace(/\.$/, '') : '';
    const matched = candidateArg.integrationDomain === 'esphome' || candidateArg.port === esphomeDefaultNativeApiPort || esphomeMdnsTypes.has(mdnsType) || manufacturer.includes('esphome') || model.includes('esphome') || Boolean(candidateArg.metadata?.esphome || candidateArg.metadata?.api_encryption);
    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has ESPHome native API metadata.' : 'Candidate is not ESPHome.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.macAddress || candidateArg.id || candidateArg.host,
    };
  }
}

export const createEsphomeDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'esphome', displayName: 'ESPHome' })
    .addMatcher(new EsphomeMdnsMatcher())
    .addMatcher(new EsphomeManualMatcher())
    .addValidator(new EsphomeCandidateValidator());
};
