import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IFroniusDhcpRecord, IFroniusManualEntry, IFroniusRawData, IFroniusSnapshot } from './fronius.types.js';
import { froniusDefaultHttpPort, froniusDhcpMacPrefix, froniusDisplayName, froniusDomain, froniusManufacturer } from './fronius.types.js';

export class FroniusDhcpMatcher implements IDiscoveryMatcher<IFroniusDhcpRecord> {
  public id = 'fronius-dhcp-match';
  public source = 'dhcp' as const;
  public description = 'Recognize Fronius DHCP leases by the Home Assistant manifest MAC prefix 0003AC* and Fronius host metadata.';

  public async matches(recordArg: IFroniusDhcpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = recordArg.metadata || {};
    const mac = normalizeMac(recordArg.macaddress || recordArg.macAddress || metadata.macAddress || metadata.macaddress);
    const hostname = stringValue(recordArg.hostname) || stringValue(recordArg.hostName) || stringValue(metadata.hostname) || '';
    const text = [hostname, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(mac?.replace(/:/g, '').toUpperCase().startsWith(froniusDhcpMacPrefix) || metadata.fronius === true || text.includes('fronius') || text.includes('solarnet') || text.includes('solarweb'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP lease does not match Fronius MAC prefix 0003AC* or Fronius host metadata.' };
    }

    const host = stringValue(recordArg.ip) || stringValue(recordArg.address) || stringValue(metadata.ip) || stringValue(metadata.address);
    const id = recordArg.id || mac || hostname || host;
    return {
      matched: true,
      confidence: host && mac ? 'high' : 'medium',
      reason: mac?.replace(/:/g, '').toUpperCase().startsWith(froniusDhcpMacPrefix) ? 'DHCP MAC address matches Home Assistant Fronius manifest prefix 0003AC*.' : 'DHCP lease contains Fronius metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: froniusDomain,
        id,
        host,
        port: froniusDefaultHttpPort,
        name: hostname || froniusDisplayName,
        manufacturer: froniusManufacturer,
        macAddress: mac,
        metadata: {
          ...metadata,
          fronius: true,
          discoveryProtocol: 'dhcp',
          dhcpMacPrefix: `${froniusDhcpMacPrefix}*`,
          hostname,
        },
      },
    };
  }
}

export class FroniusManualMatcher implements IDiscoveryMatcher<IFroniusManualEntry> {
  public id = 'fronius-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Fronius host, snapshot, raw-data, injected-client, and command-executor setup entries.';

  public async matches(inputArg: IFroniusManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host || stringValue(metadata.url) || stringValue(metadata.host));
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData);
    const hasManualData = Boolean(snapshot || rawData || inputArg.client || metadata.client);
    const text = [inputArg.name, inputArg.manufacturer, inputArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = Boolean(inputArg.host || inputArg.url || metadata.fronius === true || hasManualData || text.includes('fronius') || text.includes('solarnet') || text.includes('solarweb'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Fronius setup hints.' };
    }

    const host = parsed?.host || inputArg.host || snapshot?.host || stringValue(metadata.host);
    const port = inputArg.port || parsed?.port || snapshot?.port || numberValue(metadata.port) || froniusDefaultHttpPort;
    const id = inputArg.id || inputArg.uniqueId || inputArg.serialNumber || snapshot?.system.uniqueId || snapshot?.system.serialNumber || (host ? `${host}:${port}` : undefined);
    const isLogger = inputArg.isLogger ?? (snapshot?.loggerInfo ? true : metadata.isLogger);
    return {
      matched: true,
      confidence: host || hasManualData ? 'high' : 'medium',
      reason: 'Manual entry can start Fronius setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: froniusDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.system.name || froniusDisplayName,
        manufacturer: inputArg.manufacturer || snapshot?.system.manufacturer || froniusManufacturer,
        model: inputArg.model || snapshot?.system.model,
        serialNumber: inputArg.serialNumber || snapshot?.system.serialNumber,
        metadata: {
          ...metadata,
          fronius: true,
          discoveryProtocol: 'manual',
          protocol: inputArg.protocol || parsed?.protocol || snapshot?.protocol || metadata.protocol,
          isLogger,
          snapshot,
          rawData,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
        },
      },
    };
  }
}

export class FroniusCandidateValidator implements IDiscoveryValidator {
  public id = 'fronius-candidate-validator';
  public description = 'Validate Fronius candidates from DHCP and manual setup.';

  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);
    const mac = normalizeMac(candidateArg.macAddress || metadata.macAddress || metadata.macaddress);
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.name, metadata.manufacturer, metadata.model].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === froniusDomain
      || metadata.fronius === true
      || Boolean(mac?.replace(/:/g, '').toUpperCase().startsWith(froniusDhcpMacPrefix))
      || text.includes('fronius')
      || text.includes('solarnet')
      || text.includes('solarweb');
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.client);
    const normalizedDeviceId = candidateArg.id || snapshot?.system.uniqueId || snapshot?.system.serialNumber || mac || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || froniusDefaultHttpPort}` : undefined);
    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Fronius candidate lacks a host, injected client, snapshot, or raw data.' : 'Candidate is not Fronius.',
        normalizedDeviceId,
      };
    }
    return {
      matched: true,
      confidence: candidateArg.host && normalizedDeviceId ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Fronius metadata and a usable local endpoint, client, snapshot, or raw data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: froniusDomain,
        port: candidateArg.port || froniusDefaultHttpPort,
        manufacturer: candidateArg.manufacturer || froniusManufacturer,
        metadata: {
          ...metadata,
          fronius: true,
        },
      },
    };
  }
}

export const createFroniusDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: froniusDomain, displayName: froniusDisplayName })
    .addMatcher(new FroniusDhcpMatcher())
    .addMatcher(new FroniusManualMatcher())
    .addValidator(new FroniusCandidateValidator());
};

const snapshotValue = (valueArg: unknown): IFroniusSnapshot | undefined => valueArg && typeof valueArg === 'object' && 'system' in valueArg && 'sensors' in valueArg ? valueArg as IFroniusSnapshot : undefined;

const rawDataValue = (valueArg: unknown): Partial<IFroniusRawData> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Partial<IFroniusRawData> : undefined;

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

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

const normalizeMac = (valueArg: unknown): string | undefined => {
  const normalized = typeof valueArg === 'string' ? valueArg.replace(/[^0-9a-f]/gi, '').toLowerCase() : undefined;
  return normalized && normalized.length === 12 ? normalized.match(/.{1,2}/g)?.join(':') : undefined;
};

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