import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IAxisManualEntry, IAxisMdnsRecord, IAxisSsdpRecord, TAxisProtocol } from './axis.types.js';

const axisMdnsType = '_axis-video._tcp.local';
const axisOuis = ['00408c', 'accc8e', 'b8a44f', 'e82725'];

export class AxisMdnsMatcher implements IDiscoveryMatcher<IAxisMdnsRecord> {
  public id = 'axis-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Axis _axis-video mDNS advertisements and Axis OUIs.';

  public async matches(recordArg: IAxisMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeType(recordArg.type);
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const mac = normalizeMac(valueForKey(properties, 'macaddress') || valueForKey(properties, 'mac'));
    const name = cleanName(recordArg.name || recordArg.hostname);
    const matched = type === axisMdnsType || isAxisMac(mac) || name.toLowerCase().startsWith('axis-');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not an Axis video advertisement.' };
    }

    return {
      matched: true,
      confidence: type === axisMdnsType || isAxisMac(mac) ? 'certain' : 'high',
      reason: 'mDNS record matches Axis video metadata.',
      normalizedDeviceId: mac,
      candidate: {
        source: 'mdns',
        integrationDomain: 'axis',
        id: mac,
        host: recordArg.host || recordArg.addresses?.[0],
        port: recordArg.port || 80,
        name: name || undefined,
        manufacturer: 'Axis Communications AB',
        macAddress: mac || undefined,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt: properties,
          protocol: 'http' satisfies TAxisProtocol,
        },
      },
    };
  }
}

export class AxisSsdpMatcher implements IDiscoveryMatcher<IAxisSsdpRecord> {
  public id = 'axis-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Axis SSDP advertisements by manufacturer and UPnP metadata.';

  public async matches(recordArg: IAxisSsdpRecord): Promise<IDiscoveryMatch> {
    const upnp = { ...recordArg.headers, ...recordArg.upnp };
    const manufacturer = recordArg.manufacturer || valueForKey(upnp, 'manufacturer') || '';
    const location = recordArg.location || valueForKey(upnp, 'location') || valueForKey(upnp, 'presentationURL') || valueForKey(upnp, 'presentation_url');
    const url = safeUrl(location);
    const serial = valueForKey(upnp, 'serialNumber') || valueForKey(upnp, 'serial') || recordArg.usn;
    const friendlyName = valueForKey(upnp, 'friendlyName') || valueForKey(upnp, 'upnp:friendlyName');
    const model = valueForKey(upnp, 'modelName') || valueForKey(upnp, 'modelNumber');
    const matched = manufacturer.toLowerCase().includes('axis') || (recordArg.server || '').toLowerCase().includes('axis');
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not published by Axis.' };
    }

    return {
      matched: true,
      confidence: 'certain',
      reason: 'SSDP record manufacturer is Axis.',
      normalizedDeviceId: normalizeMac(serial) || serial,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'axis',
        id: normalizeMac(serial) || serial,
        host: url?.hostname,
        port: url?.port ? Number(url.port) : undefined,
        name: friendlyName,
        manufacturer: 'Axis Communications AB',
        model,
        serialNumber: serial,
        macAddress: normalizeMac(serial) || undefined,
        metadata: {
          protocol: url?.protocol === 'https:' ? 'https' : 'http',
          location,
          ssdp: upnp,
        },
      },
    };
  }
}

export class AxisManualMatcher implements IDiscoveryMatcher<IAxisManualEntry> {
  public id = 'axis-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Axis setup entries.';

  public async matches(inputArg: IAxisManualEntry): Promise<IDiscoveryMatch> {
    const mac = normalizeMac(inputArg.macAddress || inputArg.id || inputArg.serialNumber);
    const haystack = `${inputArg.name || ''} ${inputArg.model || ''} ${inputArg.manufacturer || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.metadata?.axis || haystack.includes('axis') || isAxisMac(mac));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Axis setup hints.' };
    }
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Axis setup.',
      normalizedDeviceId: mac || inputArg.id || inputArg.serialNumber,
      candidate: {
        source: 'manual',
        integrationDomain: 'axis',
        id: mac || inputArg.id || inputArg.serialNumber,
        host: inputArg.host,
        port: inputArg.port || (inputArg.protocol === 'http' ? 80 : 443),
        name: inputArg.name,
        manufacturer: inputArg.manufacturer || 'Axis Communications AB',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        macAddress: mac || undefined,
        metadata: {
          ...inputArg.metadata,
          protocol: inputArg.protocol || 'https',
        },
      },
    };
  }
}

export class AxisCandidateValidator implements IDiscoveryValidator {
  public id = 'axis-candidate-validator';
  public description = 'Validate that a discovery candidate can be configured as an Axis device.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const mac = normalizeMac(candidateArg.macAddress || candidateArg.id || candidateArg.serialNumber);
    const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
    const model = candidateArg.model?.toLowerCase() || '';
    const name = candidateArg.name?.toLowerCase() || '';
    const matched = candidateArg.integrationDomain === 'axis'
      || manufacturer.includes('axis')
      || model.includes('axis')
      || name.startsWith('axis')
      || isAxisMac(mac)
      || Boolean(candidateArg.metadata?.axis);

    return {
      matched,
      confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Axis metadata.' : 'Candidate is not Axis.',
      candidate: matched ? {
        ...candidateArg,
        integrationDomain: 'axis',
        manufacturer: candidateArg.manufacturer || 'Axis Communications AB',
        id: candidateArg.id || mac,
        macAddress: candidateArg.macAddress || mac || undefined,
      } : undefined,
      normalizedDeviceId: candidateArg.id || mac,
    };
  }
}

export const createAxisDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'axis', displayName: 'Axis' })
    .addMatcher(new AxisMdnsMatcher())
    .addMatcher(new AxisSsdpMatcher())
    .addMatcher(new AxisManualMatcher())
    .addValidator(new AxisCandidateValidator());
};

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

const valueForKey = (recordArg: Record<string, string | undefined> | undefined, keyArg: string): string | undefined => {
  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 cleanName = (valueArg: string | undefined): string => {
  return valueArg?.replace(/\._axis-video\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || '';
};

const normalizeMac = (valueArg: string | undefined): string => {
  const cleaned = (valueArg || '').replace(/[^a-fA-F0-9]/g, '').toLowerCase();
  return cleaned.length === 12 ? cleaned : '';
};

const isAxisMac = (macArg: string): boolean => {
  return Boolean(macArg && axisOuis.some((prefixArg) => macArg.startsWith(prefixArg)));
};

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
