import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IZhaManualEntry, IZhaMdnsRecord, IZhaUsbRecord, TZhaRadioType } from './zha.types.js';

interface IZhaKnownUsbDevice {
  vid: string;
  pid: string;
  description?: string;
  knownDevices?: string[];
  radioType?: TZhaRadioType;
}

const zhaZeroconfTypes = new Set([
  '_zigbee-coordinator._tcp.local',
  '_zigate-zigbee-gateway._tcp.local',
  '_zigstar_gw._tcp.local',
  '_uzg-01._tcp.local',
  '_slzb-06._tcp.local',
  '_xzg._tcp.local',
  '_czc._tcp.local',
]);

const legacyZeroconfTypes = new Set(['_esphomelib._tcp.local']);
const defaultZigbeeCoordinatorPort = 6638;

const haManifestUsbDevices: IZhaKnownUsbDevice[] = [
  { vid: '10c4', pid: 'ea60', description: '*2652*', knownDevices: ['slae.sh cc2652rb stick'], radioType: 'znp' },
  { vid: '10c4', pid: 'ea60', description: '*slzb-07*', knownDevices: ['smlight slzb-07'], radioType: 'znp' },
  { vid: '1a86', pid: '55d4', description: '*sonoff*plus*', knownDevices: ['sonoff zigbee dongle plus v2'], radioType: 'ezsp' },
  { vid: '10c4', pid: 'ea60', description: '*sonoff*plus*', knownDevices: ['sonoff zigbee dongle plus'], radioType: 'znp' },
  { vid: '10c4', pid: 'ea60', description: '*tubeszb*', knownDevices: ['TubesZB Coordinator'], radioType: 'znp' },
  { vid: '1a86', pid: '7523', description: '*tubeszb*', knownDevices: ['TubesZB Coordinator'], radioType: 'znp' },
  { vid: '1a86', pid: '7523', description: '*zigstar*', knownDevices: ['ZigStar Coordinators'], radioType: 'znp' },
  { vid: '1cf1', pid: '0030', description: '*conbee*', knownDevices: ['Conbee II'], radioType: 'deconz' },
  { vid: '0403', pid: '6015', description: '*conbee*', knownDevices: ['Conbee III'], radioType: 'deconz' },
  { vid: '10c4', pid: '8a2a', description: '*zigbee*', knownDevices: ['Nortek HUSBZB-1'], radioType: 'ezsp' },
  { vid: '0403', pid: '6015', description: '*zigate*', knownDevices: ['ZiGate+'], radioType: 'zigate' },
  { vid: '10c4', pid: 'ea60', description: '*zigate*', knownDevices: ['ZiGate'], radioType: 'zigate' },
  { vid: '10c4', pid: '8b34', description: '*bv 2010/10*', knownDevices: ['Bitron Video AV2010/10'] },
  { vid: '10c4', pid: 'ea60', description: '*sonoff*max*', knownDevices: ['SONOFF Dongle Max MG24'], radioType: 'ezsp' },
  { vid: '10c4', pid: 'ea60', description: '*sonoff*lite*mg21*', knownDevices: ['sonoff zigbee dongle lite mg21'], radioType: 'ezsp' },
];

const commonZigbeeUsbIds = new Set(haManifestUsbDevices.map((entryArg) => `${entryArg.vid}:${entryArg.pid}`));

const zigbeeTextHints = [
  'zigbee',
  'conbee',
  'sonoff',
  'zigate',
  'zigstar',
  'tubeszb',
  'slzb',
  'cc2652',
  'cc1352',
  'efr32',
  'ember',
  'husbzb',
  'skyconnect',
  'zbt',
];

export class ZhaUsbMatcher implements IDiscoveryMatcher<IZhaUsbRecord> {
  public id = 'zha-usb-match';
  public source = 'usb' as const;
  public description = 'Recognize known USB Zigbee coordinators supported by ZHA.';

