import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IHikvisionManualEntry, IHikvisionMdnsRecord, IHikvisionSsdpRecord, THikvisionProtocol } from './hikvision.types.js';
import { hikvisionDefaultPort } from './hikvision.types.js';

export class HikvisionManualMatcher implements IDiscoveryMatcher<IHikvisionManualEntry> {
  public id = 'hikvision-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Hikvision camera or NVR host entries.';

  public async matches(inputArg: IHikvisionManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromManual(inputArg);
    const hasHint = hasHikvisionHint(inputArg.name, inputArg.manufacturer, inputArg.model, inputArg.deviceType) || Boolean(inputArg.metadata?.hikvision || inputArg.snapshot);
    if (!endpoint.host && !hasHint) {
      return { matched: false, confidence: 'low', reason: 'Manual Hikvision entries require a host, cached snapshot, or Hikvision metadata.' };
    }
    const normalizedDeviceId = normalizeMac(inputArg.macAddress || inputArg.id || inputArg.serialNumber) || inputArg.id || inputArg.serialNumber || endpoint.host;
    return {
      matched: true,
      confidence: endpoint.host ? 'high' : 'medium',
      reason: endpoint.host ? 'Manual entry contains a local Hikvision camera/NVR endpoint.' : 'Manual entry contains Hikvision metadata.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: 'hikvision',
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || endpoint.host,
        manufacturer: inputArg.manufacturer || 'Hikvision',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        macAddress: normalizeMac(inputArg.macAddress) || undefined,
        metadata: {
          ...inputArg.metadata,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
          username: inputArg.username,
          password: inputArg.password,
          deviceType: inputArg.deviceType,
          snapshot: inputArg.snapshot,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
        manualSupported: true,
      },
    };
  }
}

export class HikvisionMdnsMatcher implements IDiscoveryMatcher<IHikvisionMdnsRecord> {
  public id = 'hikvision-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Hikvision mDNS records by name or TXT metadata.';

  public async matches(recordArg: IHikvisionMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const manufacturer = valueForKey(properties, 'manufacturer') || valueForKey(properties, 'vendor');
    const model = valueForKey(properties, 'model') || valueForKey(properties, 'product');
    const serial = valueForKey(properties, 'serial') || valueForKey(properties, 'serialNumber');
    const mac = normalizeMac(valueForKey(properties, 'mac') || valueForKey(properties, 'macAddress') || serial);
    const name = cleanMdnsName(recordArg.name || recordArg.hostname);
    const matched = hasHikvisionHint(name, manufacturer, model, valueForKey(properties, 'deviceType')) || Boolean(valueForKey(properties, 'hikvision'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not contain Hikvision camera/NVR hints.' };
    }
    return {
      matched: true,
      confidence: 'medium',
      reason: 'mDNS record contains Hikvision camera/NVR metadata.',
      normalizedDeviceId: mac || serial || recordArg.host || recordArg.addresses?.[0],
      candidate: {
        source: 'mdns',
        integrationDomain: 'hikvision',
        id: mac || serial || recordArg.host || recordArg.addresses?.[0],
        host: recordArg.host || recordArg.addresses?.[0],
        port: recordArg.port || hikvisionDefaultPort,
        name: name || undefined,
        manufacturer: manufacturer || 'Hikvision',
        model,
        serialNumber: serial,
        macAddress: mac || undefined,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt: properties,
          protocol: 'http' satisfies THikvisionProtocol,
          discoveryProtocol: 'mdns',
        },
      },
    };
  }
}

export class HikvisionSsdpMatcher implements IDiscoveryMatcher<IHikvisionSsdpRecord> {
  public id = 'hikvision-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize local Hikvision SSDP/UPnP camera or NVR records.';

  public async matches(recordArg: IHikvisionSsdpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const upnp = { ...recordArg.headers, ...recordArg.upnp };
    const manufacturer = recordArg.manufacturer || valueForKey(upnp, 'manufacturer') || '';
    const model = valueForKey(upnp, 'modelName') || valueForKey(upnp, 'modelNumber');
    const friendlyName = valueForKey(upnp, 'friendlyName') || valueForKey(upnp, 'upnp:friendlyName');
    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 mac = normalizeMac(serial);
    const matched = hasHikvisionHint(friendlyName, manufacturer, model, recordArg.server);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not published by a Hikvision-like camera/NVR.' };
    }
    return {
      matched: true,
      confidence: 'high',
      reason: 'SSDP record contains Hikvision camera/NVR metadata.',
      normalizedDeviceId: mac || serial || url?.hostname,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'hikvision',
        id: mac || serial || url?.hostname,
        host: url?.hostname,
        port: url?.port ? Number(url.port) : hikvisionDefaultPort,
        name: friendlyName,
        manufacturer: manufacturer || 'Hikvision',
        model,
        serialNumber: serial,
        macAddress: mac || undefined,
        metadata: {
          protocol: url?.protocol === 'https:' ? 'https' : 'http',
          location,
          ssdp: upnp,
          discoveryProtocol: 'ssdp',
        },
      },
    };
  }
}

