import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { ElgatoMapper } from './elgato.mapper.js';
import type { IElgatoDhcpRecord, IElgatoManualEntry, IElgatoMdnsRecord, IElgatoRawData, IElgatoSnapshot } from './elgato.types.js';
import { elgatoDefaultPort, elgatoDisplayName, elgatoDomain, elgatoManufacturer, elgatoZeroconfType } from './elgato.types.js';

export class ElgatoZeroconfMatcher implements IDiscoveryMatcher<IElgatoMdnsRecord> {
  public id = 'elgato-zeroconf-match';
  public source = 'mdns' as const;
  public description = 'Recognize Elgato _elg._tcp.local zeroconf advertisements from the Home Assistant manifest.';

  public async matches(recordArg: IElgatoMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const txt = normalizeKeys({ ...recordArg.txt, ...recordArg.properties });
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const host = recordArg.host || recordArg.hostname || recordArg.addresses?.[0];
    const mac = ElgatoMapper.normalizeMac(txt.id || txt.mac || txt.macaddress);
    const name = stringValue(txt.name) || cleanServiceName(recordArg.name) || elgatoDisplayName;
    const matched = type === normalizeMdnsType(elgatoZeroconfType) || type.includes('_elg._tcp') || recordArg.metadata?.elgato === true;
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an Elgato _elg advertisement.' };
    }
    return {
      matched: true,
      confidence: host && mac ? 'certain' : host ? 'high' : 'medium',
      reason: 'mDNS service matches _elg._tcp.local.',
      normalizedDeviceId: mac || recordArg.name || host,
      candidate: {
        source: 'mdns',
        integrationDomain: elgatoDomain,
        id: mac || recordArg.name || host,
        host,
        port: recordArg.port || elgatoDefaultPort,
        name,
        manufacturer: elgatoManufacturer,
        model: txt.model || txt.product || elgatoDisplayName,
        macAddress: mac,
        metadata: {
          ...recordArg.metadata,
          elgato: true,
          discoveryProtocol: 'zeroconf',
          mdnsType: type,
          mdnsName: recordArg.name,
          txt,
        },
      },
    };
  }
}

export class ElgatoDhcpMatcher implements IDiscoveryMatcher<IElgatoDhcpRecord> {
  public id = 'elgato-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize DHCP candidates for already-known Elgato devices and obvious Elgato hostnames.';

  public async matches(recordArg: IElgatoDhcpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = recordArg.metadata || {};
    const host = recordArg.ip || recordArg.address || recordArg.host;
    const hostname = recordArg.hostname || recordArg.hostName || stringValue(metadata.hostname);
    const mac = ElgatoMapper.normalizeMac(recordArg.macaddress || recordArg.macAddress || stringValue(metadata.macAddress));
    const text = [recordArg.name, recordArg.manufacturer, recordArg.model, hostname, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(metadata.elgato)
      || recordArg.registeredDevices === true
      || recordArg.registered_devices === true
      || metadata.registeredDevices === true
      || metadata.registered_devices === true
      || text.includes('elgato')
      || text.includes('key light')
      || text.includes('keylight');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record does not contain Elgato hints.' };
    }
    const id = mac || (host ? `${host}:${elgatoDefaultPort}` : hostname);
    return {
      matched: true,
      confidence: host && mac ? 'high' : host ? 'medium' : 'low',
      reason: mac ? 'DHCP record has Elgato hints and a MAC address.' : 'DHCP record has Elgato hostname or registered-device metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: elgatoDomain,
        id,
        host,
        port: elgatoDefaultPort,
        name: recordArg.name || hostname || elgatoDisplayName,
        manufacturer: recordArg.manufacturer || elgatoManufacturer,
        model: recordArg.model || elgatoDisplayName,
        macAddress: mac,
        metadata: {
          ...metadata,
          elgato: true,
          discoveryProtocol: 'dhcp',
          hostname,
        },
      },
    };
  }
}

export class ElgatoManualMatcher implements IDiscoveryMatcher<IElgatoManualEntry> {
  public id = 'elgato-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Elgato host, snapshot, raw-data, client, and command-executor setup entries.';