  public async matches(recordArg: IZhaUsbRecord): Promise<IDiscoveryMatch> {
    const vid = this.normalizeUsbId(recordArg.vid);
    const pid = this.normalizeUsbId(recordArg.pid);
    const usbId = `${vid}:${pid}`;
    const path = recordArg.path || recordArg.device;
    const serialNumber = recordArg.serialNumber || recordArg.serial_number;
    const text = this.recordText(recordArg);
    const haMatch = haManifestUsbDevices.find((entryArg) => entryArg.vid === vid && entryArg.pid === pid && this.matchesManifestEntry(entryArg, text));
    const idMatch = commonZigbeeUsbIds.has(usbId);
    const textMatch = zigbeeTextHints.some((hintArg) => text.includes(hintArg));
    const matched = Boolean(haMatch || idMatch || textMatch);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'USB device is not a known ZHA Zigbee coordinator.' };
    }
    const radioType = haMatch?.radioType || this.guessRadioType(text, usbId);
    return {
      matched: true,
      confidence: haMatch ? 'certain' : idMatch ? 'high' : 'medium',
      reason: haMatch ? 'USB record matches Home Assistant ZHA manifest USB metadata.' : 'USB record matches common Zigbee coordinator metadata.',
      normalizedDeviceId: serialNumber || path || usbId,
      candidate: {
        source: 'usb',
        integrationDomain: 'zha',
        id: serialNumber || path || usbId,
        name: recordArg.description || recordArg.product || 'Zigbee coordinator',
        manufacturer: recordArg.manufacturer || this.manufacturerFromText(text),
        model: recordArg.description || recordArg.product || haMatch?.knownDevices?.[0],
        serialNumber,
        metadata: {
          vid: recordArg.vid,
          pid: recordArg.pid,
          path,
          radioPath: path,
          serialNumber,
          radioType,
          baudrate: 115200,
          flowControl: 'none',
          haUsb: haMatch,
        },
      },
    };
  }

  private normalizeUsbId(valueArg?: string): string {
    return (valueArg || '').replace(/^0x/i, '').toLowerCase().padStart(4, '0');
  }

  private recordText(recordArg: IZhaUsbRecord): string {
    return [recordArg.manufacturer, recordArg.description, recordArg.product, recordArg.serialNumber, recordArg.serial_number]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
  }

  private matchesManifestEntry(entryArg: IZhaKnownUsbDevice, textArg: string): boolean {
    if (!entryArg.description && !entryArg.knownDevices?.length) {
      return true;
    }
    const descriptionMatch = entryArg.description ? this.wildcardMatch(entryArg.description, textArg) : false;
    const knownDeviceMatch = (entryArg.knownDevices || []).some((deviceArg) => textArg.includes(deviceArg.toLowerCase()));
    return descriptionMatch || knownDeviceMatch;
  }

  private wildcardMatch(patternArg: string, valueArg: string): boolean {
    const parts = patternArg.toLowerCase().split('*').filter(Boolean);
    return parts.every((partArg) => valueArg.includes(partArg));
  }

  private guessRadioType(textArg: string, usbIdArg: string): TZhaRadioType | undefined {
    if (textArg.includes('conbee')) {
      return 'deconz';
    }
    if (textArg.includes('zigate')) {
      return 'zigate';
    }
    if (textArg.includes('xbee')) {
      return 'xbee';
    }
    if (textArg.includes('sonoff') && (textArg.includes('v2') || textArg.includes('mg21') || textArg.includes('mg24') || usbIdArg === '1a86:55d4')) {
      return 'ezsp';
    }
    if (textArg.includes('efr32') || textArg.includes('ember') || textArg.includes('skyconnect') || textArg.includes('zbt')) {
      return 'ezsp';
    }
    if (textArg.includes('cc2652') || textArg.includes('cc1352') || textArg.includes('zigstar') || textArg.includes('tubeszb') || textArg.includes('slzb')) {
      return 'znp';
    }
    return undefined;
  }

  private manufacturerFromText(textArg: string): string {
    if (textArg.includes('sonoff')) {
      return 'SONOFF';
    }
    if (textArg.includes('conbee')) {
      return 'dresden elektronik';
    }
    if (textArg.includes('zigate')) {
      return 'ZiGate';
    }
    return 'Zigbee';
  }
}

export class ZhaMdnsMatcher implements IDiscoveryMatcher<IZhaMdnsRecord> {
  public id = 'zha-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize network-attached Zigbee coordinators advertised for ZHA.';

  public async matches(recordArg: IZhaMdnsRecord): Promise<IDiscoveryMatch> {
    const type = this.normalizeType(recordArg.type);
    const txt = recordArg.txt || recordArg.properties || {};
    const text = [recordArg.name, recordArg.hostname, this.txt(txt, 'name'), this.txt(txt, 'radio_type')].filter(Boolean).join(' ').toLowerCase();
    const isNativeType = zhaZeroconfTypes.has(type);
    const isLegacyType = legacyZeroconfTypes.has(type) && (text.includes('tube') || text.includes('zigbee'));
    const matched = isNativeType || isLegacyType || Boolean(this.txt(txt, 'radio_type') && this.txt(txt, 'serial_number'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a ZHA Zigbee coordinator advertisement.' };
    }
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const port = recordArg.port || defaultZigbeeCoordinatorPort;
    const radioType = this.txt(txt, 'radio_type') || this.guessRadioType(text, type);
    const serialNumber = this.txt(txt, 'serial_number') || recordArg.hostname?.replace(/\.local\.?$/i, '');
    return {
      matched: true,
      confidence: radioType && host ? 'certain' : host ? 'high' : 'medium',
      reason: 'mDNS record matches ZHA network coordinator metadata.',
      normalizedDeviceId: serialNumber || recordArg.name || host,
      candidate: {
        source: 'mdns',
        integrationDomain: 'zha',
        id: serialNumber || recordArg.name || host,
        host,
        port,
        name: this.deviceName(recordArg, txt) || 'Zigbee coordinator',
        manufacturer: 'Zigbee',
        model: 'Network Zigbee coordinator',
        serialNumber,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt,
          radioType,
          radioPath: host ? `socket://${host}:${port}` : undefined,
          socketUrl: host ? `socket://${host}:${port}` : undefined,
          baudrate: 115200,
          flowControl: 'none',
        },
      },
    };
  }

  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: IZhaMdnsRecord, txtArg: Record<string, string | undefined>): string | undefined {
    return this.txt(txtArg, 'name') || recordArg.name?.split('._', 1)[0] || recordArg.hostname?.replace(/\.local\.?$/i, '');
  }

  private guessRadioType(textArg: string, typeArg: string): TZhaRadioType | undefined {
    if (textArg.includes('zigate') || typeArg.includes('zigate')) {
      return 'zigate';
    }
    if (textArg.includes('efr32') || textArg.includes('ezsp')) {
      return 'ezsp';
    }
    if (textArg.includes('deconz') || textArg.includes('conbee')) {
      return 'deconz';
    }
    if (textArg.includes('znp') || textArg.includes('zigstar') || textArg.includes('slzb') || textArg.includes('uzg') || textArg.includes('xzg')) {
      return 'znp';
    }
    return undefined;
  }
}

