import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IArcamFmjManualEntry, IArcamFmjSsdpRecord } from './arcam_fmj.types.js';
import { arcamFmjDefaultPort } from './arcam_fmj.types.js';

const domain = 'arcam_fmj';

export class ArcamFmjSsdpMatcher implements IDiscoveryMatcher<IArcamFmjSsdpRecord> {
  public id = 'arcam-fmj-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Arcam FMJ SSDP media renderer advertisements.';

  public async matches(recordArg: IArcamFmjSsdpRecord): 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 friendlyName = upnp(recordArg, 'friendlyName');
    const udn = upnp(recordArg, 'UDN') || upnp(recordArg, 'udn') || usn;
    const deviceType = upnp(recordArg, 'deviceType') || st;
    const haystack = `${manufacturer || ''} ${model || ''} ${friendlyName || ''} ${deviceType || ''} ${usn || ''}`.toLowerCase();
    const matched = haystack.includes('arcam') || (haystack.includes('mediarenderer') && manufacturer?.toLowerCase() === 'arcam');

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not an Arcam FMJ receiver.' };
    }

    const url = parseUrl(location);
    const id = uniqueIdFromUdn(udn) || stripUuid(usn);
    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'SSDP record matches Arcam FMJ metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: domain,
        id,
        host: url?.hostname,
        port: arcamFmjDefaultPort,
        name: friendlyName,
        manufacturer: manufacturer || 'Arcam',
        model,
        metadata: { st, usn, location, udn, deviceType },
      },
    };
  }
}

export class ArcamFmjManualMatcher implements IDiscoveryMatcher<IArcamFmjManualEntry> {
  public id = 'arcam-fmj-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Arcam FMJ receiver setup entries.';

  public async matches(inputArg: IArcamFmjManualEntry): Promise<IDiscoveryMatch> {
    const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || haystack.includes('arcam') || haystack.includes('fmj') || inputArg.metadata?.arcam_fmj);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Arcam FMJ setup hints.' };
    }
    const port = inputArg.port || arcamFmjDefaultPort;
    const id = inputArg.id || inputArg.serialNumber || (inputArg.host ? `${inputArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Arcam FMJ TCP setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: domain,
        id,
        host: inputArg.host,
        port,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Arcam',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        metadata: inputArg.metadata,
      },
    };
  }
}

export class ArcamFmjCandidateValidator implements IDiscoveryValidator {
  public id = 'arcam-fmj-candidate-validator';
  public description = 'Validate Arcam FMJ candidates have Arcam receiver metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const haystack = `${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''}`.toLowerCase();
    const matched = candidateArg.integrationDomain === domain
      || haystack.includes('arcam')
      || haystack.includes('fmj')
      || Boolean(metadata.arcam_fmj);

    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Arcam FMJ metadata.' : 'Candidate is not an Arcam FMJ receiver.',
      normalizedDeviceId: candidateArg.id || candidateArg.serialNumber || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || arcamFmjDefaultPort}` : undefined),
      candidate: matched ? { ...candidateArg, port: candidateArg.port || arcamFmjDefaultPort } : undefined,
    };
  }
}

export const createArcamFmjDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: domain, displayName: 'Arcam FMJ Receivers' })
    .addMatcher(new ArcamFmjSsdpMatcher())
    .addMatcher(new ArcamFmjManualMatcher())
    .addValidator(new ArcamFmjCandidateValidator());
};

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

const upnp = (recordArg: IArcamFmjSsdpRecord, 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 uniqueIdFromUdn = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const stripped = stripUuid(valueArg);
  const parts = stripped?.split('-') || [];
  return parts[4] || stripped;
};

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