import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type {
  IFritzboxCallmonitorManualDiscoveryRecord,
  IFritzboxCallmonitorMdnsDiscoveryRecord,
  IFritzboxCallmonitorSnapshot,
  IFritzboxCallmonitorSsdpDiscoveryRecord,
} from './fritzbox_callmonitor.types.js';
import { fritzboxCallmonitorDefaultPort, fritzboxCallmonitorDomain, fritzboxCallmonitorManufacturer } from './fritzbox_callmonitor.types.js';

const fritzTextHints = ['fritz!box', 'fritzbox', 'fritz box', 'avm'];
const fritzSsdpSt = 'urn:schemas-upnp-org:device:fritzbox:1';

export class FritzboxCallmonitorManualMatcher implements IDiscoveryMatcher<IFritzboxCallmonitorManualDiscoveryRecord> {
  public id = 'fritzbox-callmonitor-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual FRITZ!Box Call Monitor setup entries, snapshots, and raw call monitor event lines.';

  public async matches(inputArg: IFritzboxCallmonitorManualDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const snapshot = this.snapshotValue(inputArg.snapshot || metadata.snapshot);
    const hasEvents = this.hasEvents(inputArg.events || inputArg.eventLines || metadata.events || metadata.eventLines);
    const host = inputArg.host || snapshot?.monitor.host;
    const text = this.text(inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, metadata.manufacturer, metadata.model, metadata.name, snapshot?.monitor.manufacturer, snapshot?.monitor.model, snapshot?.monitor.name);
    const matched = inputArg.integrationDomain === fritzboxCallmonitorDomain
      || metadata.fritzbox_callmonitor === true
      || metadata.callMonitor === true
      || Boolean(snapshot)
      || hasEvents
      || fritzTextHints.some((hintArg) => text.includes(hintArg))
      || Boolean(host && !text);

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain FRITZ!Box Call Monitor setup hints.' };
    }

    const port = inputArg.port || snapshot?.monitor.port || fritzboxCallmonitorDefaultPort;
    const id = inputArg.id || inputArg.serialNumber || snapshot?.monitor.serialNumber || snapshot?.monitor.id || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: snapshot || hasEvents || inputArg.serialNumber ? 'certain' : host ? 'high' : 'medium',
      reason: snapshot ? 'Manual entry includes a FRITZ!Box Call Monitor snapshot.' : hasEvents ? 'Manual entry includes FRITZ!Box call monitor event lines.' : 'Manual entry can start FRITZ!Box Call Monitor setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: fritzboxCallmonitorDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.monitor.name || snapshot?.monitor.phonebookName || 'FRITZ!Box Call Monitor',
        manufacturer: inputArg.manufacturer || snapshot?.monitor.manufacturer || fritzboxCallmonitorManufacturer,
        model: inputArg.model || snapshot?.monitor.model || 'FRITZ!Box Call Monitor',
        serialNumber: inputArg.serialNumber || snapshot?.monitor.serialNumber,
        macAddress: inputArg.macAddress,
        metadata: {
          ...metadata,
          snapshot,
          hasEvents,
          fritzbox_callmonitor: true,
          tcpPort: port,
          upstreamSupportsSsdp: false,
          upstreamSupportsZeroconf: false,
          nativeTcpCallMonitor: true,
          phonebookApiImplemented: false,
        },
      },
      metadata: { hasEvents, upstreamSupportsSsdp: false, upstreamSupportsZeroconf: false, nativeTcpCallMonitor: true, phonebookApiImplemented: false },
    };
  }

  private snapshotValue(valueArg: unknown): IFritzboxCallmonitorSnapshot | undefined {
    return Boolean(valueArg && typeof valueArg === 'object' && 'monitor' in valueArg && 'call' in valueArg) ? valueArg as IFritzboxCallmonitorSnapshot : undefined;
  }

  private hasEvents(valueArg: unknown): boolean {
    return Array.isArray(valueArg) ? valueArg.length > 0 : typeof valueArg === 'string' && Boolean(valueArg.trim());
  }

  private text(...valuesArg: unknown[]): string {
    return valuesArg.filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
  }
}

export class FritzboxCallmonitorSsdpMatcher implements IDiscoveryMatcher<IFritzboxCallmonitorSsdpDiscoveryRecord> {
  public id = 'fritzbox-callmonitor-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize FRITZ!Box SSDP advertisements as local call monitor candidates without claiming upstream SSDP support.';

