import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import { ciscoIosDefaultName, ciscoIosDefaultSshPort, ciscoIosDisplayName, ciscoIosDomain, ciscoIosManufacturer } from './cisco_ios.constants.js';
import { CiscoIosMapper } from './cisco_ios.mapper.js';
import type { ICiscoIosCandidateMetadata, ICiscoIosDhcpRecord, ICiscoIosManualEntry } from './cisco_ios.types.js';

export class CiscoIosDhcpMatcher implements IDiscoveryMatcher<ICiscoIosDhcpRecord> {
  public id = 'cisco-ios-dhcp-explicit-match';
  public source = 'dhcp' as const;
  public description = 'Recognize explicit Cisco IOS DHCP candidates; generic Cisco DHCP records are not treated as certain Cisco IOS routers.';

  public async matches(recordArg: ICiscoIosDhcpRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = recordArg.metadata || {};
    const host = recordArg.host || recordArg.ipAddress || recordArg.address || recordArg.ip || stringValue(metadata.host);
    const hostname = recordArg.hostname || recordArg.hostName || stringValue(metadata.hostname);
    const macAddress = CiscoIosMapper.normalizeMac(recordArg.macAddress || recordArg.macaddress || recordArg.mac || stringValue(metadata.macAddress));
    const explicit = recordArg.integrationDomain === ciscoIosDomain || metadata.integrationDomain === ciscoIosDomain || metadata.domain === ciscoIosDomain || metadata.ciscoIos === true;
    const iosText = text(recordArg.manufacturer, recordArg.model, hostname, metadata.manufacturer, metadata.model).includes('cisco') && text(recordArg.model, metadata.model, metadata.platform, metadata.software).includes('ios');
    const matched = explicit || iosText;

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'DHCP record is not an explicit Cisco IOS candidate.' };
    }

    const id = macAddress || hostname || host;
    return {
      matched: true,
      confidence: explicit && host ? 'high' : host ? 'medium' : 'low',
      reason: explicit ? 'DHCP record explicitly targets the Cisco IOS integration.' : 'DHCP metadata mentions Cisco IOS.',
      normalizedDeviceId: id,
      candidate: {
        source: 'dhcp',
        integrationDomain: ciscoIosDomain,
        id,
        host,
        port: ciscoIosDefaultSshPort,
        name: hostname || ciscoIosDefaultName,
        manufacturer: recordArg.manufacturer || ciscoIosManufacturer,
        model: recordArg.model || ciscoIosDisplayName,
        macAddress,
        metadata: {
          ...metadata,
          ciscoIos: true,
          discoveryProtocol: 'dhcp-explicit',
          hostname,
          nativeSshImplemented: false,
          readOnly: true,
        } satisfies ICiscoIosCandidateMetadata,
      },
      metadata: { explicit, iosText },
    };
  }
}

export class CiscoIosManualMatcher implements IDiscoveryMatcher<ICiscoIosManualEntry> {
  public id = 'cisco-ios-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Cisco IOS host, injected SSH/CLI, snapshot, and show ip arp output entries.';

  public async matches(inputArg: ICiscoIosManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot || metadata.snapshot as ICiscoIosManualEntry['snapshot'];
    const arpOutput = inputArg.arpOutput || inputArg.rawArpOutput || stringValue(metadata.arpOutput) || stringValue(metadata.rawArpOutput);
    const host = inputArg.host || inputArg.ipAddress || inputArg.address || inputArg.ip || snapshot?.device.host || stringValue(metadata.host);
    const macAddress = CiscoIosMapper.normalizeMac(inputArg.macAddress || inputArg.mac || snapshot?.device.macAddress || stringValue(metadata.macAddress));
    const model = inputArg.modelName || inputArg.model || snapshot?.device.modelName || stringValue(metadata.modelName) || stringValue(metadata.model);
    const explicit = inputArg.integrationDomain === ciscoIosDomain || metadata.integrationDomain === ciscoIosDomain || metadata.domain === ciscoIosDomain || metadata.ciscoIos === true;
    const hostWithUsername = Boolean(host && (inputArg.username || metadata.username));
    const textMatched = text(inputArg.manufacturer, model, inputArg.name, metadata.manufacturer).includes('cisco') && text(model, metadata.platform, metadata.software).includes('ios');
    const matched = explicit || Boolean(snapshot) || Boolean(arpOutput) || hostWithUsername || textMatched;

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Cisco IOS setup data.' };
    }

    const id = inputArg.id || macAddress || snapshot?.device.id || host;
    return {
      matched: true,
      confidence: snapshot || arpOutput ? 'certain' : hostWithUsername ? 'high' : host ? 'medium' : 'low',
      reason: snapshot ? 'Manual entry includes a Cisco IOS snapshot.' : arpOutput ? 'Manual entry includes Cisco IOS show ip arp output.' : 'Manual entry can start Cisco IOS setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: ciscoIosDomain,
        id,
        host,
        port: inputArg.port || snapshot?.device.port || ciscoIosDefaultSshPort,
        name: inputArg.name || snapshot?.device.name || host || ciscoIosDefaultName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || ciscoIosManufacturer,
        model: model || ciscoIosDisplayName,
        macAddress,
        metadata: {
          ...metadata,
          ciscoIos: true,
          discoveryProtocol: 'manual',
          username: inputArg.username || stringValue(metadata.username),
          snapshot,
          arpOutput,
          nativeSshImplemented: false,
          readOnly: true,
        } satisfies ICiscoIosCandidateMetadata,
      },
      metadata: { snapshotConfigured: Boolean(snapshot), arpOutputConfigured: Boolean(arpOutput), hostWithUsername },
    };
  }
}

export class CiscoIosCandidateValidator implements IDiscoveryValidator {
  public id = 'cisco-ios-candidate-validator';
  public description = 'Validate Cisco IOS manual and explicit DHCP candidates without pretending SSH is implemented.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = metadata.snapshot as ICiscoIosManualEntry['snapshot'];
    const arpOutput = stringValue(metadata.arpOutput) || stringValue(metadata.rawArpOutput);
    const matched = candidateArg.integrationDomain === ciscoIosDomain || metadata.ciscoIos === true || Boolean(snapshot) || Boolean(arpOutput);
    const usable = Boolean(candidateArg.host || snapshot || arpOutput);

    if (!matched || !usable) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Cisco IOS candidate lacks host, snapshot, or show ip arp data.' : 'Candidate is not Cisco IOS.',
      };
    }

    return {
      matched: true,
      confidence: snapshot || arpOutput ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Cisco IOS metadata and a usable local source.',
      candidate: { ...candidateArg, port: candidateArg.port || snapshot?.device.port || ciscoIosDefaultSshPort },
      normalizedDeviceId: candidateArg.macAddress || candidateArg.id || candidateArg.host || snapshot?.device.id,
      metadata: { snapshotConfigured: Boolean(snapshot), arpOutputConfigured: Boolean(arpOutput), nativeSshImplemented: false },
    };
  }
}

export const createCiscoIosDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: ciscoIosDomain, displayName: ciscoIosDisplayName })
    .addMatcher(new CiscoIosDhcpMatcher())
    .addMatcher(new CiscoIosManualMatcher())
    .addValidator(new CiscoIosCandidateValidator());
};

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

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