import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { FritzMapper } from './fritz.mapper.js';
import type { IFritzManualDiscoveryRecord, IFritzMdnsDiscoveryRecord, IFritzSnapshot, IFritzSsdpDiscoveryRecord } from './fritz.types.js';
import { fritzDefaultSsl, fritzDomain } from './fritz.types.js';

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

export class FritzManualMatcher implements IDiscoveryMatcher<IFritzManualDiscoveryRecord> {
  public id = 'fritz-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual FRITZ!Box Tools setup entries, including snapshot-only records.';

  public async matches(inputArg: IFritzManualDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot || metadata.snapshot as IFritzSnapshot | undefined;
    const host = inputArg.host || snapshot?.router.host;
    const mac = FritzMapper.normalizeMac(inputArg.macAddress || inputArg.serialNumber || snapshot?.router.macAddress || snapshot?.router.serialNumber);
    const text = this.text(inputArg.integrationDomain, inputArg.manufacturer, inputArg.model, inputArg.name, metadata.manufacturer, metadata.model, metadata.name, snapshot?.router.manufacturer, snapshot?.router.model, snapshot?.router.name);
    const hasSnapshot = Boolean(snapshot);
    const matched = inputArg.integrationDomain === fritzDomain
      || metadata.fritz === true
      || hasSnapshot
      || fritzTextHints.some((hintArg) => text.includes(hintArg))
      || Boolean(host && !text);

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

    const ssl = this.booleanValue(inputArg.ssl) ?? this.booleanValue(metadata.ssl) ?? snapshot?.router.ssl ?? fritzDefaultSsl;
    const port = inputArg.port || snapshot?.router.port || FritzMapper.defaultPort(ssl);
    const id = inputArg.id || inputArg.serialNumber || snapshot?.router.serialNumber || mac || snapshot?.router.id || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: hasSnapshot || mac ? 'certain' : host ? 'high' : 'medium',
      reason: hasSnapshot ? 'Manual entry includes a FRITZ!Box snapshot.' : 'Manual entry can start FRITZ!Box Tools setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: fritzDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.router.name || snapshot?.router.model || host || 'FRITZ!Box',
        manufacturer: inputArg.manufacturer || snapshot?.router.manufacturer || 'FRITZ!',
        model: inputArg.model || snapshot?.router.model || 'FRITZ!Box',
        serialNumber: inputArg.serialNumber || snapshot?.router.serialNumber,
        macAddress: mac,
        metadata: {
          ...metadata,
          fritz: true,
          ssl,
          hasSnapshot,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: false,
          liveTr064Implemented: false,
        },
      },
      metadata: { hasSnapshot, ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: false, liveTr064Implemented: false },
    };
  }

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

  private booleanValue(valueArg: unknown): boolean | undefined {
    return typeof valueArg === 'boolean' ? valueArg : undefined;
  }
}

export class FritzSsdpMatcher implements IDiscoveryMatcher<IFritzSsdpDiscoveryRecord> {
  public id = 'fritz-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Home Assistant supported FRITZ!Box SSDP advertisements.';

