import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import {
  broadlinkDefaultPort,
  broadlinkDefaultTimeoutSeconds,
  broadlinkDomain,
  broadlinkMacPrefixes,
  broadlinkSupportedTypes,
} from './broadlink.constants.js';
import type {
  IBroadlinkCandidateMetadata,
  IBroadlinkDhcpRecord,
  IBroadlinkLocalDiscoveryRecord,
  IBroadlinkManualEntry,
} from './broadlink.types.js';

export class BroadlinkDhcpMatcher implements IDiscoveryMatcher<IBroadlinkDhcpRecord> {
  public id = 'broadlink-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize Broadlink DHCP leases using Home Assistant manifest MAC prefixes.';

  public async matches(recordArg: IBroadlinkDhcpRecord): Promise<IDiscoveryMatch> {
    const metadata = recordArg.metadata || {};
    const host = recordArg.host || recordArg.ipAddress || recordArg.address || recordArg.ip;
    const hostname = recordArg.hostname || recordArg.hostName;
    const macAddress = normalizeMac(recordArg.macAddress || recordArg.mac || stringValue(metadata.macAddress));
    const model = recordArg.model || stringValue(metadata.model) || stringValue(metadata.deviceType);
    const text = [recordArg.integrationDomain, recordArg.manufacturer, model, hostname, metadata.brand, metadata.manufacturer]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const macMatched = isBroadlinkMac(macAddress);
    const textMatched = text.includes('broadlink');
    const matched = recordArg.integrationDomain === broadlinkDomain || metadata.broadlink === true || macMatched || textMatched;

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record does not match Broadlink metadata.' };
    }

    const id = macAddress || stringValue(metadata.deviceId) || hostname || host;
    return {
      matched: true,
      confidence: macMatched && host ? 'certain' : host ? 'high' : 'medium',
      reason: macMatched ? 'DHCP MAC prefix matches Home Assistant Broadlink manifest rules.' : 'DHCP metadata identifies a Broadlink device.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: broadlinkDomain,
        id,
        host,
        port: broadlinkDefaultPort,
        name: hostname || model || 'Broadlink device',
        manufacturer: recordArg.manufacturer || 'Broadlink',
        model,
        macAddress,
        metadata: {
          ...metadata,
          broadlink: true,
          hostname,
          macMatched,
          discoveryProtocol: 'dhcp',
          timeout: broadlinkDefaultTimeoutSeconds,
        },
      },
      metadata: { macMatched, model },
    };
  }
}

export class BroadlinkLocalDiscoveryMatcher implements IDiscoveryMatcher<IBroadlinkLocalDiscoveryRecord> {
  public id = 'broadlink-local-discovery-match';
  public source = 'custom' as const;
  public description = 'Recognize normalized Broadlink UDP hello responses supplied by a host discovery layer.';

  public async matches(recordArg: IBroadlinkLocalDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = recordArg.metadata || {};
    const macAddress = normalizeMac(recordArg.macAddress || recordArg.mac || stringValue(metadata.macAddress));
    const deviceType = recordArg.type || stringValue(metadata.deviceType);
    const supportedType = isSupportedType(deviceType);
    const matched = Boolean(recordArg.host && (metadata.broadlink === true || macAddress || supportedType || recordArg.devtype || recordArg.model));

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Local discovery record is not a Broadlink hello response.' };
    }

    const id = macAddress || recordArg.host;
    return {
      matched: true,
      confidence: recordArg.host && macAddress && supportedType ? 'certain' : recordArg.host ? 'high' : 'medium',
      reason: supportedType ? 'Broadlink hello response contains a supported device type.' : 'Broadlink hello response contains local host or MAC data.',
      normalizedDeviceId: id,
      candidate: {
        source: 'custom',
        integrationDomain: broadlinkDomain,
        id,
        host: recordArg.host,
        port: recordArg.port || broadlinkDefaultPort,
        name: recordArg.name || recordArg.model || 'Broadlink device',
        manufacturer: recordArg.manufacturer || 'Broadlink',
        model: recordArg.model || deviceType,
        macAddress,
        metadata: {
          ...metadata,
          broadlink: true,
          discoveryProtocol: 'broadlink-hello',
          deviceType,
          devtype: recordArg.devtype,
          isLocked: recordArg.isLocked ?? recordArg.locked,
          timeout: broadlinkDefaultTimeoutSeconds,
        } satisfies IBroadlinkCandidateMetadata,
      },
    };
  }
}

export class BroadlinkManualMatcher implements IDiscoveryMatcher<IBroadlinkManualEntry> {
  public id = 'broadlink-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Broadlink host, snapshot, learned-code, and custom-switch setup entries.';

