import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IDunehdManualEntry, IDunehdMdnsRecord, IDunehdSsdpRecord } from './dunehd.types.js';
import { dunehdDefaultPort } from './dunehd.types.js';

export class DunehdSsdpMatcher implements IDiscoveryMatcher<IDunehdSsdpRecord> {
  public id = 'dunehd-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize local Dune HD UPnP/SSDP advertisements.';

  public async matches(recordArg: IDunehdSsdpRecord): Promise<IDiscoveryMatch> {
    const st = stringValue(recordArg, 'st', 'ST', 'ssdp_st') || recordArg.ssdp_st;
    const usn = stringValue(recordArg, 'usn', 'USN', 'udn', 'UDN', 'ssdp_usn') || recordArg.ssdp_usn;
    const location = stringValue(recordArg, 'location', 'LOCATION', 'ssdp_location') || recordArg.ssdp_location;
    const manufacturer = stringValue(recordArg, 'manufacturer', 'MANUFACTURER', 'upnp:manufacturer');
    const model = stringValue(recordArg, 'modelName', 'model_name', 'model', 'upnp:modelName');
    const name = stringValue(recordArg, 'friendlyName', 'friendly_name', 'upnp:friendlyName', 'name');
    const matched = isDuneHint(st, usn, manufacturer, model, name) || Boolean(recordArg.upnp?.dunehd);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record does not contain Dune HD hints.' };
    }

    const url = safeUrl(location);
    const id = stripUuid(usn || stringValue(recordArg, 'udn', 'UDN')) || stringValue(recordArg, 'serialNumber', 'serial_number', 'macAddress');
    return {
      matched: true,
      confidence: id ? 'certain' : url?.hostname ? 'high' : 'medium',
      reason: 'SSDP record matches Dune HD metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'dunehd',
        id,
        host: url?.hostname,
        port: portFromUrl(url) || dunehdDefaultPort,
        name,
        manufacturer: manufacturer || 'Dune',
        model,
        metadata: { st, usn, location },
      },
    };
  }
}

export class DunehdMdnsMatcher implements IDiscoveryMatcher<IDunehdMdnsRecord> {
  public id = 'dunehd-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Dune HD mDNS setup hints.';

  public async matches(recordArg: IDunehdMdnsRecord): Promise<IDiscoveryMatch> {
    const properties = { ...(recordArg.txt || {}), ...(recordArg.properties || {}) };
    const type = normalizeType(recordArg.type);
    const manufacturer = valueForKey(properties, 'manufacturer') || valueForKey(properties, 'mf');
    const model = valueForKey(properties, 'model') || valueForKey(properties, 'md') || valueForKey(properties, 'modelName');
    const name = cleanName(valueForKey(properties, 'name') || valueForKey(properties, 'fn') || recordArg.name);
    const matched = type.includes('dune') || isDuneHint(manufacturer, model, name) || Boolean(valueForKey(properties, 'dunehd'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not contain Dune HD hints.' };
    }

    const id = valueForKey(properties, 'id') || valueForKey(properties, 'serial') || valueForKey(properties, 'mac') || name;
    return {
      matched: true,
      confidence: id ? 'certain' : recordArg.host ? 'high' : 'medium',
      reason: 'mDNS record matches Dune HD metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'dunehd',
        id,
        host: recordArg.host,
        port: recordArg.port || dunehdDefaultPort,
        name,
        manufacturer: manufacturer || 'Dune',
        model,
        macAddress: valueForKey(properties, 'mac') || valueForKey(properties, 'macaddress'),
        metadata: { mdnsType: recordArg.type, mdnsName: recordArg.name, txt: properties },
      },
    };
  }
}

export class DunehdManualMatcher implements IDiscoveryMatcher<IDunehdManualEntry> {
  public id = 'dunehd-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Dune HD setup entries.';

  public async matches(inputArg: IDunehdManualEntry): Promise<IDiscoveryMatch> {
    const matched = Boolean(inputArg.host || inputArg.metadata?.dunehd || isDuneHint(inputArg.manufacturer, inputArg.model, inputArg.name));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Dune HD setup hints.' };
    }
    const id = inputArg.id || inputArg.serialNumber || inputArg.macAddress;
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Dune HD setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'dunehd',
        id,
        host: inputArg.host,
        port: inputArg.port || dunehdDefaultPort,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Dune',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        macAddress: inputArg.macAddress,
        metadata: inputArg.metadata,
      },
    };
  }
}

export class DunehdCandidateValidator implements IDiscoveryValidator {
  public id = 'dunehd-candidate-validator';
  public description = 'Validate Dune HD discovery candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const matched = candidateArg.integrationDomain === 'dunehd'
      || Boolean(candidateArg.metadata?.dunehd)
      || isDuneHint(candidateArg.manufacturer, candidateArg.model, candidateArg.name);
    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Dune HD metadata.' : 'Candidate is not Dune HD.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.id || candidateArg.macAddress || candidateArg.serialNumber || candidateArg.host,
    };
  }
}

export const createDunehdDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'dunehd', displayName: 'Dune HD' })
    .addMatcher(new DunehdSsdpMatcher())
    .addMatcher(new DunehdMdnsMatcher())
    .addMatcher(new DunehdManualMatcher())
    .addValidator(new DunehdCandidateValidator());
};

const isDuneHint = (...valuesArg: Array<string | undefined>): boolean => {
  const haystack = valuesArg.filter(Boolean).join(' ').toLowerCase();
  return haystack.includes('dune hd') || haystack.includes('dunehd') || haystack.includes('dune');
};

const normalizeType = (valueArg?: string): string => (valueArg || '').toLowerCase().replace(/\.$/, '');

const cleanName = (valueArg?: string): string | undefined => valueArg?.replace(/\._[^.]+\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;

const stripUuid = (valueArg?: string): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/^uuid:/i, '').split('::')[0];
};

const safeUrl = (valueArg?: string): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const portFromUrl = (urlArg: URL | undefined): number | undefined => {
  if (!urlArg?.port) {
    return undefined;
  }
  const port = Number(urlArg.port);
  return Number.isFinite(port) ? port : undefined;
};

const valueForKey = (recordArg: Record<string, string | undefined> | undefined, keyArg: string): string | undefined => {
  if (!recordArg) {
    return undefined;
  }
  const lowerKey = keyArg.toLowerCase();
  for (const [key, value] of Object.entries(recordArg)) {
    if (key.toLowerCase() === lowerKey && value) {
      return value;
    }
  }
  return undefined;
};

const stringValue = (recordArg: IDunehdSsdpRecord, ...keysArg: string[]): string | undefined => {
  const maps = [recordArg.headers, recordArg.upnp, recordArg as Record<string, unknown>].filter(Boolean) as Array<Record<string, unknown>>;
  for (const key of keysArg) {
    const lowerKey = key.toLowerCase();
    for (const map of maps) {
      for (const [candidateKey, value] of Object.entries(map)) {
        if (candidateKey.toLowerCase() === lowerKey && typeof value === 'string' && value) {
          return value;
        }
      }
    }
  }
  return undefined;
};
