import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IAirgradientManualEntry, IAirgradientMdnsRecord } from './airgradient.types.js';
import { airgradientDefaultPort, airgradientMdnsType, airgradientMinFirmwareVersion } from './airgradient.types.js';

const airgradientDomain = 'airgradient';

export class AirgradientMdnsMatcher implements IDiscoveryMatcher<IAirgradientMdnsRecord> {
  public id = 'airgradient-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize AirGradient zeroconf advertisements.';

  public async matches(recordArg: IAirgradientMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const properties = normalizeKeys({ ...recordArg.txt, ...recordArg.properties });
    const type = normalizeType(recordArg.type || recordArg.serviceType);
    const serialNumber = textValue(properties.serialno || properties.serial || properties.serial_number);
    const model = textValue(properties.model);
    const firmwareVersion = textValue(properties.fw_ver || properties.firmware || properties.version);
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const matched = type === airgradientMdnsType || Boolean(serialNumber && model) || includesAirgradient(recordArg.name) || includesAirgradient(model);

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an AirGradient advertisement.' };
    }

    const firmwareSupported = firmwareVersion ? versionAtLeast(firmwareVersion, airgradientMinFirmwareVersion) : undefined;
    return {
      matched: true,
      confidence: serialNumber && host ? 'certain' : host ? 'high' : 'medium',
      reason: 'mDNS record matches AirGradient zeroconf metadata.',
      normalizedDeviceId: serialNumber || recordArg.name || host,
      candidate: {
        source: 'mdns',
        integrationDomain: airgradientDomain,
        id: serialNumber || recordArg.name || host,
        host,
        port: recordArg.port || airgradientDefaultPort,
        name: cleanServiceName(recordArg.name) || model || 'AirGradient',
        manufacturer: 'AirGradient',
        model,
        serialNumber,
        metadata: {
          discoveryProtocol: 'zeroconf',
          mdnsName: recordArg.name,
          mdnsType: type,
          txt: properties,
          firmwareVersion,
          firmwareSupported,
        },
      },
      metadata: { firmwareVersion, firmwareSupported },
    };
  }
}

export class AirgradientManualMatcher implements IDiscoveryMatcher<IAirgradientManualEntry> {
  public id = 'airgradient-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual AirGradient local HTTP setup entries.';

  public async matches(inputArg: IAirgradientManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, inputArg.measures?.model].filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.snapshot || inputArg.measures || inputArg.deviceConfig || inputArg.metadata?.airgradient || text.includes('airgradient'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain AirGradient setup hints.' };
    }
    const id = inputArg.serialNumber || inputArg.id || inputArg.measures?.serialNumber || inputArg.host;
    return {
      matched: true,
      confidence: inputArg.host && id ? 'high' : inputArg.host ? 'medium' : 'low',
      reason: 'Manual entry can start AirGradient setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: airgradientDomain,
        id,
        host: inputArg.host,
        port: inputArg.port || airgradientDefaultPort,
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'AirGradient',
        model: inputArg.model || inputArg.measures?.model,
        serialNumber: inputArg.serialNumber || inputArg.measures?.serialNumber,
        metadata: {
          ...inputArg.metadata,
          discoveryProtocol: 'manual',
          protocol: inputArg.protocol,
          firmwareVersion: inputArg.firmwareVersion || inputArg.measures?.firmwareVersion,
          measures: inputArg.measures,
          deviceConfig: inputArg.deviceConfig,
          snapshot: inputArg.snapshot,
        },
      },
    };
  }
}

export class AirgradientCandidateValidator implements IDiscoveryValidator {
  public id = 'airgradient-candidate-validator';
  public description = 'Validate AirGradient candidates from zeroconf and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const protocol = typeof metadata.discoveryProtocol === 'string' ? metadata.discoveryProtocol : undefined;
    const firmwareVersion = typeof metadata.firmwareVersion === 'string' ? metadata.firmwareVersion : undefined;
    if (firmwareVersion && !versionAtLeast(firmwareVersion, airgradientMinFirmwareVersion)) {
      return {
        matched: false,
        confidence: 'low',
        reason: `AirGradient firmware ${firmwareVersion} is below the supported minimum ${airgradientMinFirmwareVersion}.`,
        normalizedDeviceId: candidateArg.serialNumber || candidateArg.id || candidateArg.host,
        metadata: { firmwareVersion, firmwareSupported: false },
      };
    }

    const text = [candidateArg.name, candidateArg.manufacturer, candidateArg.model].filter(Boolean).join(' ').toLowerCase();
    const mdnsType = typeof metadata.mdnsType === 'string' ? metadata.mdnsType.toLowerCase() : '';
    const matched = candidateArg.integrationDomain === airgradientDomain
      || protocol === 'zeroconf'
      || protocol === 'manual'
      || mdnsType === airgradientMdnsType
      || text.includes('airgradient')
      || metadata.airgradient === true
      || (candidateArg.source === 'manual' && Boolean(candidateArg.host));

    return {
      matched,
      confidence: matched && candidateArg.serialNumber && candidateArg.host ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has AirGradient metadata or manual host information.' : 'Candidate is not AirGradient.',
      candidate: matched ? {
        ...candidateArg,
        integrationDomain: airgradientDomain,
        manufacturer: candidateArg.manufacturer || 'AirGradient',
        port: candidateArg.port || airgradientDefaultPort,
      } : undefined,
      normalizedDeviceId: candidateArg.serialNumber || candidateArg.id || candidateArg.host,
      metadata: matched ? { discoveryProtocol: protocol, firmwareVersion, firmwareSupported: firmwareVersion ? true : undefined } : undefined,
    };
  }
}

export const createAirgradientDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: airgradientDomain, displayName: 'AirGradient' })
    .addMatcher(new AirgradientMdnsMatcher())
    .addMatcher(new AirgradientManualMatcher())
    .addValidator(new AirgradientCandidateValidator());
};

export const versionAtLeast = (versionArg: string, minimumArg: string): boolean => {
  const left = versionParts(versionArg);
  const right = versionParts(minimumArg);
  for (let index = 0; index < Math.max(left.length, right.length); index++) {
    const leftPart = left[index] || 0;
    const rightPart = right[index] || 0;
    if (leftPart > rightPart) {
      return true;
    }
    if (leftPart < rightPart) {
      return false;
    }
  }
  return true;
};

const versionParts = (valueArg: string): number[] => valueArg.split(/[.-]/).map((partArg) => Number.parseInt(partArg.replace(/[^0-9]/g, '') || '0', 10));

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

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

const textValue = (valueArg?: string): string | undefined => valueArg?.trim() || undefined;

const cleanServiceName = (valueArg?: string): string | undefined => valueArg?.replace(/\._airgradient\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;

const includesAirgradient = (valueArg?: string): boolean => Boolean(valueArg?.toLowerCase().includes('airgradient'));
