import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IMpdManualEntry, IMpdMdnsRecord } from './mpd.types.js';
import { mpdDefaultPort } from './mpd.types.js';

const mpdMdnsTypes = new Set(['_mpd._tcp', '_mpd._tcp.local']);

export class MpdMdnsMatcher implements IDiscoveryMatcher<IMpdMdnsRecord> {
  public id = 'mpd-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Music Player Daemon mDNS advertisements.';

  public async matches(recordArg: IMpdMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeType(recordArg.type || recordArg.serviceType || '');
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const name = cleanName(recordArg.name || recordArg.hostname || properties.name) || 'Music Player Daemon';
    const serviceMatch = mpdMdnsTypes.has(type);
    const nameMatch = name.toLowerCase().includes('mpd') || name.toLowerCase().includes('music player daemon');
    if (!serviceMatch && !nameMatch) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an MPD service.' };
    }

    const host = recordArg.host || recordArg.addresses?.[0];
    const port = recordArg.port || mpdDefaultPort;
    const id = valueForKey(properties, 'id') || (host ? `${host}:${port}` : name);
    return {
      matched: true,
      confidence: serviceMatch && host ? 'certain' : serviceMatch ? 'high' : 'medium',
      reason: serviceMatch ? `mDNS service ${type} is an MPD service.` : 'mDNS name contains MPD hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'mpd',
        id,
        host,
        port,
        name,
        manufacturer: 'Music Player Daemon',
        model: 'MPD Server',
        metadata: {
          mdnsType: type,
          txt: properties,
        },
      },
      metadata: {
        mdnsType: type,
      },
    };
  }
}

export class MpdManualMatcher implements IDiscoveryMatcher<IMpdManualEntry> {
  public id = 'mpd-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual MPD setup entries.';

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

    const port = inputArg.port || mpdDefaultPort;
    const id = inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start MPD setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'mpd',
        id,
        host: inputArg.host,
        port,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Music Player Daemon',
        model: inputArg.model || 'MPD Server',
        metadata: {
          ...inputArg.metadata,
          password: inputArg.password ? true : undefined,
        },
      },
    };
  }
}

export class MpdCandidateValidator implements IDiscoveryValidator {
  public id = 'mpd-candidate-validator';
  public description = 'Validate MPD candidates have host information and MPD metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
    const model = candidateArg.model?.toLowerCase() || '';
    const name = candidateArg.name?.toLowerCase() || '';
    const mdnsType = typeof candidateArg.metadata?.mdnsType === 'string' ? normalizeType(candidateArg.metadata.mdnsType) : '';
    const matched = candidateArg.integrationDomain === 'mpd'
      || manufacturer.includes('music player daemon')
      || manufacturer.includes('mpd')
      || model.includes('mpd')
      || name.includes('mpd')
      || name.includes('music player daemon')
      || mpdMdnsTypes.has(mdnsType)
      || Boolean(candidateArg.metadata?.mpd);

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

    const port = candidateArg.port || mpdDefaultPort;
    return {
      matched: true,
      confidence: candidateArg.id ? 'certain' : 'high',
      reason: 'Candidate has MPD metadata and host information.',
      normalizedDeviceId: candidateArg.id || `${candidateArg.host}:${port}`,
      candidate: {
        ...candidateArg,
        port,
      },
    };
  }
}

export const createMpdDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'mpd', displayName: 'Music Player Daemon (MPD)' })
    .addMatcher(new MpdMdnsMatcher())
    .addMatcher(new MpdManualMatcher())
    .addValidator(new MpdCandidateValidator());
};

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

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

const cleanName = (valueArg: string | undefined): string | undefined => {
  return valueArg?.replace(/\._mpd\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;
};
