import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IForkedDaapdManualEntry, IForkedDaapdMdnsRecord } from './forked_daapd.types.js';
import { forkedDaapdDefaultPort } from './forked_daapd.types.js';

const forkedDaapdDomain = 'forked_daapd';
const forkedDaapdMdnsType = '_daap._tcp';

export class ForkedDaapdMdnsMatcher implements IDiscoveryMatcher<IForkedDaapdMdnsRecord> {
  public id = 'forked-daapd-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize OwnTone zeroconf DAAP advertisements.';

  public async matches(recordArg: IForkedDaapdMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const machineName = stringValue(valueForKey(properties, 'Machine Name')) || cleanName(recordArg.name || recordArg.hostname);
    const mtdVersion = stringValue(valueForKey(properties, 'mtd-version'));
    const versionMajor = majorVersion(mtdVersion);
    const serviceMatch = type === forkedDaapdMdnsType;

    if (!serviceMatch) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a DAAP service advertisement.' };
    }
    if (!machineName) {
      return { matched: false, confidence: 'medium', reason: 'DAAP service lacks OwnTone Machine Name metadata.' };
    }
    if (versionMajor === undefined || versionMajor < 27) {
      return { matched: false, confidence: 'medium', reason: 'DAAP service is not an OwnTone/forked-daapd server with version >= 27.' };
    }

    const host = recordArg.host || recordArg.addresses?.[0];
    const port = recordArg.port || forkedDaapdDefaultPort;
    return {
      matched: true,
      confidence: host ? 'certain' : 'high',
      reason: 'mDNS service is an OwnTone DAAP advertisement with supported mtd-version.',
      normalizedDeviceId: machineName,
      candidate: {
        source: 'mdns',
        integrationDomain: forkedDaapdDomain,
        id: machineName,
        host,
        port,
        name: machineName,
        manufacturer: 'OwnTone',
        model: 'OwnTone Server',
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: type,
          txt: properties,
          mtdVersion,
          versionMajor,
        },
      },
      metadata: { mdnsType: type, mtdVersion, versionMajor },
    };
  }
}

export class ForkedDaapdManualMatcher implements IDiscoveryMatcher<IForkedDaapdManualEntry> {
  public id = 'forked-daapd-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual OwnTone endpoint setup entries.';

  public async matches(inputArg: IForkedDaapdManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const haystack = `${inputArg.name || ''} ${inputArg.id || ''} ${inputArg.uniqueId || ''}`.toLowerCase();
    const hinted = Boolean(metadata.owntone || metadata.forked_daapd || metadata.forkedDaapd) || haystack.includes('owntone') || haystack.includes('forked_daapd');
    if (!inputArg.host && !hinted) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain an OwnTone host or setup hint.' };
    }

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

export class ForkedDaapdCandidateValidator implements IDiscoveryValidator {
  public id = 'forked-daapd-candidate-validator';
  public description = 'Validate OwnTone candidates have a local API endpoint.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const mdnsType = stringMetadata(metadata.mdnsType);
    const versionMajor = numberMetadata(metadata.versionMajor) ?? majorVersion(stringMetadata(metadata.mtdVersion));
    const haystack = `${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''}`.toLowerCase();
    const matched = candidateArg.integrationDomain === forkedDaapdDomain
      || normalizeMdnsType(mdnsType || '') === forkedDaapdMdnsType
      || Boolean(metadata.owntone || metadata.forked_daapd || metadata.forkedDaapd)
      || haystack.includes('owntone')
      || haystack.includes('forked-daapd')
      || haystack.includes('forked_daapd');

    if (!matched || !candidateArg.host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'OwnTone candidate lacks host information.' : 'Candidate is not OwnTone.',
      };
    }
    if (versionMajor !== undefined && versionMajor < 27) {
      return { matched: false, confidence: 'medium', reason: 'OwnTone candidate version is below 27.' };
    }

    const port = candidateArg.port || forkedDaapdDefaultPort;
    return {
      matched: true,
      confidence: candidateArg.id ? 'certain' : 'high',
      reason: 'Candidate has OwnTone metadata and host information.',
      normalizedDeviceId: candidateArg.id || `${candidateArg.host}:${port}`,
      candidate: {
        ...candidateArg,
        integrationDomain: forkedDaapdDomain,
        port,
        manufacturer: candidateArg.manufacturer || 'OwnTone',
        model: candidateArg.model || 'OwnTone Server',
      },
    };
  }
}

export const createForkedDaapdDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: forkedDaapdDomain, displayName: 'OwnTone' })
    .addMatcher(new ForkedDaapdMdnsMatcher())
    .addMatcher(new ForkedDaapdManualMatcher())
    .addValidator(new ForkedDaapdCandidateValidator());
};

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

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

const valueForKey = (recordArg: Record<string, unknown> | undefined, keyArg: string): unknown => {
  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 stringValue = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'string' && valueArg.trim()) {
    return valueArg.trim();
  }
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return String(valueArg);
  }
  if (valueArg instanceof Uint8Array) {
    return Buffer.from(valueArg).toString('utf8');
  }
  return undefined;
};

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

const numberMetadata = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;

const majorVersion = (valueArg: string | undefined): number | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const parsed = Number.parseInt(valueArg.split('.')[0], 10);
  return Number.isFinite(parsed) ? parsed : undefined;
};