  public async matches(inputArg: IFritzboxCallmonitorSsdpDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const upnp = inputArg.upnp || {};
    const st = this.stringValue(inputArg.st || metadata.st || metadata.ssdpSt);
    const location = this.stringValue(inputArg.ssdpLocation || inputArg.ssdp_location || inputArg.location || metadata.ssdpLocation || metadata.location);
    const host = inputArg.host || this.hostFromLocation(location);
    const friendlyName = this.firstString(upnp.friendlyName, upnp.FriendlyName, upnp.friendly_name, metadata.friendlyName, inputArg.name);
    const modelName = this.firstString(upnp.modelName, upnp.ModelName, upnp.model_name, metadata.modelName, inputArg.model);
    const udn = this.firstString(upnp.UDN, upnp.udn, metadata.udn, inputArg.udn, inputArg.usn);
    const uuid = udn?.replace(/^uuid:/i, '').split('::')[0];
    const text = this.text(inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, friendlyName, modelName, st, metadata.manufacturer, metadata.model, metadata.name);
    const matched = inputArg.integrationDomain === fritzboxCallmonitorDomain
      || metadata.fritzbox_callmonitor === true
      || st === fritzSsdpSt
      || text.includes(fritzSsdpSt)
      || fritzTextHints.some((hintArg) => text.includes(hintArg));

    if (!matched || !host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'FRITZ!Box SSDP advertisement lacks a usable host.' : 'SSDP advertisement is not a FRITZ!Box Call Monitor candidate.',
      };
    }

    return {
      matched: true,
      confidence: st === fritzSsdpSt || uuid ? 'certain' : 'high',
      reason: 'SSDP advertisement identifies a FRITZ!Box; native call monitor setup can use TCP port 1012 if enabled on the router.',
      normalizedDeviceId: inputArg.id || uuid || `${host}:${fritzboxCallmonitorDefaultPort}`,
      candidate: {
        source: 'ssdp',
        integrationDomain: fritzboxCallmonitorDomain,
        id: inputArg.id || uuid || `${host}:${fritzboxCallmonitorDefaultPort}`,
        host,
        port: fritzboxCallmonitorDefaultPort,
        name: inputArg.name || friendlyName || 'FRITZ!Box Call Monitor',
        manufacturer: inputArg.manufacturer || fritzboxCallmonitorManufacturer,
        model: inputArg.model || modelName || 'FRITZ!Box',
        serialNumber: inputArg.serialNumber || uuid,
        macAddress: inputArg.macAddress,
        metadata: {
          ...metadata,
          fritzbox_callmonitor: true,
          ssdpSt: st,
          ssdpLocation: location,
          upnp,
          upstreamSupportsSsdp: false,
          upstreamSupportsZeroconf: false,
          nativeTcpCallMonitor: true,
          phonebookApiImplemented: false,
        },
      },
      metadata: { ssdpSt: st, upstreamSupportsSsdp: false, upstreamSupportsZeroconf: false, nativeTcpCallMonitor: true, phonebookApiImplemented: false },
    };
  }

  private firstString(...valuesArg: unknown[]): string | undefined {
    return valuesArg.find((valueArg): valueArg is string => typeof valueArg === 'string' && Boolean(valueArg.trim()))?.trim();
  }

  private stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
  }

  private text(...valuesArg: unknown[]): string {
    return valuesArg.filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
  }

  private hostFromLocation(locationArg?: string): string | undefined {
    if (!locationArg) {
      return undefined;
    }
    try {
      const host = new URL(locationArg).hostname;
      return host && !host.toLowerCase().startsWith('fe80') ? host : undefined;
    } catch {
      return undefined;
    }
  }
}

export class FritzboxCallmonitorMdnsMatcher implements IDiscoveryMatcher<IFritzboxCallmonitorMdnsDiscoveryRecord> {
  public id = 'fritzbox-callmonitor-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize supplied FRITZ!Box mDNS candidates as call monitor setup candidates without claiming upstream zeroconf support.';

  public async matches(inputArg: IFritzboxCallmonitorMdnsDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const text = this.text(inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, inputArg.serviceType, inputArg.type, inputArg.fullname, metadata.manufacturer, metadata.model, metadata.name, metadata.serviceType);
    const matched = inputArg.integrationDomain === fritzboxCallmonitorDomain
      || metadata.fritzbox_callmonitor === true
      || fritzTextHints.some((hintArg) => text.includes(hintArg));

    if (!matched || !inputArg.host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'FRITZ!Box mDNS candidate lacks a usable host.' : 'mDNS candidate is not a FRITZ!Box Call Monitor candidate.',
      };
    }

