import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IDeconzDiscoveryRecord, IDeconzManualEntry, IDeconzMdnsRecord, IDeconzSsdpRecord } from './deconz.types.js';

const DECONZ_MANUFACTURER = 'dresden elektronik';
const DECONZ_MANUFACTURER_URL = 'http://www.dresden-elektronik.de';

export class DeconzMdnsMatcher implements IDiscoveryMatcher<IDeconzMdnsRecord> {
  public id = 'deconz-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize deCONZ and Phoscon mDNS records.';

  public async matches(recordArg: IDeconzMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const txt = recordArg.txt || {};
    const id = normalizeBridgeId(txt.bridgeid || txt.id || txt.serial || txt.serialnumber);
    const metadata = [recordArg.name, recordArg.type, txt.modelid, txt.model, txt.manufacturer, txt.manufacturername].join(' ').toLowerCase();
    const matched = recordArg.type?.toLowerCase() === '_deconz._tcp.local.' || hasDeconzText(metadata);

    if (!matched) {
      return {
        matched: false,
        confidence: 'low',
        reason: 'mDNS record does not contain deCONZ or Phoscon metadata.',
      };
    }

    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'mDNS record contains deCONZ or Phoscon metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'deconz',
        id,
        host: recordArg.host,
        port: recordArg.port || 80,
        manufacturer: 'dresden elektronik',
        model: txt.modelid || txt.model || 'deCONZ',
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt,
        },
      },
    };
  }
}

export class DeconzSsdpMatcher implements IDiscoveryMatcher<IDeconzSsdpRecord> {
  public id = 'deconz-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize deCONZ SSDP records by dresden elektronik manufacturer metadata.';

  public async matches(recordArg: IDeconzSsdpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const upnp = recordArg.upnp || {};
    const manufacturerUrl = recordArg.manufacturerURL || upnp.manufacturerURL || upnp.manufacturerUrl || '';
    const manufacturer = recordArg.manufacturer || upnp.manufacturer || '';
    const model = recordArg.modelName || recordArg.modelNumber || upnp.modelName || upnp.modelNumber || '';
    const friendlyName = recordArg.friendlyName || upnp.friendlyName || '';
    const serial = recordArg.serialNumber || upnp.serialNumber || upnp.serial || upnp.UDN;
    const id = normalizeBridgeId(serial);
    const metadata = [manufacturerUrl, manufacturer, model, friendlyName].join(' ').toLowerCase();
    const matched = manufacturerUrl.toLowerCase() === DECONZ_MANUFACTURER_URL || hasDeconzText(metadata);

    if (!matched) {
      return {
        matched: false,
        confidence: 'low',
        reason: 'SSDP record does not contain deCONZ manufacturer metadata.',
      };
    }

    const endpoint = parseEndpoint(recordArg.ssdpLocation || recordArg.location);
    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'SSDP record matches deCONZ manufacturer metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'deconz',
        id,
        host: endpoint.host,
        port: endpoint.port || 80,
        manufacturer: DECONZ_MANUFACTURER,
        model: model || 'deCONZ',
        name: friendlyName || undefined,
        metadata: {
          manufacturer,
          manufacturerUrl,
          ssdpLocation: recordArg.ssdpLocation || recordArg.location,
          upnp,
        },
      },
    };
  }
}

export class DeconzManualMatcher implements IDiscoveryMatcher<IDeconzManualEntry> {
  public id = 'deconz-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual deCONZ setup entries by host and deCONZ metadata.';

  public async matches(inputArg: IDeconzManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = [
      inputArg.name,
      inputArg.manufacturer,
      inputArg.model,
      inputArg.metadata ? JSON.stringify(inputArg.metadata) : '',
    ].join(' ').toLowerCase();
    const matched = Boolean(inputArg.host && (hasDeconzText(metadata) || inputArg.metadata?.deconz || inputArg.metadata?.phoscon));

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

    const id = normalizeBridgeId(inputArg.id);
    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'Manual entry can start deCONZ setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'deconz',
        id,
        host: inputArg.host,
        port: inputArg.port || 80,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || DECONZ_MANUFACTURER,
        model: inputArg.model || 'deCONZ',
        metadata: inputArg.metadata,
      },
    };
  }
}

export class DeconzPhosconDiscoveryMatcher implements IDiscoveryMatcher<IDeconzDiscoveryRecord> {
  public id = 'deconz-phoscon-discovery-match';
  public source = 'broker' as const;
  public description = 'Recognize Phoscon discovery broker records.';

  public async matches(recordArg: IDeconzDiscoveryRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const host = recordArg.internalipaddress;
    if (!host) {
      return {
        matched: false,
        confidence: 'low',
        reason: 'Phoscon discovery record does not include an internal IP address.',
      };
    }

    const id = normalizeBridgeId(recordArg.id || recordArg.macaddress);
    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'Phoscon discovery record contains a local deCONZ gateway endpoint.',
      normalizedDeviceId: id,
      candidate: {
        source: 'broker',
        integrationDomain: 'deconz',
        id,
        host,
        port: Number(recordArg.internalport || 80),
        name: recordArg.name,
        manufacturer: DECONZ_MANUFACTURER,
        model: 'deCONZ',
        macAddress: recordArg.macaddress,
        metadata: { discovery: recordArg },
      },
    };
  }
}

export class DeconzCandidateValidator implements IDiscoveryValidator {
  public id = 'deconz-candidate-validator';
  public description = 'Validate deCONZ candidates before starting local setup.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = [
      candidateArg.integrationDomain,
      candidateArg.manufacturer,
      candidateArg.model,
      candidateArg.name,
      candidateArg.metadata ? JSON.stringify(candidateArg.metadata) : '',
    ].join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === 'deconz' || hasDeconzText(metadata);

    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has deCONZ metadata.' : 'Candidate is not deCONZ.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: normalizeBridgeId(candidateArg.id),
    };
  }
}

export const createDeconzDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({
    integrationDomain: 'deconz',
    displayName: 'deCONZ',
  })
    .addMatcher(new DeconzMdnsMatcher())
    .addMatcher(new DeconzSsdpMatcher())
    .addMatcher(new DeconzManualMatcher())
    .addMatcher(new DeconzPhosconDiscoveryMatcher())
    .addValidator(new DeconzCandidateValidator());
};

export const normalizeBridgeId = (valueArg?: string): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const cleaned = valueArg.replace(/[^a-fA-F0-9]/g, '').toUpperCase();
  return cleaned.length >= 8 ? cleaned : valueArg;
};

const hasDeconzText = (valueArg: string): boolean => {
  return valueArg.includes('deconz')
    || valueArg.includes('phoscon')
    || valueArg.includes('dresden')
    || valueArg.includes('conbee')
    || valueArg.includes('raspbee');
};

const parseEndpoint = (urlArg?: string): { host?: string; port?: number } => {
  if (!urlArg) {
    return {};
  }
  try {
    const parsed = new URL(urlArg);
    return {
      host: parsed.hostname,
      port: parsed.port ? Number(parsed.port) : undefined,
    };
  } catch {
    return {};
  }
};
