import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { harmonyDisplayName, harmonyDomain, harmonyHubPort, harmonyManufacturer, harmonySsdpDeviceType } from './harmony.constants.js';
import type { IHarmonyManualEntry, IHarmonySsdpRecord } from './harmony.types.js';

export class HarmonySsdpMatcher implements IDiscoveryMatcher<IHarmonySsdpRecord> {
  public id = 'harmony-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Logitech Harmony Hub SSDP advertisements.';

  public async matches(recordArg: IHarmonySsdpRecord): Promise<IDiscoveryMatch> {
    const upnp = recordArg.upnp || {};
    const metadata = recordArg.metadata || {};
    const deviceType = this.stringValue(recordArg.deviceType || recordArg.device_type || recordArg.st || recordArg.nt || upnp.deviceType || metadata.deviceType);
    const manufacturer = this.stringValue(recordArg.manufacturer || upnp.manufacturer || metadata.manufacturer);
    const name = this.stringValue(recordArg.friendlyName || recordArg.friendly_name || upnp.friendlyName || upnp.friendly_name || upnp['upnp:friendlyName'] || metadata.friendlyName || recordArg.name);
    const model = this.stringValue(recordArg.modelName || recordArg.model_name || upnp.modelName || upnp.model_name || metadata.modelName);
    const matchedByManifest = normalize(deviceType) === normalize(harmonySsdpDeviceType) && normalize(manufacturer) === normalize(harmonyManufacturer);
    const matchedByHarmonyHint = normalize(manufacturer) === normalize(harmonyManufacturer) && (normalize(name).includes('harmony') || normalize(model).includes('harmony') || normalize(deviceType).includes('harmony'));

    if (!matchedByManifest && !matchedByHarmonyHint) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not a Logitech Harmony Hub advertisement.' };
    }

    const location = this.stringValue(recordArg.ssdp_location || recordArg.location || metadata.ssdpLocation || metadata.location);
    const locationUrl = this.parseUrl(location);
    const host = this.stringValue(recordArg.host || metadata.host) || locationUrl?.hostname;
    const udn = this.stringValue(recordArg.udn || recordArg.usn || upnp.udn || metadata.udn || metadata.usn);
    const remoteId = this.stringValue(metadata.remoteId || metadata.hubId || metadata.uniqueId || recordArg.serialNumber || upnp.serialNumber);
    const id = remoteId || udn || host;

    return {
      matched: true,
      confidence: matchedByManifest && host ? 'certain' : host ? 'high' : 'medium',
      reason: matchedByManifest ? 'SSDP record matches Home Assistant Harmony manifest metadata.' : 'SSDP record has Logitech Harmony Hub hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: harmonyDomain,
        id,
        host,
        port: harmonyHubPort,
        name: name || harmonyDisplayName,
        manufacturer: harmonyManufacturer,
        model: model || 'Harmony Hub',
        serialNumber: recordArg.serialNumber,
        metadata: {
          ssdpDeviceType: deviceType,
          ssdpLocation: location,
          ssdpLocationPort: locationUrl ? Number(locationUrl.port) || undefined : undefined,
          usn: recordArg.usn,
          udn,
          remoteId: remoteId || undefined,
          upnp,
        },
      },
    };
  }

  private parseUrl(valueArg: string): URL | undefined {
    if (!valueArg) {
      return undefined;
    }
    try {
      return new URL(valueArg);
    } catch {
      return undefined;
    }
  }

  private stringValue(valueArg: unknown): string {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : '';
  }
}

export class HarmonyManualMatcher implements IDiscoveryMatcher<IHarmonyManualEntry> {
  public id = 'harmony-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Harmony Hub host entries.';

  public async matches(inputArg: IHarmonyManualEntry): Promise<IDiscoveryMatch> {
    const manufacturer = normalize(inputArg.manufacturer);
    const model = normalize(inputArg.model);
    const name = normalize(inputArg.name || inputArg.deviceName);
    const matched = Boolean(inputArg.host || inputArg.metadata?.harmony || inputArg.metadata?.harmonyHub || manufacturer === normalize(harmonyManufacturer) && (model.includes('harmony') || name.includes('harmony')));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Harmony Hub setup hints.' };
    }
    const remoteId = this.stringValue(inputArg.remoteId || inputArg.hubId || inputArg.metadata?.remoteId || inputArg.metadata?.hubId);
    const id = inputArg.id || remoteId || inputArg.host;
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start local Harmony Hub setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: harmonyDomain,
        id,
        host: inputArg.host,
        port: inputArg.port || harmonyHubPort,
        name: inputArg.deviceName || inputArg.name || harmonyDisplayName,
        manufacturer: inputArg.manufacturer || harmonyManufacturer,
        model: inputArg.model || 'Harmony Hub',
        metadata: {
          ...inputArg.metadata,
          remoteId: remoteId || undefined,
        },
      },
    };
  }

  private stringValue(valueArg: unknown): string {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : '';
  }
}

export class HarmonyCandidateValidator implements IDiscoveryValidator {
  public id = 'harmony-candidate-validator';
  public description = 'Validate Harmony Hub candidates before local setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const manufacturer = normalize(candidateArg.manufacturer);
    const model = normalize(candidateArg.model);
    const name = normalize(candidateArg.name);
    const matched = candidateArg.integrationDomain === harmonyDomain
      || Boolean(candidateArg.metadata?.harmony || candidateArg.metadata?.harmonyHub)
      || manufacturer === normalize(harmonyManufacturer) && (model.includes('harmony') || name.includes('harmony'));
    const hasUsableAddress = Boolean(candidateArg.host && (!candidateArg.port || isValidPort(candidateArg.port)));
    return {
      matched: matched && hasUsableAddress,
      confidence: matched && candidateArg.id && hasUsableAddress ? 'certain' : matched && hasUsableAddress ? 'high' : matched ? 'medium' : 'low',
      reason: matched
        ? hasUsableAddress ? 'Candidate has Harmony Hub metadata and a usable local address.' : 'Candidate has Harmony Hub metadata but no usable local address.'
        : 'Candidate is not a Harmony Hub.',
      candidate: matched && hasUsableAddress ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.id || candidateArg.host,
      metadata: matched ? { port: candidateArg.port || harmonyHubPort } : undefined,
    };
  }
}

export const createHarmonyDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: harmonyDomain, displayName: harmonyDisplayName })
    .addMatcher(new HarmonySsdpMatcher())
    .addMatcher(new HarmonyManualMatcher())
    .addValidator(new HarmonyCandidateValidator());
};

const normalize = (valueArg?: string): string => {
  return (valueArg || '').toLowerCase().trim().replace(/\.$/, '');
};

const isValidPort = (valueArg: number): boolean => {
  return Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
};
