import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IAgentDvrManualEntry, IAgentDvrMdnsRecord, IAgentDvrSsdpRecord, TAgentDvrProtocol } from './agent_dvr.types.js';
import { agentDvrDefaultPort, agentDvrDomain } from './agent_dvr.types.js';

export class AgentDvrManualMatcher implements IDiscoveryMatcher<IAgentDvrManualEntry> {
  public id = 'agent-dvr-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Agent DVR local server host, URL, or snapshot entries.';

  public async matches(inputArg: IAgentDvrManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromManual(inputArg);
    const snapshot = inputArg.snapshot || (inputArg.metadata?.snapshot as IAgentDvrManualEntry['snapshot'] | undefined);
    const hint = hasAgentDvrHint(inputArg.name, inputArg.manufacturer, inputArg.model) || Boolean(inputArg.metadata?.agentDvr || inputArg.metadata?.ispy);
    if (!endpoint.host && !snapshot && !hint) {
      return { matched: false, confidence: 'low', reason: 'Manual Agent DVR entry requires host, url, snapshot, or Agent DVR metadata.' };
    }
    const normalizedDeviceId = inputArg.uniqueId || inputArg.id || snapshot?.server.unique || endpoint.host || snapshot?.server.name;
    return {
      matched: true,
      confidence: snapshot ? 'certain' : endpoint.host ? 'high' : 'medium',
      reason: snapshot ? 'Manual entry contains an Agent DVR snapshot.' : endpoint.host ? 'Manual entry contains a local Agent DVR endpoint.' : 'Manual entry contains Agent DVR metadata.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: agentDvrDomain,
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || snapshot?.server.name || endpoint.host,
        manufacturer: inputArg.manufacturer || 'iSpyConnect',
        model: inputArg.model || 'Agent DVR',
        serialNumber: inputArg.serialNumber || snapshot?.server.unique,
        macAddress: normalizeMac(inputArg.macAddress) || undefined,
        metadata: {
          ...inputArg.metadata,
          agentDvr: true,
          protocol: endpoint.protocol,
          url: endpoint.url,
          serverUrl: endpoint.url,
          snapshot,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class AgentDvrMdnsMatcher implements IDiscoveryMatcher<IAgentDvrMdnsRecord> {
  public id = 'agent-dvr-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize local Agent DVR mDNS records by name, service type, and TXT metadata.';

  public async matches(recordArg: IAgentDvrMdnsRecord, 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, 'unique') || valueForKey(properties, 'id');
    const name = cleanMdnsName(recordArg.name || recordArg.hostname);
    const matched = hasAgentDvrHint(name, recordArg.type, manufacturer, model, valueForKey(properties, 'server')) || Boolean(valueForKey(properties, 'agentDvr') || valueForKey(properties, 'ispy'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not contain Agent DVR hints.' };
    }
    const host = recordArg.host || recordArg.addresses?.[0];
    return {
      matched: true,
      confidence: 'medium',
      reason: 'mDNS record contains Agent DVR metadata.',
      normalizedDeviceId: serial || host || name,
      candidate: {
        source: 'mdns',
        integrationDomain: agentDvrDomain,
        id: serial || host || name,
        host,
        port: recordArg.port || agentDvrDefaultPort,
        name: name || undefined,
        manufacturer: manufacturer || 'iSpyConnect',
        model: model || 'Agent DVR',
        serialNumber: serial,
        metadata: {
          agentDvr: true,
          mdnsName: recordArg.name,
          mdnsType: recordArg.type,
          txt: properties,
          protocol: 'http' satisfies TAgentDvrProtocol,
          discoveryProtocol: 'mdns',
        },
      },
    };
  }
}

export class AgentDvrSsdpMatcher implements IDiscoveryMatcher<IAgentDvrSsdpRecord> {
  public id = 'agent-dvr-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize local Agent DVR SSDP records by UPnP manufacturer and friendly name metadata.';

  public async matches(recordArg: IAgentDvrSsdpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const upnp = { ...recordArg.headers, ...recordArg.upnp };
    const manufacturer = recordArg.manufacturer || valueForKey(upnp, 'manufacturer') || valueForKey(upnp, 'vendor') || '';
    const model = valueForKey(upnp, 'modelName') || valueForKey(upnp, 'modelNumber') || valueForKey(upnp, 'model');
    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, 'unique') || recordArg.usn;
    const matched = hasAgentDvrHint(friendlyName, manufacturer, model, recordArg.server, recordArg.st);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not published by Agent DVR.' };
    }
    const protocol: TAgentDvrProtocol = url?.protocol === 'https:' ? 'https' : 'http';
    return {
      matched: true,
      confidence: 'medium',
      reason: 'SSDP record contains Agent DVR metadata.',
      normalizedDeviceId: serial || url?.hostname || friendlyName,
      candidate: {
        source: 'ssdp',
        integrationDomain: agentDvrDomain,
        id: serial || url?.hostname || friendlyName,
        host: url?.hostname,
        port: url?.port ? Number(url.port) : agentDvrDefaultPort,
        name: friendlyName,
        manufacturer: manufacturer || 'iSpyConnect',
        model: model || 'Agent DVR',
        serialNumber: serial,
        metadata: {
          agentDvr: true,
          protocol,
          location,
          ssdp: upnp,
          discoveryProtocol: 'ssdp',
        },
      },
    };
  }
}

export class AgentDvrCandidateValidator implements IDiscoveryValidator {
  public id = 'agent-dvr-candidate-validator';
  public description = 'Validate that a candidate can be configured as a local Agent DVR server.';

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

export const createAgentDvrDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: agentDvrDomain, displayName: 'Agent DVR' })
    .addMatcher(new AgentDvrManualMatcher())
    .addMatcher(new AgentDvrMdnsMatcher())
    .addMatcher(new AgentDvrSsdpMatcher())
    .addValidator(new AgentDvrCandidateValidator());
};

export const agentDvrDefaultConfigFromCandidate = (candidateArg: IDiscoveryCandidate) => {
  const endpoint = endpointFromCandidate(candidateArg);
  return {
    protocol: endpoint.protocol,
    host: endpoint.host,
    port: endpoint.port,
    url: endpoint.url,
    serverUrl: endpoint.url,
  };
};

const endpointFromManual = (inputArg: IAgentDvrManualEntry): { protocol: TAgentDvrProtocol; host?: string; port: number; url?: string } => {
  const url = safeUrl(inputArg.serverUrl || inputArg.url || inputArg.host);
  if (url) {
    const protocol: TAgentDvrProtocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : inputArg.port || (protocol === 'https' ? 443 : agentDvrDefaultPort);
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}/` };
  }
  const protocol = inputArg.protocol || 'http';
  const port = inputArg.port || agentDvrDefaultPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}/` : undefined };
};

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

const hasAgentDvrHint = (...valuesArgs: Array<string | undefined>): boolean => {
  const haystack = valuesArgs.filter(Boolean).join(' ').toLowerCase();
  return haystack.includes('agent dvr') || haystack.includes('agentdvr') || haystack.includes('ispyconnect') || haystack.includes('ispy');
};

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 {
    const url = new URL(valueArg);
    if (url.protocol === 'http:' || url.protocol === 'https:') {
      return url;
    }
  } catch {
    // Try again below with an explicit local HTTP scheme.
  }
  try {
    return new URL(`http://${valueArg}`);
  } catch {
    return undefined;
  }
};
