import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IBluesoundManualEntry, IBluesoundMdnsRecord } from './bluesound.types.js';
import { bluesoundDefaultPort } from './bluesound.types.js';

const bluesoundDomain = 'bluesound';
const bluesoundMdnsType = '_musc._tcp';

export class BluesoundMdnsMatcher implements IDiscoveryMatcher<IBluesoundMdnsRecord> {
  public id = 'bluesound-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Bluesound/BluOS mDNS advertisements.';

  public async matches(recordArg: IBluesoundMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const txt = { ...recordArg.txt, ...recordArg.properties };
    const name = cleanName(recordArg.name || recordArg.hostname || valueForKey(txt, 'name'));
    const manufacturer = valueForKey(txt, 'brand') || valueForKey(txt, 'manufacturer');
    const model = valueForKey(txt, 'modelName') || valueForKey(txt, 'model');
    const haystack = `${name || ''} ${type} ${manufacturer || ''} ${model || ''}`.toLowerCase();
    const matched = type === bluesoundMdnsType || haystack.includes('bluesound') || haystack.includes('bluos');

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

    const host = recordArg.host || recordArg.addresses?.[0];
    const port = recordArg.port || numberString(valueForKey(txt, 'port')) || bluesoundDefaultPort;
    const macAddress = valueForKey(txt, 'mac') || valueForKey(txt, 'macAddress');
    const id = macAddress || valueForKey(txt, 'id') || valueForKey(txt, 'uuid') || (host ? `${host}:${port}` : name);
    return {
      matched: true,
      confidence: type === bluesoundMdnsType && host ? 'certain' : host ? 'high' : 'medium',
      reason: type === bluesoundMdnsType ? 'mDNS service type matches Home Assistant Bluesound zeroconf.' : 'mDNS metadata contains Bluesound/BluOS hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: bluesoundDomain,
        id,
        host,
        port,
        name,
        manufacturer: manufacturer || 'Bluesound',
        model,
        macAddress,
        metadata: {
          mdnsType: type,
          txt,
        },
      },
      metadata: { mdnsType: type },
    };
  }
}

export class BluesoundManualMatcher implements IDiscoveryMatcher<IBluesoundManualEntry> {
  public id = 'bluesound-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Bluesound/BluOS setup entries.';

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

    const port = inputArg.port || bluesoundDefaultPort;
    const id = inputArg.macAddress || inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Bluesound setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: bluesoundDomain,
        id,
        host: inputArg.host,
        port,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Bluesound',
        model: inputArg.modelName || inputArg.model,
        macAddress: inputArg.macAddress,
        metadata: inputArg.metadata,
      },
    };
  }
}

export class BluesoundCandidateValidator implements IDiscoveryValidator {
  public id = 'bluesound-candidate-validator';
  public description = 'Validate Bluesound/BluOS discovery candidates.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const mdnsType = typeof metadata.mdnsType === 'string' ? normalizeMdnsType(metadata.mdnsType) : '';
    const haystack = `${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''}`.toLowerCase();
    const matched = candidateArg.integrationDomain === bluesoundDomain
      || mdnsType === bluesoundMdnsType
      || Boolean(metadata.bluesound || metadata.bluos)
      || haystack.includes('bluesound')
      || haystack.includes('bluos');

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

export const createBluesoundDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: bluesoundDomain, displayName: 'Bluesound' })
    .addMatcher(new BluesoundMdnsMatcher())
    .addMatcher(new BluesoundManualMatcher())
    .addValidator(new BluesoundCandidateValidator());
};

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

const cleanName = (valueArg: string | undefined): string | undefined => {
  return valueArg
    ?.replace(/\._musc\._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;
};