export class ZhaManualMatcher implements IDiscoveryMatcher<IZhaManualEntry> {
  public id = 'zha-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual ZHA radio setup entries.';

  public async matches(inputArg: IZhaManualEntry): Promise<IDiscoveryMatch> {
    const radioPath = inputArg.radioPath || inputArg.devicePath || inputArg.usbPath || inputArg.path || inputArg.socketUrl || (inputArg.host ? `socket://${inputArg.host}:${inputArg.port || defaultZigbeeCoordinatorPort}` : undefined);
    const model = inputArg.model?.toLowerCase() || '';
    const manufacturer = inputArg.manufacturer?.toLowerCase() || '';
    const matched = Boolean(radioPath || inputArg.radioType || inputArg.metadata?.zha || inputArg.metadata?.zigbee || model.includes('zigbee') || manufacturer.includes('zigbee'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain ZHA radio setup hints.' };
    }
    return {
      matched: true,
      confidence: radioPath && inputArg.radioType ? 'high' : 'medium',
      reason: 'Manual entry can start ZHA radio setup.',
      normalizedDeviceId: inputArg.id || radioPath,
      candidate: {
        source: 'manual',
        integrationDomain: 'zha',
        id: inputArg.id || radioPath,
        host: inputArg.host,
        port: inputArg.port,
        name: inputArg.name || 'Zigbee Home Automation',
        manufacturer: inputArg.manufacturer || 'Zigbee',
        model: inputArg.model || 'ZHA radio',
        metadata: {
          ...inputArg.metadata,
          radioPath,
          socketUrl: radioPath?.startsWith('socket://') ? radioPath : undefined,
          radioType: inputArg.radioType,
          baudrate: inputArg.baudrate,
          flowControl: inputArg.flowControl,
          snapshot: inputArg.snapshot,
        },
      },
    };
  }
}

export class ZhaCandidateValidator implements IDiscoveryValidator {
  public id = 'zha-candidate-validator';
  public description = 'Validate ZHA radio discovery candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const model = candidateArg.model?.toLowerCase() || '';
    const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
    const mdnsType = typeof metadata.mdnsType === 'string' ? metadata.mdnsType.toLowerCase().replace(/\.$/, '') : '';
    const vid = typeof metadata.vid === 'string' ? metadata.vid.replace(/^0x/i, '').toLowerCase().padStart(4, '0') : '';
    const pid = typeof metadata.pid === 'string' ? metadata.pid.replace(/^0x/i, '').toLowerCase().padStart(4, '0') : '';
    const usbRecognized = Boolean(vid && pid && commonZigbeeUsbIds.has(`${vid}:${pid}`));
    const hasRadioPath = typeof metadata.radioPath === 'string' || typeof metadata.socketUrl === 'string';
    const textMatched = zigbeeTextHints.some((hintArg) => model.includes(hintArg) || manufacturer.includes(hintArg));
    const matched = candidateArg.integrationDomain === 'zha' || usbRecognized || zhaZeroconfTypes.has(mdnsType) || Boolean(metadata.radioType || metadata.zha || metadata.zigbee || hasRadioPath && textMatched || textMatched);
    return {
      matched,
      confidence: matched && candidateArg.integrationDomain === 'zha' && (hasRadioPath || metadata.radioType) ? 'certain' : matched && (usbRecognized || hasRadioPath) ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has ZHA radio metadata.' : 'Candidate is not ZHA.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.serialNumber || candidateArg.id || candidateArg.host,
      metadata: matched ? { usbRecognized, hasRadioPath, radioType: metadata.radioType } : undefined,
    };
  }
}

export const createZhaDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'zha', displayName: 'Zigbee Home Automation' })
    .addMatcher(new ZhaUsbMatcher())
    .addMatcher(new ZhaMdnsMatcher())
    .addMatcher(new ZhaManualMatcher())
    .addValidator(new ZhaCandidateValidator());
};
