import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { dlinkDefaultName, dlinkDefaultPort, dlinkDhcpHostnames, dlinkDomain, dlinkManufacturer, dlinkSupportedModels } from './dlink.constants.js';
import type { IDlinkCandidateMetadata, IDlinkDhcpRecord, IDlinkManualEntry } from './dlink.types.js';

export class DlinkDhcpMatcher implements IDiscoveryMatcher<IDlinkDhcpRecord> {
  public id = 'dlink-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize D-Link W215 DHCP leases using the Home Assistant manifest hostname matcher.';

  public async matches(recordArg: IDlinkDhcpRecord): Promise<IDiscoveryMatch> {
    const metadata = recordArg.metadata || {};
    const host = recordArg.host || recordArg.ipAddress || recordArg.address || recordArg.ip || stringValue(metadata.host);
    const hostname = recordArg.hostname || recordArg.hostName || stringValue(metadata.hostname);
    const model = recordArg.model || stringValue(metadata.model) || stringValue(metadata.modelName);
    const macAddress = normalizeMac(recordArg.macAddress || recordArg.mac || stringValue(metadata.macAddress));
    const normalizedHostname = normalizeHostname(hostname);
    const hostnameMatched = dlinkDhcpHostnames.has(normalizedHostname) || [...dlinkDhcpHostnames].some((hostnameArg) => normalizedHostname.startsWith(`${hostnameArg}-`));
    const modelMatched = isDlinkModel(model);
    const textMatched = dlinkText(recordArg.integrationDomain, recordArg.manufacturer, model, hostname, metadata.brand, metadata.manufacturer).includes('d-link')
      || dlinkText(recordArg.integrationDomain, recordArg.manufacturer, model, hostname, metadata.brand, metadata.manufacturer).includes('dlink');
    const matched = recordArg.integrationDomain === dlinkDomain || metadata.dlink === true || hostnameMatched || modelMatched || textMatched;

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record does not match the D-Link W215 hostname or metadata.' };
    }

    const id = macAddress || hostname || host;
    return {
      matched: true,
      confidence: hostnameMatched && host ? 'certain' : host ? 'high' : 'medium',
      reason: hostnameMatched ? 'DHCP hostname matches the Home Assistant dlink manifest rule dsp-w215.' : 'DHCP metadata identifies a D-Link smart plug.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: dlinkDomain,
        id,
        host,
        port: dlinkDefaultPort,
        name: hostname || dlinkDefaultName,
        manufacturer: recordArg.manufacturer || dlinkManufacturer,
        model: model || dlinkDefaultName,
        macAddress,
        metadata: {
          ...metadata,
          dlink: true,
          discoveryProtocol: 'dhcp',
          hostname,
          hostnameMatched,
          useLegacyProtocol: booleanValue(metadata.useLegacyProtocol) ?? booleanValue(metadata.use_legacy_protocol) ?? false,
          nativeHttpImplemented: true,
        } satisfies IDlinkCandidateMetadata,
      },
      metadata: { hostnameMatched, modelMatched },
    };
  }
}

export class DlinkManualMatcher implements IDiscoveryMatcher<IDlinkManualEntry> {
  public id = 'dlink-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual D-Link smart plug host, credential, and snapshot setup entries.';

