import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { ITplinkDhcpRecord, ITplinkManualDiscoveryRecord, ITplinkMdnsRecord } from './tplink.types.js';
import { tplinkDefaultHttpPort, tplinkLegacyDiscoveryPort, tplinkSmartDiscoveryPort } from './tplink.types.js';

const tplinkMacPrefixes = [
  '3c52a1', '54af97', 'e848b8', '1c61b4', '003192', 'b4b024', '9c5322', '5091e3',
  '1c3bf3', '50c7bf', '68ff7b', '98dac4', 'b09575', 'c006c3', '60a4b7', '005f67',
  '1027f5', 'b0a7b9', '403f8c', 'c0c9e3', '909a4a', '6c5ab0', 'ac15a2', '788cb5',
  '3460f9', '5ce931', '5c628b', '14ebb6', '482254', '30de4b', 'a842a1', '704f57',
  '74da88', 'cc32e5', 'd80d17', 'd84732', 'f0a731',
];

const tplinkHostnamePatterns = [/^e[sp]/i, /^hs/i, /^k[lps]/i, /^p[13]/i, /^s5/i, /^l[59]/i, /^tp/i, /^h1/i, /^ks2/i, /^kh1/i];
const tplinkTextHints = ['tp-link', 'tplink', 'kasa', 'tapo', 'smart plug', 'smart bulb', 'smart switch', 'smart dimmer'];

export class TplinkMdnsMatcher implements IDiscoveryMatcher<ITplinkMdnsRecord> {
  public id = 'tplink-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize TP-Link Kasa/Tapo mDNS records by service, host, TXT, MAC, or model metadata.';

  public async matches(recordArg: ITplinkMdnsRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const txt = recordArg.txt || recordArg.properties || {};
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const hostname = recordArg.hostname || recordArg.host || recordArg.name;
    const model = this.txt(txt, 'model') || this.txt(txt, 'modelid') || recordArg.model;
    const manufacturer = recordArg.manufacturer || this.txt(txt, 'manufacturer') || this.txt(txt, 'vendor');
    const macAddress = normalizeMac(recordArg.macAddress || this.txt(txt, 'mac') || this.txt(txt, 'macaddress') || this.txt(txt, 'mac_address'));
    const text = [recordArg.type, recordArg.serviceType, recordArg.name, hostname, host, model, manufacturer, this.txt(txt, 'brand')]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const macMatched = isTplinkMac(macAddress);
    const hostMatched = isTplinkHostname(hostname);
    const textMatched = hasTplinkTextHint(text);
    const matched = macMatched || textMatched || hostMatched && text.includes('local');

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a TP-Link Kasa/Tapo advertisement.' };
    }

    const id = macAddress || this.txt(txt, 'device_id') || this.txt(txt, 'deviceid') || recordArg.name || host;
    return {
      matched: true,
      confidence: macMatched && host ? 'certain' : host && (textMatched || hostMatched) ? 'high' : 'medium',
      reason: macMatched ? 'mDNS record contains a known TP-Link MAC prefix.' : 'mDNS record contains Kasa/Tapo/TP-Link metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'tplink',
        id,
        host,
        port: recordArg.port || tplinkDefaultHttpPort,
        name: this.txt(txt, 'alias') || this.txt(txt, 'name') || recordArg.name || model,
        manufacturer: manufacturer || 'TP-Link',
        model,
        macAddress,
        metadata: {
          tplink: true,
          mdnsName: recordArg.name,
          mdnsType: recordArg.type || recordArg.serviceType,
          txt,
          model,
          hostMatched,
          macMatched,
          discoveryPorts: [tplinkLegacyDiscoveryPort, tplinkSmartDiscoveryPort],
        },
      },
      metadata: { model, macAddress, hostMatched, macMatched },
    };
  }

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

export class TplinkDhcpMatcher implements IDiscoveryMatcher<ITplinkDhcpRecord> {
  public id = 'tplink-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize Kasa/Tapo DHCP leases using Home Assistant TP-Link hostname and MAC rules.';

  public async matches(recordArg: ITplinkDhcpRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    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 || this.stringValue(metadata.macAddress));
    const model = recordArg.model || this.stringValue(metadata.model);
    const manufacturer = recordArg.manufacturer || this.stringValue(metadata.manufacturer);
    const text = [hostname, manufacturer, model, recordArg.vendorClassIdentifier, metadata.brand, metadata.deviceType]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const macMatched = isTplinkMac(macAddress);
    const hostMatched = isTplinkHostname(hostname);
    const textMatched = hasTplinkTextHint(text);
    const matched = recordArg.integrationDomain === 'tplink'
      || metadata.tplink === true
      || metadata.kasa === true
      || metadata.tapo === true
      || macMatched
      || hostMatched && textMatched;

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record does not match TP-Link Kasa/Tapo metadata.' };
    }

    const id = macAddress || this.stringValue(metadata.deviceId) || hostname || host;
    return {
      matched: true,
      confidence: macMatched && host ? 'certain' : host && (hostMatched || textMatched) ? 'high' : 'medium',
      reason: macMatched ? 'DHCP MAC prefix matches Home Assistant TP-Link manifest rules.' : 'DHCP hostname or metadata matches TP-Link Kasa/Tapo.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: 'tplink',
        id,
        host,
        port: tplinkDefaultHttpPort,
        name: hostname || model || 'TP-Link Smart Home device',
        manufacturer: manufacturer || 'TP-Link',
        model,
        macAddress,
        metadata: {
          ...metadata,
          tplink: true,
          hostname,
          macMatched,
          hostMatched,
          discoveryPorts: [tplinkLegacyDiscoveryPort, tplinkSmartDiscoveryPort],
        },
      },
      metadata: { macMatched, hostMatched, model },
    };
  }

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

