import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { ISqueezeboxDhcpRecord, ISqueezeboxManualEntry, ISqueezeboxMdnsRecord } from './squeezebox.types.js';
import { squeezeboxDefaultHttpPort } from './squeezebox.types.js';

const squeezeboxDomain = 'squeezebox';
const lmsNames = ['squeezebox', 'lyrion', 'logitech media server', 'lms', 'slimserver'];
const lmsMdnsTypes = new Set([
  '_squeezebox._tcp',
  '_squeezebox-jsonrpc._tcp',
  '_squeezebox-server._tcp',
  '_lms._tcp',
  '_slimserver._tcp',
]);

export class SqueezeboxMdnsMatcher implements IDiscoveryMatcher<ISqueezeboxMdnsRecord> {
  public id = 'squeezebox-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Lyrion/Logitech Media Server mDNS advertisements.';

  public async matches(recordArg: ISqueezeboxMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const name = cleanName(recordArg.name || recordArg.hostname || valueForKey(properties, 'name')) || 'Lyrion Music Server';
    const haystack = `${name} ${type} ${valueForKey(properties, 'model') || ''} ${valueForKey(properties, 'server') || ''}`.toLowerCase();
    const serviceMatch = lmsMdnsTypes.has(type);
    const nameMatch = lmsNames.some((needleArg) => haystack.includes(needleArg));

    if (!serviceMatch && !nameMatch) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an LMS/Squeezebox service.' };
    }

    const host = recordArg.host || recordArg.addresses?.[0];
    const port = recordArg.port || numberString(valueForKey(properties, 'port')) || squeezeboxDefaultHttpPort;
    const id = valueForKey(properties, 'uuid') || valueForKey(properties, 'id') || valueForKey(properties, 'mac') || (host ? `${host}:${port}` : name);
    return {
      matched: true,
      confidence: serviceMatch && host ? 'certain' : serviceMatch ? 'high' : 'medium',
      reason: serviceMatch ? `mDNS service ${type} is an LMS service.` : 'mDNS metadata contains LMS/Squeezebox hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: squeezeboxDomain,
        id,
        host,
        port,
        name,
        manufacturer: 'Lyrion',
        model: 'Lyrion Music Server',
        metadata: {
          mdnsType: type,
          txt: properties,
          uuid: valueForKey(properties, 'uuid'),
        },
      },
      metadata: { mdnsType: type },
    };
  }
}

export class SqueezeboxDhcpMatcher implements IDiscoveryMatcher<ISqueezeboxDhcpRecord> {
  public id = 'squeezebox-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize Squeezebox player DHCP hints that can start LMS setup.';

  public async matches(recordArg: ISqueezeboxDhcpRecord): Promise<IDiscoveryMatch> {
    const hostname = recordArg.hostname || recordArg.name || '';
    const mac = recordArg.macaddress || recordArg.macAddress || '';
    const matched = hostname.toLowerCase().startsWith('squeezebox') || normalizeMac(mac).startsWith('000420');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record is not a known Squeezebox player hint.' };
    }

    const host = recordArg.host || recordArg.ipAddress;
    const id = normalizeMac(mac) || host || hostname;
    return {
      matched: true,
      confidence: normalizeMac(mac).startsWith('000420') ? 'high' : 'medium',
      reason: 'DHCP record matches the Home Assistant Squeezebox player discovery hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: squeezeboxDomain,
        id,
        host,
        port: squeezeboxDefaultHttpPort,
        name: hostname || 'Squeezebox player',
        manufacturer: 'Logitech',
        model: 'Squeezebox Player',
        macAddress: mac || undefined,
        metadata: {
          ...recordArg.metadata,
          playerDiscovery: true,
        },
      },
    };
  }
}

export class SqueezeboxManualMatcher implements IDiscoveryMatcher<ISqueezeboxManualEntry> {
  public id = 'squeezebox-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Lyrion Music Server setup entries.';

  public async matches(inputArg: ISqueezeboxManualEntry): Promise<IDiscoveryMatch> {
    const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.metadata?.squeezebox || inputArg.metadata?.lms || lmsNames.some((needleArg) => haystack.includes(needleArg)));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain LMS/Squeezebox setup hints.' };
    }

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

export class SqueezeboxCandidateValidator implements IDiscoveryValidator {
  public id = 'squeezebox-candidate-validator';
  public description = 'Validate LMS/Squeezebox discovery candidates have local setup metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const haystack = `${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''}`.toLowerCase();
    const mdnsType = typeof metadata.mdnsType === 'string' ? normalizeMdnsType(metadata.mdnsType) : '';
    const matched = candidateArg.integrationDomain === squeezeboxDomain
      || Boolean(metadata.squeezebox || metadata.lms || metadata.playerDiscovery)
      || lmsMdnsTypes.has(mdnsType)
      || lmsNames.some((needleArg) => haystack.includes(needleArg));

    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has LMS/Squeezebox metadata.' : 'Candidate is not LMS/Squeezebox.',
      normalizedDeviceId: candidateArg.id || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || squeezeboxDefaultHttpPort}` : undefined),
      candidate: matched ? { ...candidateArg, port: candidateArg.port || squeezeboxDefaultHttpPort } : undefined,
    };
  }
}

export const createSqueezeboxDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: squeezeboxDomain, displayName: 'Squeezebox (Lyrion Music Server)' })
    .addMatcher(new SqueezeboxMdnsMatcher())
    .addMatcher(new SqueezeboxDhcpMatcher())
    .addMatcher(new SqueezeboxManualMatcher())
    .addValidator(new SqueezeboxCandidateValidator());
};

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

const cleanName = (valueArg: string | undefined): string | undefined => {
  return valueArg
    ?.replace(/\._squeezebox(?:-jsonrpc|-server)?\._tcp\.local\.?$/i, '')
    .replace(/\._lms\._tcp\.local\.?$/i, '')
    .replace(/\._slimserver\._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 numberString = (valueArg: string | undefined): number | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const value = Number(valueArg);
  return Number.isFinite(value) && value > 0 ? Math.round(value) : undefined;
};

const normalizeMac = (valueArg: string | undefined): string => (valueArg || '').toLowerCase().replace(/[^a-f0-9]/g, '');
