import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { normalizeMac } from './fully_kiosk.classes.client.js';
import type { IFullyKioskDhcpEntry, IFullyKioskDeviceInfo, IFullyKioskManualEntry, IFullyKioskSnapshot, TFullyKioskProtocol } from './fully_kiosk.types.js';
import { fullyKioskDefaultPort, fullyKioskDomain } from './fully_kiosk.types.js';

export class FullyKioskManualMatcher implements IDiscoveryMatcher<IFullyKioskManualEntry> {
  public id = 'fully-kiosk-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Fully Kiosk Browser host, URL, snapshot, or injected client entries.';

  public async matches(inputArg: IFullyKioskManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const endpoint = endpointFromManual(inputArg);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const deviceInfo = deviceInfoValue(inputArg.deviceInfo || metadata.deviceInfo);
    const macAddress = normalizeMac(inputArg.macAddress || stringMetadata(metadata, 'macAddress') || stringMetadata(metadata, 'macaddress') || snapshot?.macAddress || snapshot?.deviceInfo.Mac || deviceInfo?.Mac);
    const hasManualData = Boolean(snapshot || deviceInfo || inputArg.settings || inputArg.client || metadata.client);
    const matched = Boolean(endpoint.host || hasManualData || isFullyKioskHint(inputArg) || isFullyKioskHint({ ...inputArg, metadata }));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Fully Kiosk setup hints.' };
    }

    const id = inputArg.id || snapshot?.uniqueId || snapshot?.deviceInfo.deviceID || snapshot?.deviceInfo.deviceId || deviceInfo?.deviceID || macAddress || (endpoint.host ? `${endpoint.host}:${endpoint.port || fullyKioskDefaultPort}` : undefined);
    return {
      matched: true,
      confidence: endpoint.host || hasManualData ? 'high' : 'medium',
      reason: endpoint.host ? 'Manual entry contains a local Fully Kiosk REST endpoint.' : 'Manual entry contains Fully Kiosk metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: fullyKioskDomain,
        id,
        host: endpoint.host,
        port: endpoint.port || fullyKioskDefaultPort,
        name: inputArg.name || snapshot?.name || snapshot?.deviceInfo.deviceName || deviceInfo?.deviceName || endpoint.host,
        manufacturer: inputArg.manufacturer || snapshot?.deviceInfo.deviceManufacturer || deviceInfo?.deviceManufacturer || 'Fully Kiosk',
        model: inputArg.model || snapshot?.deviceInfo.deviceModel || deviceInfo?.deviceModel,
        macAddress,
        metadata: {
          ...metadata,
          fullyKiosk: true,
          fully_kiosk: true,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
          password: inputArg.password || stringMetadata(metadata, 'password'),
          snapshot,
          deviceInfo,
          settings: inputArg.settings || metadata.settings,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
        manualSupported: true,
      },
    };
  }
}

export class FullyKioskDhcpMatcher implements IDiscoveryMatcher<IFullyKioskDhcpEntry> {
  public id = 'fully-kiosk-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize DHCP updates for previously known Fully Kiosk devices or DHCP entries carrying Fully Kiosk hints.';

  public async matches(inputArg: IFullyKioskDhcpEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const host = inputArg.ip || inputArg.ipAddress || inputArg.host;
    const macAddress = normalizeMac(inputArg.macAddress || inputArg.macaddress || stringMetadata(metadata, 'macAddress') || stringMetadata(metadata, 'macaddress'));
    const registered = inputArg.registeredDevices === true || inputArg.registered_devices === true || metadata.registeredDevices === true || metadata.registered_devices === true;
    const hinted = isFullyKioskHint(inputArg) || isFullyKioskHint({ ...inputArg, metadata });
    if (!host || !macAddress || (!registered && !hinted)) {
      return {
        matched: false,
        confidence: hinted || registered ? 'medium' : 'low',
        reason: 'DHCP entry lacks a Fully Kiosk hint, registered-device marker, host, or MAC address.',
        normalizedDeviceId: macAddress,
      };
    }

    return {
      matched: true,
      confidence: hinted ? 'high' : 'medium',
      reason: hinted ? 'DHCP entry carries Fully Kiosk metadata.' : 'DHCP registered-device entry can update an existing Fully Kiosk host by MAC address.',
      normalizedDeviceId: macAddress,
      candidate: {
        source: 'dhcp',
        integrationDomain: fullyKioskDomain,
        id: macAddress,
        host,
        port: numberMetadata(metadata, 'port') || fullyKioskDefaultPort,
        name: inputArg.name || inputArg.hostname || host,
        manufacturer: inputArg.manufacturer || 'Fully Kiosk',
        model: inputArg.model,
        macAddress,
        metadata: {
          ...metadata,
          fullyKiosk: true,
          fully_kiosk: true,
          registeredDevices: registered,
          discoveryProtocol: 'dhcp',
        },
      },
    };
  }
}

