import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IHeosManualEntry, IHeosMdnsRecord, IHeosSsdpRecord } from './heos.types.js';
import { heosDefaultPort } from './heos.types.js';

const heosDomain = 'heos';
const heosSsdpSt = 'urn:schemas-denon-com:device:ACT-Denon:1';
const heosMdnsType = '_heos-audio._tcp.local.';

export class HeosSsdpMatcher implements IDiscoveryMatcher<IHeosSsdpRecord> {
  public id = 'heos-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Denon HEOS SSDP advertisements.';

  public async matches(recordArg: IHeosSsdpRecord): Promise<IDiscoveryMatch> {
    const st = header(recordArg, 'st') || upnp(recordArg, 'deviceType');
    const usn = header(recordArg, 'usn');
    const location = header(recordArg, 'location');
    const manufacturer = upnp(recordArg, 'manufacturer');
    const model = upnp(recordArg, 'modelName') || upnp(recordArg, 'model');
    const serialNumber = upnp(recordArg, 'serialNumber') || upnp(recordArg, 'serial');
    const friendlyName = upnp(recordArg, 'friendlyName');
    const haystack = `${st || ''} ${usn || ''} ${manufacturer || ''} ${model || ''} ${friendlyName || ''}`.toLowerCase();
    const matched = normalizeUrn(st) === normalizeUrn(heosSsdpSt)
      || haystack.includes('act-denon')
      || haystack.includes('heos');

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not a HEOS advertisement.' };
    }

    const url = parseUrl(location);
    const id = serialNumber || stripUuid(usn) || model;
    return {
      matched: true,
      confidence: normalizeUrn(st) === normalizeUrn(heosSsdpSt) && url?.hostname ? 'certain' : url?.hostname ? 'high' : 'medium',
      reason: 'SSDP record matches Denon HEOS metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: heosDomain,
        id,
        host: url?.hostname,
        port: heosDefaultPort,
        name: friendlyName,
        manufacturer: normalizedManufacturer(manufacturer),
        model,
        serialNumber,
        metadata: { st, usn, location, deviceType: upnp(recordArg, 'deviceType') },
      },
    };
  }
}

export class HeosMdnsMatcher implements IDiscoveryMatcher<IHeosMdnsRecord> {
  public id = 'heos-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize HEOS zeroconf advertisements.';

  public async matches(recordArg: IHeosMdnsRecord): Promise<IDiscoveryMatch> {
    const type = recordArg.type || '';
    const txt = recordArg.txt || {};
    const model = valueForKey(txt, 'model') || valueForKey(txt, 'modelName');
    const serialNumber = valueForKey(txt, 'serial') || valueForKey(txt, 'serialNumber');
    const manufacturer = valueForKey(txt, 'manufacturer') || valueForKey(txt, 'brand');
    const haystack = `${recordArg.name || ''} ${type} ${manufacturer || ''} ${model || ''}`.toLowerCase();
    const matched = normalizeMdnsType(type) === normalizeMdnsType(heosMdnsType) || haystack.includes('heos');

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a HEOS advertisement.' };
    }

    const id = serialNumber || valueForKey(txt, 'id') || recordArg.name;
    return {
      matched: true,
      confidence: recordArg.host ? 'certain' : 'high',
      reason: 'mDNS record matches HEOS zeroconf metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: heosDomain,
        id,
        host: recordArg.host,
        port: heosDefaultPort,
        name: recordArg.name,
        manufacturer: normalizedManufacturer(manufacturer),
        model,
        serialNumber,
        metadata: { mdnsName: recordArg.name, mdnsType: type, advertisedPort: recordArg.port, txt },
      },
    };
  }
}

export class HeosManualMatcher implements IDiscoveryMatcher<IHeosManualEntry> {
  public id = 'heos-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual HEOS setup entries.';

  public async matches(inputArg: IHeosManualEntry): Promise<IDiscoveryMatch> {
    const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.metadata?.heos || haystack.includes('heos'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain HEOS setup hints.' };
    }

    const id = inputArg.serialNumber || inputArg.id || (inputArg.host ? `${inputArg.host}:${inputArg.port || heosDefaultPort}` : undefined);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start HEOS setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: heosDomain,
        id,
        host: inputArg.host,
        port: inputArg.port || heosDefaultPort,
        name: inputArg.name,
        manufacturer: normalizedManufacturer(inputArg.manufacturer),
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        metadata: { ...inputArg.metadata, username: inputArg.username ? true : undefined, password: inputArg.password ? true : undefined },
      },
    };
  }
}

export class HeosCandidateValidator implements IDiscoveryValidator {
  public id = 'heos-candidate-validator';
  public description = 'Validate HEOS candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const matched = candidateArg.integrationDomain === heosDomain
      || includesHeos(candidateArg.name)
      || includesHeos(candidateArg.model)
      || includesHeos(candidateArg.manufacturer)
      || Boolean(metadata.heos)
      || normalizeUrn(stringMetadata(metadata.st)) === normalizeUrn(heosSsdpSt)
      || normalizeMdnsType(stringMetadata(metadata.mdnsType) || '') === normalizeMdnsType(heosMdnsType);

    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has HEOS metadata.' : 'Candidate is not HEOS.',
      candidate: matched ? { ...candidateArg, port: candidateArg.port || heosDefaultPort } : undefined,
      normalizedDeviceId: candidateArg.serialNumber || candidateArg.id,
    };
  }
}

export const createHeosDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: heosDomain, displayName: 'Denon HEOS' })
    .addMatcher(new HeosSsdpMatcher())
    .addMatcher(new HeosMdnsMatcher())
    .addMatcher(new HeosManualMatcher())
    .addValidator(new HeosCandidateValidator());
};

const header = (recordArg: IHeosSsdpRecord, keyArg: string): string | undefined => {
  return recordArg[keyArg as keyof IHeosSsdpRecord] as string | undefined || valueForKey(recordArg.headers, keyArg);
};

const upnp = (recordArg: IHeosSsdpRecord, keyArg: string): string | undefined => {
  return valueForKey(recordArg.upnp, keyArg) || valueForKey(recordArg.headers, keyArg);
};

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) {
      return value;
    }
  }
  return undefined;
};

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

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

const normalizeUrn = (valueArg: string | undefined): string => {
  return (valueArg || '').toLowerCase();
};

const normalizeMdnsType = (valueArg: string): string => {
  return valueArg.toLowerCase().replace(/\.$/, '');
};

const normalizedManufacturer = (valueArg: string | undefined): string => {
  if (!valueArg) {
    return 'Denon';
  }
  if (valueArg.toLowerCase().includes('marantz')) {
    return 'Marantz';
  }
  return valueArg.toLowerCase().includes('denon') ? 'Denon' : valueArg;
};

const includesHeos = (valueArg: string | undefined): boolean => {
  return Boolean(valueArg?.toLowerCase().includes('heos'));
};

const stringMetadata = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' ? valueArg : undefined;
};
