import * as plugins from '../../plugins.js';
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryProbe, IDiscoveryProbeResult, IDiscoveryValidator } from '../../core/types.js';
import type { IOpenRGBManualDiscoveryRecord } from './openrgb.types.js';
import { openrgbDefaultPort } from './openrgb.types.js';

export class OpenRGBLocalSdkProbe implements IDiscoveryProbe {
  public id = 'openrgb-local-sdk-probe';
  public source = 'manual' as const;
  public description = 'Check the default local OpenRGB SDK endpoint on 127.0.0.1:6742.';

  public async probe(contextArg: IDiscoveryContext): Promise<IDiscoveryProbeResult> {
    if (contextArg.abortSignal?.aborted) {
      return { candidates: [] };
    }
    const reachable = await this.canConnect('127.0.0.1', openrgbDefaultPort, 400);
    return {
      candidates: reachable ? [{
        source: 'manual',
        integrationDomain: 'openrgb',
        id: '127.0.0.1:6742',
        host: '127.0.0.1',
        port: openrgbDefaultPort,
        name: 'OpenRGB SDK Server',
        manufacturer: 'OpenRGB',
        model: 'OpenRGB SDK Server',
        metadata: { openrgb: true, discoveryProtocol: 'local-sdk' },
      }] : [],
    };
  }

  private async canConnect(hostArg: string, portArg: number, timeoutMsArg: number): Promise<boolean> {
    return new Promise((resolve) => {
      let settled = false;
      const socket = plugins.net.createConnection({ host: hostArg, port: portArg });
      const finish = (valueArg: boolean) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        resolve(valueArg);
      };
      socket.setTimeout(timeoutMsArg);
      socket.once('connect', () => finish(true));
      socket.once('error', () => finish(false));
      socket.once('timeout', () => finish(false));
    });
  }
}

export class OpenRGBManualMatcher implements IDiscoveryMatcher<IOpenRGBManualDiscoveryRecord> {
  public id = 'openrgb-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual OpenRGB SDK setup entries and fixture snapshots.';

  public async matches(inputArg: IOpenRGBManualDiscoveryRecord): Promise<IDiscoveryMatch> {
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot || metadata.snapshot;
    const text = [inputArg.integrationDomain, inputArg.name, inputArg.manufacturer, inputArg.model, metadata.manufacturer, metadata.model]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const matched = inputArg.integrationDomain === 'openrgb'
      || metadata.openrgb === true
      || Boolean(snapshot)
      || text.includes('openrgb')
      || inputArg.port === openrgbDefaultPort
      || Boolean(inputArg.host && !text);
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain OpenRGB setup data.' };
    }
    const id = inputArg.id || (inputArg.host ? `${inputArg.host}:${inputArg.port || openrgbDefaultPort}` : 'openrgb-manual');
    return {
      matched: true,
      confidence: snapshot ? 'certain' : inputArg.host && inputArg.port === openrgbDefaultPort ? 'high' : inputArg.host ? 'medium' : 'low',
      reason: snapshot ? 'Manual entry includes an OpenRGB snapshot.' : 'Manual entry can start OpenRGB SDK setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: 'openrgb',
        id,
        host: inputArg.host,
        port: inputArg.port || openrgbDefaultPort,
        name: inputArg.name || 'OpenRGB SDK Server',
        manufacturer: inputArg.manufacturer || 'OpenRGB',
        model: inputArg.model || 'OpenRGB SDK Server',
        metadata: {
          ...metadata,
          openrgb: true,
          manual: true,
          controller: inputArg.controller,
          device: inputArg.device,
          devices: inputArg.devices,
          profiles: inputArg.profiles,
          snapshot,
        },
      },
      metadata: { snapshotConfigured: Boolean(snapshot) },
    };
  }
}

export class OpenRGBCandidateValidator implements IDiscoveryValidator {
  public id = 'openrgb-candidate-validator';
  public description = 'Validate OpenRGB SDK candidates before local setup.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.manufacturer, metadata.model]
      .filter((valueArg): valueArg is string => typeof valueArg === 'string')
      .join(' ')
      .toLowerCase();
    const matched = candidateArg.integrationDomain === 'openrgb'
      || metadata.openrgb === true
      || metadata.snapshot !== undefined
      || text.includes('openrgb')
      || candidateArg.port === openrgbDefaultPort;
    const hasUsableEndpoint = Boolean(candidateArg.host && (!candidateArg.port || this.isValidPort(candidateArg.port)));
    return {
      matched: matched && (hasUsableEndpoint || metadata.snapshot !== undefined),
      confidence: matched && metadata.snapshot !== undefined ? 'certain' : matched && candidateArg.host && candidateArg.port === openrgbDefaultPort ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has OpenRGB SDK metadata or default port information.' : 'Candidate is not OpenRGB.',
      candidate: matched && (hasUsableEndpoint || metadata.snapshot !== undefined) ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.id || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || openrgbDefaultPort}` : undefined),
    };
  }

  private isValidPort(valueArg: number): boolean {
    return Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
  }
}

export const createOpenRGBDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'openrgb', displayName: 'OpenRGB' })
    .addProbe(new OpenRGBLocalSdkProbe())
    .addMatcher(new OpenRGBManualMatcher())
    .addValidator(new OpenRGBCandidateValidator());
};