  public async matches(inputArg: IDlinkManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot || metadata.snapshot as IDlinkManualEntry['snapshot'];
    const host = inputArg.host || inputArg.ipAddress || inputArg.address || inputArg.ip || snapshot?.device.host || stringValue(metadata.host);
    const model = inputArg.modelName || inputArg.model || snapshot?.device.modelName || stringValue(metadata.modelName) || stringValue(metadata.model);
    const macAddress = normalizeMac(inputArg.macAddress || inputArg.mac || snapshot?.device.macAddress || stringValue(metadata.macAddress));
    const hasCredentials = Boolean(inputArg.password || metadata.password);
    const modelMatched = isDlinkModel(model);
    const textMatched = dlinkText(inputArg.integrationDomain, inputArg.manufacturer, model, inputArg.name, metadata.brand, metadata.manufacturer).includes('d-link')
      || dlinkText(inputArg.integrationDomain, inputArg.manufacturer, model, inputArg.name, metadata.brand, metadata.manufacturer).includes('dlink');
    const matched = inputArg.integrationDomain === dlinkDomain
      || metadata.dlink === true
      || Boolean(snapshot)
      || modelMatched
      || textMatched
      || Boolean(host && hasCredentials);

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

    const id = inputArg.id || macAddress || snapshot?.device.id || host;
    return {
      matched: true,
      confidence: snapshot ? 'certain' : host && hasCredentials ? 'high' : host ? 'medium' : 'low',
      reason: snapshot ? 'Manual entry includes a D-Link smart plug snapshot.' : 'Manual entry can start D-Link smart plug setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: dlinkDomain,
        id,
        host,
        port: inputArg.port || snapshot?.device.port || dlinkDefaultPort,
        name: inputArg.name || snapshot?.device.name || dlinkDefaultName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || dlinkManufacturer,
        model: model || dlinkDefaultName,
        macAddress,
        metadata: {
          ...metadata,
          dlink: true,
          discoveryProtocol: 'manual',
          username: inputArg.username || stringValue(metadata.username),
          useLegacyProtocol: booleanValue(inputArg.useLegacyProtocol ?? inputArg.use_legacy_protocol ?? metadata.useLegacyProtocol ?? metadata.use_legacy_protocol) ?? false,
          snapshot,
          nativeHttpImplemented: true,
        } satisfies IDlinkCandidateMetadata,
      },
      metadata: { snapshotConfigured: Boolean(snapshot), credentialsConfigured: hasCredentials },
    };
  }
}

export class DlinkCandidateValidator implements IDiscoveryValidator {
  public id = 'dlink-candidate-validator';
  public description = 'Validate D-Link candidates from DHCP and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const snapshot = metadata.snapshot as IDlinkManualEntry['snapshot'];
    const hostname = stringValue(metadata.hostname) || candidateArg.name;
    const hostnameMatched = dlinkDhcpHostnames.has(normalizeHostname(hostname));
    const modelMatched = isDlinkModel(candidateArg.model || stringValue(metadata.modelName) || stringValue(metadata.model));
    const textMatched = dlinkText(candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.brand, metadata.manufacturer).includes('d-link')
      || dlinkText(candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.brand, metadata.manufacturer).includes('dlink');
    const matched = candidateArg.integrationDomain === dlinkDomain || metadata.dlink === true || Boolean(snapshot) || hostnameMatched || modelMatched || textMatched;
    const usable = Boolean(candidateArg.host || snapshot?.device.host || snapshot);

    if (!matched || !usable) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'D-Link candidate lacks a host or snapshot.' : 'Candidate is not D-Link.',
      };
    }

    return {
      matched: true,
      confidence: hostnameMatched && candidateArg.host ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has D-Link smart plug metadata and a usable host or snapshot.',
      candidate: { ...candidateArg, port: candidateArg.port || dlinkDefaultPort },
      normalizedDeviceId: candidateArg.macAddress || candidateArg.id || candidateArg.host || snapshot?.device.id,
      metadata: { hostnameMatched, modelMatched, snapshotConfigured: Boolean(snapshot) },
    };
  }
}

export const createDlinkDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: dlinkDomain, displayName: dlinkDefaultName })
    .addMatcher(new DlinkDhcpMatcher())
    .addMatcher(new DlinkManualMatcher())
    .addValidator(new DlinkCandidateValidator());
};

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

const dlinkText = (...valuesArg: unknown[]): string => {
  return valuesArg.filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
};

const normalizeHostname = (valueArg?: string): string => {
  return (valueArg || '').trim().toLowerCase().replace(/\.local\.?$/, '');
};

const isDlinkModel = (valueArg?: string): boolean => {
  const normalized = (valueArg || '').trim().toLowerCase();
  return dlinkSupportedModels.has(normalized) || [...dlinkSupportedModels].some((modelArg) => normalized.includes(modelArg));
};

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 stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    const normalized = valueArg.trim().toLowerCase();
    if (normalized === 'true' || normalized === 'yes' || normalized === '1') {
      return true;
    }
    if (normalized === 'false' || normalized === 'no' || normalized === '0') {
      return false;
    }
  }
  return undefined;
};
