import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { ILinkplayManualEntry, ILinkplayMdnsRecord, ILinkplaySsdpRecord, TLinkplayProtocol } from './linkplay.types.js';
import { linkplayDefaultPort, linkplayProjectLookup } from './linkplay.types.js';

const linkplayDomain = 'linkplay';
const linkplayMdnsType = '_linkplay._tcp';
const mediaRendererUrn = 'urn:schemas-upnp-org:device:MediaRenderer:1';
const linkplayNames = ['linkplay', 'arylic', 'up2stream', 'wiim', 'muzo', 'audiocast', 'ieast', 'ggmm', 'medion'];

export class LinkplayMdnsMatcher implements IDiscoveryMatcher<ILinkplayMdnsRecord> {
  public id = 'linkplay-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize LinkPlay zeroconf advertisements.';

  public async matches(recordArg: ILinkplayMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const project = valueForKey(properties, 'project') || valueForKey(properties, 'project_build_name');
    const uuid = valueForKey(properties, 'uuid') || valueForKey(properties, 'id') || valueForKey(properties, 'UDN');
    const name = cleanName(valueForKey(properties, 'name') || valueForKey(properties, 'DeviceName') || recordArg.name || recordArg.hostname);
    const projectInfo = project ? linkplayProjectLookup[project] : undefined;
    const haystack = `${name || ''} ${type} ${project || ''} ${projectInfo?.manufacturer || ''} ${projectInfo?.model || ''}`.toLowerCase();
    const serviceMatch = type === linkplayMdnsType;
    const hintMatch = linkplayNames.some((needleArg) => haystack.includes(needleArg));
    if (!serviceMatch && !hintMatch) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a LinkPlay advertisement.' };
    }

    const host = recordArg.host || recordArg.addresses?.[0];
    const port = recordArg.port || linkplayDefaultPort;
    const id = uuid || (host ? `${host}:${port}` : name);
    return {
      matched: true,
      confidence: serviceMatch && uuid ? 'certain' : serviceMatch && host ? 'high' : 'medium',
      reason: serviceMatch ? 'mDNS service is a LinkPlay zeroconf service.' : 'mDNS metadata contains LinkPlay hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: linkplayDomain,
        id,
        host,
        port,
        name,
        manufacturer: projectInfo?.manufacturer || valueForKey(properties, 'manufacturer') || 'LinkPlay',
        model: projectInfo?.model || valueForKey(properties, 'model'),
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: type,
          txt: properties,
          project,
        },
      },
      metadata: { mdnsType: type, project },
    };
  }
}

export class LinkplaySsdpMatcher implements IDiscoveryMatcher<ILinkplaySsdpRecord> {
  public id = 'linkplay-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize LinkPlay UPnP MediaRenderer advertisements with LinkPlay metadata.';

  public async matches(recordArg: ILinkplaySsdpRecord): 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 serialNumber = upnp(recordArg, 'serialNumber') || upnp(recordArg, 'serial');
    const project = upnp(recordArg, 'project') || upnp(recordArg, 'modelNumber');
    const projectInfo = project ? linkplayProjectLookup[project] : undefined;
    const haystack = `${st || ''} ${usn || ''} ${manufacturer || ''} ${model || ''} ${friendlyName || ''} ${project || ''}`.toLowerCase();
    const renderer = normalizeUrn(st) === normalizeUrn(mediaRendererUrn);
    const hintMatch = linkplayNames.some((needleArg) => haystack.includes(needleArg)) || Boolean(projectInfo);
    if (!renderer || !hintMatch) {
      return { matched: false, confidence: renderer ? 'medium' : 'low', reason: renderer ? 'MediaRenderer record lacks LinkPlay metadata.' : 'SSDP record is not a LinkPlay MediaRenderer.' };
    }