  public async matches(inputArg: IFritzSsdpDiscoveryRecord): 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, upnp['upnp:ATTR_UPNP_FRIENDLY_NAME'], upnp['friendlyName'], metadata.friendlyName, inputArg.name);
    const modelName = this.firstString(upnp.modelName, upnp.ModelName, upnp.model_name, upnp['upnp:ATTR_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 === fritzDomain
      || metadata.fritz === 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 FRITZ!Box Tools.',
      };
    }

    const ssl = this.booleanValue(metadata.ssl) ?? fritzDefaultSsl;
    const port = inputArg.port || this.portFromLocation(location) || FritzMapper.defaultPort(ssl);
    return {
      matched: true,
      confidence: st === fritzSsdpSt || uuid ? 'certain' : 'high',
      reason: 'SSDP advertisement matches the FRITZ!Box device type supported by Home Assistant.',
      normalizedDeviceId: inputArg.id || uuid || `${host}:${port}`,
      candidate: {
        source: 'ssdp',
        integrationDomain: fritzDomain,
        id: inputArg.id || uuid || `${host}:${port}`,
        host,
        port,
        name: inputArg.name || friendlyName || modelName || 'FRITZ!Box',
        manufacturer: inputArg.manufacturer || 'FRITZ!',
        model: inputArg.model || modelName || 'FRITZ!Box',
        serialNumber: uuid,
        metadata: {
          ...metadata,
          fritz: true,
          ssl,
          ssdpSt: st,
          ssdpLocation: location,
          upnp,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: false,
          liveTr064Implemented: false,
        },
      },
      metadata: { ssl, ssdpSt: st, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: false, liveTr064Implemented: 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 booleanValue(valueArg: unknown): boolean | undefined {
    return typeof valueArg === 'boolean' ? valueArg : undefined;
  }

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

  private portFromLocation(locationArg?: string): number | undefined {
    if (!locationArg) {
      return undefined;
    }
    try {
      const parsed = Number(new URL(locationArg).port);
      return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
    } catch {
      return undefined;
    }
  }
}

export class FritzMdnsMatcher implements IDiscoveryMatcher<IFritzMdnsDiscoveryRecord> {
  public id = 'fritz-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize zeroconf/mDNS FRITZ candidates when supplied by a local discovery layer.';

  public async matches(inputArg: IFritzMdnsDiscoveryRecord): 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 === fritzDomain
      || metadata.fritz === true
      || fritzTextHints.some((hintArg) => text.includes(hintArg));

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

    const ssl = this.booleanValue(metadata.ssl) ?? fritzDefaultSsl;
    const id = inputArg.id || inputArg.serialNumber || FritzMapper.normalizeMac(inputArg.macAddress) || `${inputArg.host}:${inputArg.port || FritzMapper.defaultPort(ssl)}`;
    return {
      matched: true,
      confidence: inputArg.serialNumber || inputArg.macAddress ? 'certain' : 'high',
      reason: 'mDNS/zeroconf candidate contains FRITZ!Box identity hints.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: fritzDomain,
        id,
        host: inputArg.host,
        port: inputArg.port || FritzMapper.defaultPort(ssl),
        name: inputArg.name || inputArg.model || 'FRITZ!Box',
        manufacturer: inputArg.manufacturer || 'FRITZ!',
        model: inputArg.model || 'FRITZ!Box',
        serialNumber: inputArg.serialNumber,
        macAddress: FritzMapper.normalizeMac(inputArg.macAddress),
        metadata: {
          ...metadata,
          fritz: true,
          ssl,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: false,
          liveTr064Implemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: false, liveTr064Implemented: false },
    };
  }

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

  private booleanValue(valueArg: unknown): boolean | undefined {
    return typeof valueArg === 'boolean' ? valueArg : undefined;
  }
}

export class FritzCandidateValidator implements IDiscoveryValidator {
  public id = 'fritz-candidate-validator';
  public description = 'Validate FRITZ candidates have a host or snapshot and FRITZ!Box identity metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const snapshot = metadata.snapshot as IFritzSnapshot | undefined;
    const mac = FritzMapper.normalizeMac(candidateArg.macAddress || snapshot?.router.macAddress || snapshot?.router.serialNumber);
    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 === fritzDomain
      || metadata.fritz === true
      || Boolean(snapshot)
      || metadata.ssdpSt === fritzSsdpSt
      || fritzTextHints.some((hintArg) => text.includes(hintArg));
    const hasUsableSource = Boolean(candidateArg.host || snapshot);

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

    const ssl = typeof metadata.ssl === 'boolean' ? metadata.ssl : snapshot?.router.ssl ?? fritzDefaultSsl;
    const port = candidateArg.port || snapshot?.router.port || FritzMapper.defaultPort(ssl);
    const normalizedDeviceId = candidateArg.id || snapshot?.router.serialNumber || mac || (candidateArg.host ? `${candidateArg.host}:${port}` : snapshot?.router.id);
    return {
      matched: true,
      confidence: mac || snapshot || metadata.ssdpSt === fritzSsdpSt ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has FRITZ!Box metadata and a usable local source.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        id: candidateArg.id || normalizedDeviceId,
        port,
        macAddress: mac || candidateArg.macAddress,
        metadata: {
          ...metadata,
          ssl,
          upstreamSupportsSsdp: true,
          upstreamSupportsZeroconf: false,
          liveTr064Implemented: false,
        },
      },
      metadata: { ssl, upstreamSupportsSsdp: true, upstreamSupportsZeroconf: false, liveTr064Implemented: false },
    };
  }
}

export const createFritzDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: fritzDomain, displayName: 'FRITZ!Box Tools' })
    .addMatcher(new FritzManualMatcher())
    .addMatcher(new FritzSsdpMatcher())
    .addMatcher(new FritzMdnsMatcher())
    .addValidator(new FritzCandidateValidator());
};