export class FullyKioskCandidateValidator implements IDiscoveryValidator {
  public id = 'fully-kiosk-candidate-validator';
  public description = 'Validate Fully Kiosk candidates have manual/DHCP metadata and a local setup source.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== fullyKioskDomain) {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not Fully Kiosk.` };
    }
    const metadata = candidateArg.metadata || {};
    const endpoint = endpointFromCandidate(candidateArg);
    const snapshot = snapshotValue(metadata.snapshot);
    const deviceInfo = deviceInfoValue(metadata.deviceInfo);
    const macAddress = normalizeMac(candidateArg.macAddress || candidateArg.id || stringMetadata(metadata, 'macAddress') || stringMetadata(metadata, 'macaddress') || snapshot?.macAddress || snapshot?.deviceInfo.Mac || deviceInfo?.Mac);
    const matched = candidateArg.integrationDomain === fullyKioskDomain || isFullyKioskHint(candidateArg) || Boolean(snapshot || deviceInfo || metadata.client);
    const hasSource = Boolean(endpoint.host || snapshot || deviceInfo || metadata.client);

    if (!matched || !hasSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Fully Kiosk candidate lacks a host, injected client, snapshot, or device info.' : 'Candidate is not Fully Kiosk.',
        normalizedDeviceId: candidateArg.id || macAddress,
      };
    }
    if (endpoint.port !== undefined && !validPort(endpoint.port)) {
      return { matched: false, confidence: 'low', reason: 'Fully Kiosk candidate has an invalid port.' };
    }

    const id = candidateArg.id || snapshot?.uniqueId || snapshot?.deviceInfo.deviceID || snapshot?.deviceInfo.deviceId || deviceInfo?.deviceID || macAddress || (endpoint.host ? `${endpoint.host}:${endpoint.port || fullyKioskDefaultPort}` : undefined);
    return {
      matched: true,
      confidence: id && endpoint.host ? 'certain' : endpoint.host ? 'high' : 'medium',
      reason: 'Candidate has Fully Kiosk metadata and a local endpoint, client, snapshot, or device info.',
      normalizedDeviceId: id,
      candidate: {
        ...candidateArg,
        integrationDomain: fullyKioskDomain,
        id,
        host: endpoint.host,
        port: endpoint.port || fullyKioskDefaultPort,
        manufacturer: candidateArg.manufacturer || snapshot?.deviceInfo.deviceManufacturer || deviceInfo?.deviceManufacturer || 'Fully Kiosk',
        macAddress: macAddress || candidateArg.macAddress,
        metadata: {
          ...metadata,
          fullyKiosk: true,
          fully_kiosk: true,
          protocol: endpoint.protocol,
          ssl: endpoint.protocol === 'https',
          url: endpoint.url,
        },
      },
    };
  }
}

export const createFullyKioskDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: fullyKioskDomain, displayName: 'Fully Kiosk Browser' })
    .addMatcher(new FullyKioskManualMatcher())
    .addMatcher(new FullyKioskDhcpMatcher())
    .addValidator(new FullyKioskCandidateValidator());
};

const endpointFromManual = (inputArg: IFullyKioskManualEntry): { protocol: TFullyKioskProtocol; host?: string; port?: number; url?: string } => {
  const parsed = parseEndpoint(inputArg.url || inputArg.host);
  if (parsed) {
    return parsed;
  }
  const protocol: TFullyKioskProtocol = inputArg.protocol || (inputArg.ssl ? 'https' : 'http');
  const port = inputArg.port || fullyKioskDefaultPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${inputArg.host}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TFullyKioskProtocol; host?: string; port?: number; url?: string } => {
  const metadata = candidateArg.metadata || {};
  const parsed = parseEndpoint(stringMetadata(metadata, 'url') || candidateArg.host);
  if (parsed) {
    return parsed;
  }
  const protocol: TFullyKioskProtocol = stringMetadata(metadata, 'protocol') === 'https' || metadata.ssl === true ? 'https' : 'http';
  const port = candidateArg.port || fullyKioskDefaultPort;
  return { protocol, host: candidateArg.host, port, url: candidateArg.host ? `${protocol}://${candidateArg.host}:${port}` : undefined };
};

const parseEndpoint = (valueArg: string | undefined): { protocol: TFullyKioskProtocol; host: string; port: number; url: string } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    const protocol: TFullyKioskProtocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : fullyKioskDefaultPort;
    return { protocol, host: url.hostname, port, url: `${protocol}://${url.hostname}:${port}` };
  } catch {
    return undefined;
  }
};

const snapshotValue = (valueArg: unknown): IFullyKioskSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'deviceInfo' in valueArg && 'settings' in valueArg ? valueArg as IFullyKioskSnapshot : undefined;
};

const deviceInfoValue = (valueArg: unknown): IFullyKioskDeviceInfo | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as IFullyKioskDeviceInfo : undefined;
};

const isFullyKioskHint = (valueArg: { integrationDomain?: string; manufacturer?: string; model?: string; name?: string; metadata?: Record<string, unknown> }): boolean => {
  const text = [valueArg.manufacturer, valueArg.model, valueArg.name, valueArg.metadata?.manufacturer, valueArg.metadata?.model, valueArg.metadata?.name].filter(Boolean).join(' ').toLowerCase();
  return valueArg.integrationDomain === fullyKioskDomain
    || text.includes('fully kiosk')
    || text.includes('fully browser')
    || valueArg.metadata?.fullyKiosk === true
    || valueArg.metadata?.fully_kiosk === true;
};

const stringMetadata = (metadataArg: Record<string, unknown>, keyArg: string): string | undefined => {
  const value = metadataArg[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
};

const numberMetadata = (metadataArg: Record<string, unknown>, keyArg: string): number | undefined => {
  const value = metadataArg[keyArg];
  if (typeof value === 'number' && Number.isFinite(value)) {
    return Math.round(value);
  }
  if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {
    return Math.round(Number(value));
  }
  return undefined;
};

const validPort = (valueArg: number): boolean => Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