  public async matches(inputArg: IBroadlinkManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const macAddress = normalizeMac(inputArg.macAddress || inputArg.mac || stringValue(metadata.macAddress));
    const deviceType = inputArg.type || stringValue(metadata.deviceType);
    const text = [inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, deviceType, metadata.brand, metadata.manufacturer]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const snapshot = inputArg.snapshot || metadata.snapshot as IBroadlinkCandidateMetadata['snapshot'];
    const hasManualData = Boolean(inputArg.host || inputArg.device || inputArg.devices?.length || inputArg.codes || inputArg.switches?.length || inputArg.state || inputArg.sensors);
    const matched = inputArg.integrationDomain === broadlinkDomain
      || metadata.broadlink === true
      || Boolean(snapshot)
      || Boolean(macAddress && isBroadlinkMac(macAddress))
      || isSupportedType(deviceType)
      || text.includes('broadlink')
      || hasManualData && Boolean(inputArg.host || macAddress || deviceType || inputArg.codes || inputArg.switches?.length);

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Broadlink setup data.' };
    }

    const id = inputArg.id || inputArg.deviceId || macAddress || inputArg.host;
    return {
      matched: true,
      confidence: snapshot ? 'certain' : inputArg.host && (macAddress || deviceType) ? 'high' : inputArg.host ? 'medium' : 'low',
      reason: snapshot ? 'Manual entry includes a Broadlink snapshot.' : 'Manual entry can start Broadlink setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: broadlinkDomain,
        id,
        host: inputArg.host,
        port: inputArg.port || broadlinkDefaultPort,
        name: inputArg.name || inputArg.model || 'Broadlink device',
        manufacturer: inputArg.manufacturer || 'Broadlink',
        model: inputArg.model || deviceType,
        macAddress,
        metadata: {
          ...metadata,
          broadlink: true,
          manual: true,
          deviceType,
          devtype: inputArg.devtype,
          timeout: inputArg.timeout || broadlinkDefaultTimeoutSeconds,
          snapshot,
          device: inputArg.device,
          devices: inputArg.devices,
          state: inputArg.state,
          sensors: inputArg.sensors,
          codesConfigured: Boolean(inputArg.codes),
          customSwitchesConfigured: Boolean(inputArg.switches?.length),
        } satisfies IBroadlinkCandidateMetadata,
      },
      metadata: { snapshotConfigured: Boolean(snapshot), codesConfigured: Boolean(inputArg.codes) },
    };
  }
}

export class BroadlinkCandidateValidator implements IDiscoveryValidator {
  public id = 'broadlink-candidate-validator';
  public description = 'Validate Broadlink candidates from DHCP, local hello, and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const macAddress = normalizeMac(candidateArg.macAddress || stringValue(metadata.macAddress));
    const deviceType = stringValue(metadata.deviceType) || candidateArg.model;
    const text = [candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.brand, metadata.manufacturer]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const macMatched = isBroadlinkMac(macAddress);
    const snapshotConfigured = metadata.snapshot !== undefined;
    const matched = candidateArg.integrationDomain === broadlinkDomain
      || metadata.broadlink === true
      || snapshotConfigured
      || macMatched
      || isSupportedType(deviceType)
      || text.includes('broadlink')
      || candidateArg.source === 'manual' && Boolean(candidateArg.host);

    return {
      matched,
      confidence: matched && (macMatched || snapshotConfigured || candidateArg.integrationDomain === broadlinkDomain) && candidateArg.host ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Broadlink metadata or manual setup data.' : 'Candidate is not Broadlink.',
      candidate: matched ? { ...candidateArg, port: candidateArg.port || broadlinkDefaultPort } : undefined,
      normalizedDeviceId: macAddress || candidateArg.id || candidateArg.host,
      metadata: matched ? { macMatched, snapshotConfigured, deviceType } : undefined,
    };
  }
}

export const createBroadlinkDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: broadlinkDomain, displayName: 'Broadlink' })
    .addMatcher(new BroadlinkDhcpMatcher())
    .addMatcher(new BroadlinkLocalDiscoveryMatcher())
    .addMatcher(new BroadlinkManualMatcher())
    .addValidator(new BroadlinkCandidateValidator());
};

export const normalizeBroadlinkMac = (valueArg?: string): string | undefined => normalizeMac(valueArg);

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

const isBroadlinkMac = (valueArg?: string): boolean => {
  const compact = (valueArg || '').replace(/[^0-9a-f]/gi, '').toLowerCase();
  return broadlinkMacPrefixes.some((prefixArg) => compact.startsWith(prefixArg));
};

const isSupportedType = (valueArg?: string): boolean => {
  return Boolean(valueArg && broadlinkSupportedTypes.has(valueArg.toUpperCase()));
};

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