export class TplinkManualMatcher implements IDiscoveryMatcher<ITplinkManualDiscoveryRecord> {
  public id = 'tplink-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Kasa/Tapo setup entries, including snapshot-only records.';

  public async matches(inputArg: ITplinkManualDiscoveryRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const host = inputArg.host;
    const model = inputArg.model;
    const macAddress = normalizeMac(inputArg.macAddress || inputArg.mac);
    const text = [inputArg.integrationDomain, inputArg.manufacturer, inputArg.brand, inputArg.model, inputArg.alias, inputArg.name, metadata.brand, metadata.model]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const snapshot = inputArg.snapshot || metadata.snapshot;
    const matched = inputArg.integrationDomain === 'tplink'
      || metadata.tplink === true
      || metadata.kasa === true
      || metadata.tapo === true
      || Boolean(snapshot)
      || Boolean(host && (hasTplinkTextHint(text) || model || macAddress))
      || Boolean(host && !text);

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

    const id = inputArg.id || inputArg.deviceId || macAddress || host || `snapshot-${Date.now()}`;
    return {
      matched: true,
      confidence: snapshot ? 'certain' : host && (macAddress || model) ? 'high' : host ? 'medium' : 'low',
      reason: snapshot ? 'Manual entry includes a TP-Link snapshot.' : 'Manual entry can start TP-Link setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'tplink',
        id,
        host,
        port: inputArg.port || tplinkDefaultHttpPort,
        name: inputArg.alias || inputArg.name || model || 'TP-Link Smart Home device',
        manufacturer: inputArg.manufacturer || 'TP-Link',
        model,
        macAddress,
        metadata: {
          ...metadata,
          tplink: true,
          manual: true,
          deviceType: inputArg.deviceType,
          snapshot,
          device: inputArg.device,
          devices: inputArg.devices,
          credentialsConfigured: Boolean(inputArg.credentials?.username || inputArg.credentials?.credentialsHash),
        },
      },
      metadata: { snapshotConfigured: Boolean(snapshot), credentialsConfigured: Boolean(inputArg.credentials) },
    };
  }
}

export class TplinkCandidateValidator implements IDiscoveryValidator {
  public id = 'tplink-candidate-validator';
  public description = 'Validate TP-Link Kasa/Tapo candidates from mDNS, DHCP, and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const macAddress = normalizeMac(candidateArg.macAddress || this.stringValue(metadata.macAddress));
    const text = [candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.brand, metadata.model, metadata.deviceType]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const snapshotConfigured = metadata.snapshot !== undefined;
    const macMatched = isTplinkMac(macAddress);
    const textMatched = hasTplinkTextHint(text);
    const matched = candidateArg.integrationDomain === 'tplink'
      || metadata.tplink === true
      || metadata.kasa === true
      || metadata.tapo === true
      || snapshotConfigured
      || macMatched
      || textMatched
      || candidateArg.source === 'manual' && Boolean(candidateArg.host);

    return {
      matched,
      confidence: matched && (macMatched || snapshotConfigured || candidateArg.integrationDomain === 'tplink') && candidateArg.host ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has TP-Link Kasa/Tapo metadata or manual setup data.' : 'Candidate is not TP-Link Kasa/Tapo.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: macAddress || candidateArg.id || candidateArg.host,
      metadata: matched ? { macMatched, snapshotConfigured, encryptedLocalProtocolImplemented: false } : undefined,
    };
  }

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

export const createTplinkDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'tplink', displayName: 'TP-Link Smart Home' })
    .addMatcher(new TplinkMdnsMatcher())
    .addMatcher(new TplinkDhcpMatcher())
    .addMatcher(new TplinkManualMatcher())
    .addValidator(new TplinkCandidateValidator());
};

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 isTplinkMac = (valueArg?: string): boolean => {
  const compact = (valueArg || '').replace(/[^0-9a-f]/gi, '').toLowerCase();
  return tplinkMacPrefixes.some((prefixArg) => compact.startsWith(prefixArg));
};

const isTplinkHostname = (valueArg?: string): boolean => {
  return Boolean(valueArg && tplinkHostnamePatterns.some((patternArg) => patternArg.test(valueArg)));
};

const hasTplinkTextHint = (valueArg: string): boolean => {
  return tplinkTextHints.some((hintArg) => valueArg.includes(hintArg));
};