  public async matches(inputArg: IElgatoManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData || rawDataFromInput(inputArg));
    const hasManualData = Boolean(snapshot || rawData || inputArg.client || metadata.client);
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.url || inputArg.serialNumber || inputArg.macAddress || metadata.elgato || hasManualData || text.includes('elgato') || text.includes('key light'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Elgato setup hints.' };
    }
    const host = parsed?.host || (inputArg.url ? undefined : inputArg.host) || snapshot?.device.host;
    const port = inputArg.port || parsed?.port || snapshot?.device.port || elgatoDefaultPort;
    const id = inputArg.id || inputArg.uniqueId || inputArg.serialNumber || inputArg.macAddress || snapshot?.device.id || snapshot?.device.serialNumber || snapshot?.device.macAddress || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Elgato setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: elgatoDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.device.name || elgatoDisplayName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || elgatoManufacturer,
        model: inputArg.model || snapshot?.device.model,
        serialNumber: inputArg.serialNumber || snapshot?.device.serialNumber,
        macAddress: inputArg.macAddress || snapshot?.device.macAddress,
        metadata: {
          ...metadata,
          elgato: true,
          discoveryProtocol: 'manual',
          snapshot,
          rawData,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
          serialNumber: inputArg.serialNumber || metadata.serialNumber,
          macAddress: inputArg.macAddress || metadata.macAddress,
        },
      },
    };
  }
}

export class ElgatoCandidateValidator implements IDiscoveryValidator {
  public id = 'elgato-candidate-validator';
  public description = 'Validate Elgato candidates from zeroconf, DHCP, and manual setup.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData);
    const mdnsType = typeof metadata.mdnsType === 'string' ? normalizeMdnsType(metadata.mdnsType) : '';
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, mdnsType, metadata.hostname].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === elgatoDomain
      || metadata.elgato === true
      || mdnsType.includes('_elg._tcp')
      || text.includes('elgato')
      || text.includes('key light')
      || text.includes('keylight');
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.client);
    const normalizedDeviceId = candidateArg.serialNumber || ElgatoMapper.normalizeMac(candidateArg.macAddress) || candidateArg.id || snapshot?.device.serialNumber || snapshot?.device.macAddress || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || elgatoDefaultPort}` : undefined);
    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Elgato candidate lacks a host, injected client, snapshot, or manual data.' : 'Candidate is not Elgato.',
        normalizedDeviceId,
      };
    }
    return {
      matched: true,
      confidence: candidateArg.host && normalizedDeviceId ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Elgato metadata and a usable local endpoint, client, snapshot, or manual data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: elgatoDomain,
        port: candidateArg.port || elgatoDefaultPort,
        manufacturer: candidateArg.manufacturer || elgatoManufacturer,
      },
    };
  }
}

export const createElgatoDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: elgatoDomain, displayName: elgatoDisplayName })
    .addMatcher(new ElgatoZeroconfMatcher())
    .addMatcher(new ElgatoDhcpMatcher())
    .addMatcher(new ElgatoManualMatcher())
    .addValidator(new ElgatoCandidateValidator());
};

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

const cleanServiceName = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/\._.*$/u, '').replace(/\.local\.?$/iu, '').trim() || undefined;
};

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined };
  } catch {
    return undefined;
  }
};

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 stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

const snapshotValue = (valueArg: unknown): IElgatoSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'light' in valueArg ? valueArg as IElgatoSnapshot : undefined;
};

const rawDataValue = (valueArg: unknown): Partial<IElgatoRawData> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Partial<IElgatoRawData> : undefined;
};

const rawDataFromInput = (inputArg: IElgatoManualEntry): Partial<IElgatoRawData> | undefined => {
  const rawData: Partial<IElgatoRawData> = {
    info: inputArg.info,
    settings: inputArg.settings,
    state: inputArg.state,
    battery: inputArg.battery,
  };
  return Object.values(rawData).some(Boolean) ? rawData : undefined;
};