    const id = inputArg.id || inputArg.serialNumber || `${inputArg.host}:${fritzboxCallmonitorDefaultPort}`;
    return {
      matched: true,
      confidence: inputArg.serialNumber || inputArg.macAddress ? 'certain' : 'high',
      reason: 'mDNS/zeroconf candidate contains FRITZ!Box identity hints; call monitor uses TCP port 1012 if enabled.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: fritzboxCallmonitorDomain,
        id,
        host: inputArg.host,
        port: fritzboxCallmonitorDefaultPort,
        name: inputArg.name || 'FRITZ!Box Call Monitor',
        manufacturer: inputArg.manufacturer || fritzboxCallmonitorManufacturer,
        model: inputArg.model || 'FRITZ!Box',
        serialNumber: inputArg.serialNumber,
        macAddress: inputArg.macAddress,
        metadata: {
          ...metadata,
          fritzbox_callmonitor: true,
          upstreamSupportsSsdp: false,
          upstreamSupportsZeroconf: false,
          nativeTcpCallMonitor: true,
          phonebookApiImplemented: false,
        },
      },
      metadata: { upstreamSupportsSsdp: false, upstreamSupportsZeroconf: false, nativeTcpCallMonitor: true, phonebookApiImplemented: false },
    };
  }

  private text(...valuesArg: unknown[]): string {
    return valuesArg.filter((valueArg): valueArg is string => typeof valueArg === 'string').join(' ').toLowerCase();
  }
}

export class FritzboxCallmonitorCandidateValidator implements IDiscoveryValidator {
  public id = 'fritzbox-callmonitor-candidate-validator';
  public description = 'Validate FRITZ!Box Call Monitor candidates have a host, snapshot, or manual event data.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const snapshot = metadata.snapshot as IFritzboxCallmonitorSnapshot | undefined;
    const hasEvents = Array.isArray(metadata.events) || Array.isArray(metadata.eventLines) || typeof metadata.events === 'string' || typeof metadata.eventLines === 'string';
    const text = [candidateArg.integrationDomain, candidateArg.manufacturer, candidateArg.model, candidateArg.name, metadata.manufacturer, metadata.model, metadata.name, metadata.ssdpSt]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const matched = candidateArg.integrationDomain === fritzboxCallmonitorDomain
      || metadata.fritzbox_callmonitor === true
      || Boolean(snapshot)
      || hasEvents
      || fritzTextHints.some((hintArg) => text.includes(hintArg));
    const hasUsableSource = Boolean(candidateArg.host || snapshot || hasEvents);

    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'FRITZ!Box Call Monitor candidate lacks host, snapshot, or event data.' : 'Candidate is not FRITZ!Box Call Monitor.',
      };
    }

    const port = candidateArg.port || snapshot?.monitor.port || fritzboxCallmonitorDefaultPort;
    const normalizedDeviceId = candidateArg.id || snapshot?.monitor.serialNumber || snapshot?.monitor.id || (candidateArg.host ? `${candidateArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: snapshot || hasEvents || candidateArg.serialNumber ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has FRITZ!Box Call Monitor metadata and a usable local source.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        id: candidateArg.id || normalizedDeviceId,
        port,
        metadata: {
          ...metadata,
          fritzbox_callmonitor: true,
          upstreamSupportsSsdp: false,
          upstreamSupportsZeroconf: false,
          nativeTcpCallMonitor: true,
          phonebookApiImplemented: false,
        },
      },
      metadata: { upstreamSupportsSsdp: false, upstreamSupportsZeroconf: false, nativeTcpCallMonitor: true, phonebookApiImplemented: false },
    };
  }
}

export const createFritzboxCallmonitorDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: fritzboxCallmonitorDomain, displayName: 'FRITZ!Box Call Monitor' })
    .addMatcher(new FritzboxCallmonitorManualMatcher())
    .addMatcher(new FritzboxCallmonitorSsdpMatcher())
    .addMatcher(new FritzboxCallmonitorMdnsMatcher())
    .addValidator(new FritzboxCallmonitorCandidateValidator());
};
