import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { HddtempMapper } from './hddtemp.mapper.js';
import type { IHddtempManualEntry, IHddtempSnapshot, IHddtempTcpDiscoveryRecord, THddtempRawData } from './hddtemp.types.js';
import { hddtempDefaultName, hddtempDefaultPort, hddtempDisplayName, hddtempDomain } from './hddtemp.types.js';

export class HddtempManualMatcher implements IDiscoveryMatcher<IHddtempManualEntry> {
  public id = 'hddtemp-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual hddtemp daemon host, disk, raw data, snapshot, and injected client setup entries.';

  public async matches(inputArg: IHddtempManualEntry, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const endpoint = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || inputArg.rawText || inputArg.rawDisks || inputArg.raw || metadata.rawData || metadata.rawText || metadata.banner);
    const hasData = Boolean(snapshot || rawData || inputArg.client || metadata.client);
    const haystack = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.url || inputArg.port === hddtempDefaultPort || metadata.hddtemp || hasData || haystack.includes('hddtemp') || haystack.includes('hd temperature') || haystack.includes('hard disk temperature'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain hddtemp setup hints.' };
    }

    const host = endpoint?.host || (inputArg.host && !inputArg.host.includes('://') ? inputArg.host : undefined) || snapshot?.daemon.host;
    const port = inputArg.port || endpoint?.port || snapshot?.daemon.port || hddtempDefaultPort;
    const id = inputArg.id || snapshot?.daemon.id || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: hasData || (host && port === hddtempDefaultPort) ? 'high' : 'medium',
      reason: 'Manual entry can start hddtemp setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: hddtempDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.daemon.name || hddtempDefaultName,
        manufacturer: inputArg.manufacturer || hddtempDisplayName,
        model: inputArg.model || 'hddtemp daemon',
        metadata: {
          ...metadata,
          hddtemp: true,
          discoveryProtocol: 'manual',
          disks: inputArg.disks || metadata.disks || snapshot?.daemon.configuredDisks,
          snapshot,
          rawData,
          client: inputArg.client || metadata.client,
        },
      },
    };
  }
}

export class HddtempTcpMatcher implements IDiscoveryMatcher<IHddtempTcpDiscoveryRecord> {
  public id = 'hddtemp-tcp-match';
  public source = 'custom' as const;
  public description = 'Recognize hddtemp daemon TCP endpoints by the default port or daemon output records.';

  public async matches(inputArg: IHddtempTcpDiscoveryRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const rawData = rawDataValue(inputArg.rawData || inputArg.banner || metadata.rawData || metadata.banner);
    const parsedDisks = rawData === undefined ? [] : HddtempMapper.parseRawData(rawData);
    const port = inputArg.port || numberValue(metadata.port) || hddtempDefaultPort;
    const haystack = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(parsedDisks.length || (inputArg.host && port === hddtempDefaultPort) || metadata.hddtemp || haystack.includes('hddtemp'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'TCP record is not an hddtemp daemon candidate.' };
    }

    const id = inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : parsedDisks[0]?.device);
    return {
      matched: true,
      confidence: parsedDisks.length && inputArg.host ? 'certain' : parsedDisks.length || inputArg.host ? 'high' : 'medium',
      reason: parsedDisks.length ? 'TCP record includes hddtemp daemon output.' : 'TCP record uses the hddtemp daemon default port.',
      normalizedDeviceId: id,
      candidate: {
        source: 'custom',
        integrationDomain: hddtempDomain,
        id,
        host: inputArg.host,
        port,
        name: inputArg.name || hddtempDefaultName,
        manufacturer: inputArg.manufacturer || hddtempDisplayName,
        model: inputArg.model || 'hddtemp daemon',
        metadata: {
          ...metadata,
          hddtemp: true,
          discoveryProtocol: 'tcp',
          rawData,
          disks: parsedDisks.map((diskArg) => diskArg.device),
        },
      },
    };
  }
}

export class HddtempCandidateValidator implements IDiscoveryValidator {
  public id = 'hddtemp-candidate-validator';
  public description = 'Validate hddtemp candidates have a host, raw daemon data, snapshot, or injected client.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData || metadata.rawText || metadata.banner);
    const haystack = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === hddtempDomain
      || candidateArg.port === hddtempDefaultPort
      || metadata.hddtemp === true
      || haystack.includes('hddtemp')
      || haystack.includes('hd temperature')
      || haystack.includes('hard disk temperature');
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.client);

    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'hddtemp candidate lacks a host, injected client, snapshot, or raw data.' : 'Candidate is not hddtemp.',
      };
    }

    const port = candidateArg.port || snapshot?.daemon.port || hddtempDefaultPort;
    const normalizedDeviceId = candidateArg.id || snapshot?.daemon.id || (candidateArg.host ? `${candidateArg.host}:${port}` : undefined);
    return {
      matched: true,
      confidence: normalizedDeviceId ? 'certain' : 'high',
      reason: 'Candidate has hddtemp metadata and a usable source.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: hddtempDomain,
        port,
        manufacturer: candidateArg.manufacturer || hddtempDisplayName,
      },
    };
  }
}

export const createHddtempDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: hddtempDomain, displayName: hddtempDisplayName })
    .addMatcher(new HddtempManualMatcher())
    .addMatcher(new HddtempTcpMatcher())
    .addValidator(new HddtempCandidateValidator());
};

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

const snapshotValue = (valueArg: unknown): IHddtempSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'daemon' in valueArg && 'disks' in valueArg ? valueArg as IHddtempSnapshot : undefined;
};

const rawDataValue = (valueArg: unknown): THddtempRawData | undefined => {
  if (typeof valueArg === 'string' && valueArg.trim()) {
    return valueArg;
  }
  if (Array.isArray(valueArg)) {
    return valueArg as THddtempRawData;
  }
  return undefined;
};

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Math.round(Number(valueArg));
  }
  return undefined;
};
