import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { brotherDefaultCommunity, brotherDefaultPort, brotherDomain } from './brother.constants.js';
import { BrotherMapper } from './brother.mapper.js';
import type { IBrotherManualEntry, IBrotherMdnsRecord, IBrotherRawData, IBrotherSensors, IBrotherSnapshot, TBrotherPrinterType } from './brother.types.js';

const brotherPrinterMdnsType = '_printer._tcp.local';

export class BrotherMdnsMatcher implements IDiscoveryMatcher<IBrotherMdnsRecord> {
  public id = 'brother-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Brother printer mDNS advertisements matching Home Assistant zeroconf metadata.';

  public async matches(recordArg: IBrotherMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeType(recordArg.type || stringMetadata(recordArg.metadata || {}, 'type'));
    const properties = { ...(recordArg.txt || {}), ...(recordArg.properties || {}) };
    const serviceName = cleanName(recordArg.name || recordArg.hostname, type);
    const product = stringValue(valueForKey(properties, 'product')) || stringValue(valueForKey(properties, 'ty'));
    const usbManufacturer = stringValue(valueForKey(properties, 'usb_MFG')) || stringValue(valueForKey(properties, 'mfg'));
    const model = stringValue(valueForKey(properties, 'usb_MDL')) || stringValue(valueForKey(properties, 'mdl')) || modelFromProduct(product);
    const haystack = [recordArg.name, serviceName, product, usbManufacturer, model].filter(Boolean).join(' ').toLowerCase();
    const matched = type === brotherPrinterMdnsType && (serviceName.toLowerCase().startsWith('brother') || haystack.includes('brother'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a Brother printer advertisement.' };
    }

    const serial = stringValue(valueForKey(properties, 'serial')) || stringValue(valueForKey(properties, 'serialNumber')) || stringValue(valueForKey(properties, 'usb_SN'));
    const macAddress = stringValue(valueForKey(properties, 'mac')) || stringValue(valueForKey(properties, 'macAddress'));
    const host = recordArg.host || recordArg.addresses?.[0];
    const id = serial || macAddress || host;
    return {
      matched: true,
      confidence: id && host ? 'certain' : host ? 'high' : 'medium',
      reason: 'mDNS record matches Brother printer zeroconf hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: brotherDomain,
        id,
        host,
        port: brotherDefaultPort,
        name: serviceName || product || model || 'Brother Printer',
        manufacturer: 'Brother',
        model,
        serialNumber: serial,
        macAddress,
        metadata: {
          brother: true,
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          mdnsPort: recordArg.port,
          txt: properties,
          product,
          printerType: printerTypeFromMetadata(properties) || 'laser',
          community: brotherDefaultCommunity,
        },
      },
    };
  }
}

export class BrotherManualMatcher implements IDiscoveryMatcher<IBrotherManualEntry> {
  public id = 'brother-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Brother printer host, snapshot, raw SNMP, or sensor setup entries.';

  public async matches(inputArg: IBrotherManualEntry): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = inputArg.snapshot || snapshotFromMetadata(metadata);
    const rawData = inputArg.rawData || rawDataFromMetadata(metadata);
    const sensors = inputArg.sensors || sensorsFromMetadata(metadata);
    const hasManualData = Boolean(snapshot || rawData || sensors || 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 || metadata.brother || hasManualData || text.includes('brother'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Brother printer setup hints.' };
    }

    const host = parsed?.host || (inputArg.url ? undefined : inputArg.host) || snapshot?.printer.host;
    const port = inputArg.port || parsed?.port || numberMetadata(metadata, 'port') || snapshot?.printer.port || brotherDefaultPort;
    const serial = inputArg.serialNumber || inputArg.serial || snapshot?.printer.serialNumber || snapshot?.printer.serial || stringMetadata(metadata, 'serial') || stringMetadata(metadata, 'serialNumber');
    const macAddress = inputArg.macAddress || snapshot?.printer.macAddress || stringMetadata(metadata, 'macAddress');
    const id = inputArg.id || inputArg.uniqueId || BrotherMapper.snapshotId(snapshot || manualSnapshot(inputArg, host, port, rawData, sensors)) || serial || macAddress || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Brother printer setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: brotherDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.printer.name,
        manufacturer: inputArg.manufacturer || snapshot?.printer.manufacturer || 'Brother',
        model: inputArg.model || snapshot?.printer.model,
        serialNumber: serial,
        macAddress,
        metadata: {
          ...metadata,
          brother: true,
          community: inputArg.community || stringMetadata(metadata, 'community') || brotherDefaultCommunity,
          printerType: printerType(inputArg.printerType || inputArg.type || metadata.printerType || metadata.type || snapshot?.printer.printerType) || 'laser',
          snapshot,
          rawData,
          sensors,
          client: inputArg.client || metadata.client,
        },
      },
    };
  }
}

