import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IAmcrestManualEntry, IAmcrestMdnsRecord, IAmcrestSsdpRecord, TAmcrestProtocol } from './amcrest.types.js';
import { amcrestDefaultPort } from './amcrest.types.js';

export class AmcrestManualMatcher implements IDiscoveryMatcher<IAmcrestManualEntry> {
  public id = 'amcrest-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Amcrest local camera host or base URL entries.';

  public async matches(inputArg: IAmcrestManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(inputArg);
    const hint = hasAmcrestHint(inputArg.name, inputArg.manufacturer, inputArg.model) || Boolean(inputArg.metadata?.amcrest || inputArg.metadata?.dahua);
    if (!endpoint.host && !hint) {
      return { matched: false, confidence: 'low', reason: 'Manual Amcrest entry requires host, url, or Amcrest metadata.' };
    }
    const normalizedDeviceId = normalizeMac(inputArg.macAddress || inputArg.id || inputArg.serialNumber) || inputArg.id || inputArg.serialNumber || endpoint.host || endpoint.url;
    return {
      matched: true,
      confidence: endpoint.host ? 'high' : 'medium',
      reason: endpoint.host ? 'Manual entry contains a local Amcrest camera endpoint.' : 'Manual entry contains Amcrest metadata.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: 'amcrest',
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || endpoint.host,
        manufacturer: inputArg.manufacturer || 'Amcrest',
        model: inputArg.model,
        serialNumber: inputArg.serialNumber,
        macAddress: normalizeMac(inputArg.macAddress) || undefined,
        metadata: {
          ...inputArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          username: inputArg.username,
          password: inputArg.password,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class AmcrestMdnsMatcher implements IDiscoveryMatcher<IAmcrestMdnsRecord> {
  public id = 'amcrest-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Amcrest-style mDNS records by host, name, and TXT metadata.';

  public async matches(recordArg: IAmcrestMdnsRecord, 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 = hasAmcrestHint(name, manufacturer, model) || Boolean(valueForKey(properties, 'amcrest'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not contain Amcrest camera hints.' };
    }
    return {
      matched: true,
      confidence: 'high',
      reason: 'mDNS record contains Amcrest camera metadata.',
      normalizedDeviceId: mac || serial || recordArg.host,
      candidate: {
        source: 'mdns',
        integrationDomain: 'amcrest',
        id: mac || serial || recordArg.host,
        host: recordArg.host || recordArg.addresses?.[0],
        port: recordArg.port || amcrestDefaultPort,
        name: name || undefined,
        manufacturer: manufacturer || 'Amcrest',
        model,
        serialNumber: serial,
        macAddress: mac || undefined,
        metadata: {
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt: properties,
          protocol: 'http' satisfies TAmcrestProtocol,
        },
      },
    };
  }
}

export class AmcrestSsdpMatcher implements IDiscoveryMatcher<IAmcrestSsdpRecord> {
  public id = 'amcrest-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize local Amcrest cameras from SSDP manufacturer and UPnP metadata.';

  public async matches(recordArg: IAmcrestSsdpRecord, 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 = hasAmcrestHint(friendlyName, manufacturer, model) || hasAmcrestHint(recordArg.server);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not published by an Amcrest-like camera.' };
    }
    return {
      matched: true,
      confidence: 'high',
      reason: 'SSDP record contains Amcrest camera metadata.',
      normalizedDeviceId: mac || serial,
      candidate: {
        source: 'ssdp',
        integrationDomain: 'amcrest',
        id: mac || serial,
        host: url?.hostname,
        port: url?.port ? Number(url.port) : amcrestDefaultPort,
        name: friendlyName,
        manufacturer: manufacturer || 'Amcrest',
        model,
        serialNumber: serial,
        macAddress: mac || undefined,
        metadata: {
          protocol: url?.protocol === 'https:' ? 'https' : 'http',
          location,
          ssdp: upnp,
        },
      },
    };
  }
}

export class AmcrestCandidateValidator implements IDiscoveryValidator {
  public id = 'amcrest-candidate-validator';
  public description = 'Validate that a candidate can be configured as a local Amcrest camera.';

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

export const createAmcrestDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'amcrest', displayName: 'Amcrest' })
    .addMatcher(new AmcrestManualMatcher())
    .addMatcher(new AmcrestMdnsMatcher())
    .addMatcher(new AmcrestSsdpMatcher())
    .addValidator(new AmcrestCandidateValidator());
};

const endpointFromInput = (inputArg: IAmcrestManualEntry): { protocol: TAmcrestProtocol; host?: string; port: number; url?: string } => {
  const url = safeUrl(inputArg.url || inputArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : protocol === 'https' ? 443 : amcrestDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const protocol = inputArg.protocol || 'http';
  const port = inputArg.port || amcrestDefaultPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TAmcrestProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof candidateArg.metadata?.url === 'string' ? candidateArg.metadata.url : undefined;
  const metadataProtocol = candidateArg.metadata?.protocol === 'https' ? '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 : amcrestDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const port = candidateArg.port || amcrestDefaultPort;
  return { protocol: metadataProtocol, host: candidateArg.host, port, url: candidateArg.host ? `${metadataProtocol}://${candidateArg.host}:${port}` : metadataUrl };
};

const hasAmcrestHint = (...valuesArgs: Array<string | undefined>): boolean => {
  const haystack = valuesArgs.filter(Boolean).join(' ').toLowerCase();
  return haystack.includes('amcrest') || haystack.includes('dahua');
};

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 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;
  }
};
