import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IFoscamManualEntry, IFoscamMdnsRecord, IFoscamSsdpRecord, TFoscamProtocol, TFoscamStream } from './foscam.types.js';
import { foscamDefaultPort, foscamDefaultRtspPort, foscamDomain } from './foscam.types.js';

export class FoscamManualMatcher implements IDiscoveryMatcher<IFoscamManualEntry> {
  public id = 'foscam-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Foscam local camera host or base URL entries.';

  public async matches(inputArg: IFoscamManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(inputArg);
    const hint = hasFoscamHint(inputArg.name, inputArg.manufacturer, inputArg.model) || Boolean(inputArg.metadata?.foscam);
    if (!endpoint.host && !hint) {
      return { matched: false, confidence: 'low', reason: 'Manual Foscam entry requires host, url, or Foscam 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 Foscam camera endpoint.' : 'Manual entry contains Foscam metadata.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: foscamDomain,
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || endpoint.host,
        manufacturer: inputArg.manufacturer || 'Foscam',
        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,
          stream: inputArg.stream,
          rtspPort: inputArg.rtspPort,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class FoscamMdnsMatcher implements IDiscoveryMatcher<IFoscamMdnsRecord> {
  public id = 'foscam-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Foscam-like mDNS records by host, name, and TXT metadata.';

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

export class FoscamSsdpMatcher implements IDiscoveryMatcher<IFoscamSsdpRecord> {
  public id = 'foscam-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize local Foscam cameras from SSDP manufacturer and UPnP metadata.';

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

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

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== foscamDomain) {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not Foscam.` };
    }
    const endpoint = endpointFromCandidate(candidateArg);
    const mac = normalizeMac(candidateArg.macAddress || candidateArg.id || candidateArg.serialNumber);
    const hasHint = candidateArg.integrationDomain === foscamDomain
      || candidateArg.source === 'manual'
      || hasFoscamHint(candidateArg.name, candidateArg.manufacturer, candidateArg.model)
      || Boolean(candidateArg.metadata?.foscam);
    if (!hasHint || !endpoint.host) {
      return { matched: false, confidence: 'low', reason: 'Foscam candidates require a host plus manual or Foscam camera metadata.' };
    }
    if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
      return { matched: false, confidence: 'low', reason: 'Foscam candidate has an invalid port.' };
    }
    return {
      matched: true,
      confidence: candidateArg.source === 'manual' ? 'high' : 'medium',
      reason: 'Candidate has enough local Foscam metadata to start configuration.',
      normalizedDeviceId: candidateArg.id || mac || endpoint.host,
      candidate: {
        ...candidateArg,
        integrationDomain: foscamDomain,
        id: candidateArg.id || mac || endpoint.host,
        host: endpoint.host,
        port: endpoint.port,
        manufacturer: candidateArg.manufacturer || 'Foscam',
        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 createFoscamDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: foscamDomain, displayName: 'Foscam' })
    .addMatcher(new FoscamManualMatcher())
    .addMatcher(new FoscamMdnsMatcher())
    .addMatcher(new FoscamSsdpMatcher())
    .addValidator(new FoscamCandidateValidator());
};

const endpointFromInput = (inputArg: IFoscamManualEntry): { protocol: TFoscamProtocol; 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 : foscamDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const protocol = inputArg.protocol || 'http';
  const port = inputArg.port || foscamDefaultPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TFoscamProtocol; 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 : foscamDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  }
  const port = candidateArg.port || foscamDefaultPort;
  return { protocol: metadataProtocol, host: candidateArg.host, port, url: candidateArg.host ? `${metadataProtocol}://${candidateArg.host}:${port}` : metadataUrl };
};

export const foscamStreamFromMetadata = (candidateArg: IDiscoveryCandidate): TFoscamStream | undefined => {
  return candidateArg.metadata?.stream === 'Main' || candidateArg.metadata?.stream === 'Sub' ? candidateArg.metadata.stream : undefined;
};

export const foscamRtspPortFromMetadata = (candidateArg: IDiscoveryCandidate): number | undefined => {
  const value = candidateArg.metadata?.rtspPort;
  return typeof value === 'number' && Number.isInteger(value) ? value : typeof value === 'string' && value.trim() ? Number(value) || undefined : undefined;
};

export const foscamDefaultConfigFromCandidate = (candidateArg: IDiscoveryCandidate) => {
  const endpoint = endpointFromCandidate(candidateArg);
  return {
    protocol: endpoint.protocol,
    host: endpoint.host,
    port: endpoint.port,
    url: endpoint.url,
    stream: foscamStreamFromMetadata(candidateArg) || 'Main' as TFoscamStream,
    rtspPort: foscamRtspPortFromMetadata(candidateArg) || foscamDefaultRtspPort,
  };
};

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

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