export class BrotherCandidateValidator implements IDiscoveryValidator {
  public id = 'brother-candidate-validator';
  public description = 'Validate Brother candidates have a usable host, snapshot, raw SNMP data, sensors, or injected client.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const type = normalizeType(stringMetadata(metadata, 'mdnsType'));
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === brotherDomain
      || Boolean(metadata.brother)
      || (type === brotherPrinterMdnsType && text.includes('brother'))
      || text.includes('brother');
    const snapshot = snapshotFromMetadata(metadata);
    const hasManualData = Boolean(snapshot || metadata.rawData || metadata.sensors || metadata.client);
    const hasUsableSource = Boolean(candidateArg.host || hasManualData);
    const normalizedDeviceId = candidateArg.id || candidateArg.serialNumber || candidateArg.macAddress || (snapshot ? BrotherMapper.snapshotId(snapshot) : undefined) || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || brotherDefaultPort}` : undefined);

    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Brother candidate lacks host, snapshot, raw SNMP data, sensors, or client information.' : 'Candidate is not Brother.',
        normalizedDeviceId,
      };
    }

    return {
      matched: true,
      confidence: normalizedDeviceId && candidateArg.host ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Brother printer metadata and a usable source.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: brotherDomain,
        port: candidateArg.port || brotherDefaultPort,
        manufacturer: candidateArg.manufacturer || 'Brother',
        metadata: {
          ...metadata,
          brother: true,
          community: stringMetadata(metadata, 'community') || brotherDefaultCommunity,
          printerType: printerType(metadata.printerType || metadata.type) || 'laser',
        },
      },
    };
  }
}

export const createBrotherDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: brotherDomain, displayName: 'Brother Printer' })
    .addMatcher(new BrotherMdnsMatcher())
    .addMatcher(new BrotherManualMatcher())
    .addValidator(new BrotherCandidateValidator());
};

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

const cleanName = (valueArg: string | undefined, typeArg: string): string => {
  const escaped = typeArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  return valueArg?.replace(new RegExp(`\\.${escaped}\\.?$`, 'i'), '').replace(/\.local\.?$/i, '').trim() || '';
};

const valueForKey = (recordArg: Record<string, unknown>, keyArg: string): unknown => {
  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 => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

const stringMetadata = (metadataArg: Record<string, unknown>, keyArg: string): string | undefined => stringValue(metadataArg[keyArg]);

const numberMetadata = (metadataArg: Record<string, unknown>, keyArg: string): number | undefined => {
  const value = metadataArg[keyArg];
  if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
    return Math.round(value);
  }
  if (typeof value === 'string' && value.trim()) {
    const parsed = Number(value);
    return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : undefined;
  }
  return undefined;
};

const modelFromProduct = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/^\(?brother\s+/i, '').replace(/[()]$/g, '').trim() || valueArg;
};

const printerTypeFromMetadata = (metadataArg: Record<string, unknown>): TBrotherPrinterType | undefined => {
  const value = printerType(valueForKey(metadataArg, 'printerType') || valueForKey(metadataArg, 'type'));
  if (value) {
    return value;
  }
  const text = Object.values(metadataArg).filter((itemArg) => typeof itemArg === 'string').join(' ').toLowerCase();
  return text.includes('ink') ? 'ink' : undefined;
};

const printerType = (valueArg: unknown): TBrotherPrinterType | undefined => {
  const value = stringValue(valueArg)?.toLowerCase();
  return value === 'ink' || value === 'laser' ? value : 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 snapshotFromMetadata = (metadataArg: Record<string, unknown>): IBrotherSnapshot | undefined => {
  const snapshot = metadataArg.snapshot;
  return snapshot && typeof snapshot === 'object' && 'printer' in snapshot && 'sensors' in snapshot ? snapshot as IBrotherSnapshot : undefined;
};

const rawDataFromMetadata = (metadataArg: Record<string, unknown>): IBrotherRawData | undefined => {
  const rawData = metadataArg.rawData;
  return rawData && typeof rawData === 'object' && !Array.isArray(rawData) ? rawData as IBrotherRawData : undefined;
};

const sensorsFromMetadata = (metadataArg: Record<string, unknown>): IBrotherSensors | undefined => {
  const sensors = metadataArg.sensors;
  return sensors && typeof sensors === 'object' && !Array.isArray(sensors) ? sensors as IBrotherSensors : undefined;
};

const manualSnapshot = (inputArg: IBrotherManualEntry, hostArg: string | undefined, portArg: number, rawDataArg?: IBrotherRawData, sensorsArg?: IBrotherSensors): IBrotherSnapshot => {
  return BrotherMapper.toSnapshot({
    config: {
      host: hostArg,
      port: portArg,
      name: inputArg.name,
      manufacturer: inputArg.manufacturer,
      model: inputArg.model,
      serial: inputArg.serial,
      serialNumber: inputArg.serialNumber,
      macAddress: inputArg.macAddress,
      printerType: printerType(inputArg.printerType || inputArg.type) || 'laser',
    },
    rawData: rawDataArg,
    sensors: sensorsArg,
    online: Boolean(rawDataArg || sensorsArg),
    source: rawDataArg || sensorsArg ? 'manual' : 'runtime',
  });
};