export class HikvisionCandidateValidator implements IDiscoveryValidator {
  public id = 'hikvision-candidate-validator';
  public description = 'Validate local Hikvision camera/NVR candidates for configuration.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== 'hikvision') {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not Hikvision.` };
    }
    const endpoint = endpointFromCandidate(candidateArg);
    const snapshot = candidateArg.metadata?.snapshot;
    const hasHint = candidateArg.integrationDomain === 'hikvision'
      || candidateArg.source === 'manual'
      || hasHikvisionHint(candidateArg.name, candidateArg.manufacturer, candidateArg.model, stringMetadata(candidateArg, 'deviceType'))
      || Boolean(candidateArg.metadata?.hikvision || snapshot);
    if (!hasHint || (!endpoint.host && !snapshot)) {
      return { matched: false, confidence: 'low', reason: 'Hikvision candidates require a host or cached snapshot plus Hikvision/manual metadata.' };
    }
    if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
      return { matched: false, confidence: 'low', reason: 'Hikvision candidate has an invalid port.' };
    }
    const mac = normalizeMac(candidateArg.macAddress || candidateArg.id || candidateArg.serialNumber);
    return {
      matched: true,
      confidence: candidateArg.source === 'manual' || snapshot ? 'high' : 'medium',
      reason: 'Candidate has enough local Hikvision metadata to start configuration.',
      normalizedDeviceId: candidateArg.id || mac || candidateArg.serialNumber || endpoint.host,
      candidate: {
        ...candidateArg,
        integrationDomain: 'hikvision',
        id: candidateArg.id || mac || candidateArg.serialNumber || endpoint.host,
        host: endpoint.host,
        port: endpoint.port,
        manufacturer: candidateArg.manufacturer || 'Hikvision',
        macAddress: candidateArg.macAddress || mac || undefined,
        metadata: {
          ...candidateArg.metadata,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
        manualSupported: candidateArg.source === 'manual',
      },
    };
  }
}

export const createHikvisionDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'hikvision', displayName: 'Hikvision' })
    .addMatcher(new HikvisionManualMatcher())
    .addMatcher(new HikvisionMdnsMatcher())
    .addMatcher(new HikvisionSsdpMatcher())
    .addValidator(new HikvisionCandidateValidator());
};

const endpointFromManual = (inputArg: IHikvisionManualEntry): { protocol: THikvisionProtocol; host?: string; port: number; url?: string } => {
  const url = safeUrl(typeof inputArg.metadata?.url === 'string' ? inputArg.metadata.url : inputArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : protocol === 'https' ? 443 : hikvisionDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const protocol = inputArg.protocol || (inputArg.ssl ? 'https' : 'http');
  const port = inputArg.port || (protocol === 'https' ? 443 : hikvisionDefaultPort);
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: THikvisionProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof candidateArg.metadata?.url === 'string' ? candidateArg.metadata.url : undefined;
  const metadataProtocol = candidateArg.metadata?.protocol === 'https' || candidateArg.metadata?.ssl === true ? 'https' : 'http';
  const url = safeUrl(metadataUrl || candidateArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : protocol === 'https' ? 443 : hikvisionDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const port = candidateArg.port || (metadataProtocol === 'https' ? 443 : hikvisionDefaultPort);
  return { protocol: metadataProtocol, host: candidateArg.host, port, url: candidateArg.host ? `${metadataProtocol}://${candidateArg.host}:${port}` : metadataUrl };
};

const hasHikvisionHint = (...valuesArgs: Array<string | undefined>): boolean => {
  const haystack = valuesArgs.filter(Boolean).join(' ').toLowerCase();
  return haystack.includes('hikvision') || haystack.includes('hik-connect') || haystack.includes('hiwatch') || haystack.includes('ezviz');
};

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 stringMetadata = (candidateArg: IDiscoveryCandidate, keyArg: string): string | undefined => {
  const value = candidateArg.metadata?.[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
};

const cleanMdnsName = (valueArg: string | undefined): string => {
  return valueArg?.replace(/\._[^.]+\._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 safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