    const url = parseUrl(location);
    const id = serialNumber || stripUuid(usn) || (url?.hostname ? `${url.hostname}:${linkplayDefaultPort}` : undefined);
    return {
      matched: true,
      confidence: url?.hostname && id ? 'high' : 'medium',
      reason: 'SSDP MediaRenderer has LinkPlay-specific metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: linkplayDomain,
        id,
        host: url?.hostname,
        port: linkplayDefaultPort,
        name: friendlyName,
        manufacturer: projectInfo?.manufacturer || manufacturer || 'LinkPlay',
        model: projectInfo?.model || model,
        serialNumber,
        metadata: { st, usn, location, project, deviceType: upnp(recordArg, 'deviceType') },
      },
    };
  }
}

export class LinkplayManualMatcher implements IDiscoveryMatcher<ILinkplayManualEntry> {
  public id = 'linkplay-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual LinkPlay setup entries.';

  public async matches(inputArg: ILinkplayManualEntry): Promise<IDiscoveryMatch> {
    const projectInfo = inputArg.project ? linkplayProjectLookup[inputArg.project] : undefined;
    const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''} ${inputArg.project || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.metadata?.linkplay || projectInfo || linkplayNames.some((needleArg) => haystack.includes(needleArg)));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain LinkPlay setup hints.' };
    }

    const protocol = inputArg.protocol || protocolFromPort(inputArg.port);
    const port = inputArg.port || linkplayDefaultPort;
    const id = inputArg.uuid || inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start LinkPlay setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: linkplayDomain,
        id,
        host: inputArg.host,
        port,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || projectInfo?.manufacturer || 'LinkPlay',
        model: inputArg.model || projectInfo?.model,
        macAddress: inputArg.macAddress,
        metadata: {
          ...inputArg.metadata,
          protocol,
          project: inputArg.project,
          modelId: inputArg.modelId,
        },
      },
    };
  }
}

export class LinkplayCandidateValidator implements IDiscoveryValidator {
  public id = 'linkplay-candidate-validator';
  public description = 'Validate LinkPlay candidates have local endpoint metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const project = stringMetadata(metadata.project);
    const mdnsType = stringMetadata(metadata.mdnsType);
    const haystack = `${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''} ${project || ''}`.toLowerCase();
    const matched = candidateArg.integrationDomain === linkplayDomain
      || Boolean(metadata.linkplay)
      || normalizeMdnsType(mdnsType || '') === linkplayMdnsType
      || Boolean(project && linkplayProjectLookup[project])
      || linkplayNames.some((needleArg) => haystack.includes(needleArg));

    if (!matched || !candidateArg.host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'LinkPlay candidate lacks host information.' : 'Candidate is not LinkPlay.',
      };
    }

    const protocol = stringMetadata(metadata.protocol) === 'https' ? 'https' : protocolFromPort(candidateArg.port);
    const port = candidateArg.port || linkplayDefaultPort;
    return {
      matched: true,
      confidence: candidateArg.id ? 'certain' : 'high',
      reason: 'Candidate has LinkPlay metadata and host information.',
      normalizedDeviceId: candidateArg.id || `${candidateArg.host}:${port}`,
      candidate: {
        ...candidateArg,
        port,
        metadata: {
          ...metadata,
          protocol,
        },
      },
    };
  }
}

export const createLinkplayDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: linkplayDomain, displayName: 'LinkPlay' })
    .addMatcher(new LinkplayMdnsMatcher())
    .addMatcher(new LinkplaySsdpMatcher())
    .addMatcher(new LinkplayManualMatcher())
    .addValidator(new LinkplayCandidateValidator());
};

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

const cleanName = (valueArg: string | undefined): string | undefined => {
  return valueArg?.replace(/\._linkplay\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || 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) {
      return value;
    }
  }
  return undefined;
};

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

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

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 => valueArg?.replace(/^uuid:/i, '').split('::')[0];

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

const protocolFromPort = (portArg: number | undefined): TLinkplayProtocol => portArg === 443 || portArg === 4443 ? 'https' : 'http';

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