import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type {
  IDiscoveryCandidate,
  IDiscoveryContext,
  IDiscoveryMatch,
  IDiscoveryMatcher,
  IDiscoveryProbe,
  IDiscoveryProbeResult,
  IDiscoveryValidator,
} from '../../core/types.js';
import type { IHyperionManualEntry, IHyperionRawData, IHyperionSnapshot, IHyperionSsdpRecord, THyperionTransport } from './hyperion.types.js';
import {
  hyperionDefaultJsonPort,
  hyperionDefaultSslWebPort,
  hyperionDefaultWebPort,
  hyperionDisplayName,
  hyperionDomain,
  hyperionSsdpManufacturer,
  hyperionSsdpServiceType,
} from './hyperion.types.js';

export class HyperionSsdpDiscoveryProbe implements IDiscoveryProbe {
  public id = 'hyperion-ssdp-discovery-probe';
  public source = 'ssdp' as const;
  public description = 'Discover Hyperion JSON API servers using the upstream SSDP service type.';

  public async probe(contextArg: IDiscoveryContext): Promise<IDiscoveryProbeResult> {
    if (contextArg.abortSignal?.aborted) {
      return { candidates: [] };
    }
    return { candidates: await this.discover(1200) };
  }

  private async discover(timeoutMsArg: number): Promise<IDiscoveryCandidate[]> {
    const { createSocket } = await import('node:dgram');
    const matcher = new HyperionSsdpMatcher();
    const candidates: IDiscoveryCandidate[] = [];
    const request = [
      'M-SEARCH * HTTP/1.1',
      'HOST: 239.255.255.250:1900',
      'MAN: "ssdp:discover"',
      'MX: 1',
      `ST: ${hyperionSsdpServiceType}`,
      '',
      '',
    ].join('\r\n');

    return new Promise((resolve) => {
      const socket = createSocket({ type: 'udp4', reuseAddr: true });
      const timer = setTimeout(() => {
        closeSocket();
        resolve(candidates);
      }, timeoutMsArg);
      const closeSocket = () => {
        clearTimeout(timer);
        try {
          socket.close();
        } catch {
          // Discovery sockets may already be closed after timeout or OS errors.
        }
      };
      socket.on('message', async (dataArg) => {
        const match = await matcher.matches({ headers: parseSsdpHeaders(dataArg.toString('utf8')) });
        if (match.matched && match.candidate && !candidates.some((candidateArg) => candidateArg.id === match.candidate?.id || candidateArg.host === match.candidate?.host)) {
          candidates.push(match.candidate);
        }
      });
      socket.on('error', () => {
        closeSocket();
        resolve(candidates);
      });
      socket.bind(() => {
        socket.setBroadcast(true);
        socket.send(Buffer.from(request, 'utf8'), 1900, '239.255.255.250');
      });
    });
  }
}

export class HyperionSsdpMatcher implements IDiscoveryMatcher<IHyperionSsdpRecord> {
  public id = 'hyperion-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Hyperion SSDP records by service type, manufacturer, and JSON server port metadata.';

  public async matches(recordArg: IHyperionSsdpRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const headers = normalizeHeaders(recordArg.headers || {});
    const upnp = normalizeObject(recordArg.upnp || {});
    const ports = normalizeObject(recordArg.ports || upnp.ports || {});
    const st = stringValue(recordArg.ssdp_st || recordArg.st || headers.st);
    const manufacturer = stringValue(recordArg.manufacturer || upnp.manufacturer || headers.manufacturer);
    const server = stringValue(recordArg.ssdp_server || recordArg.server || headers.server);
    const model = stringValue(recordArg.modelName || upnp.modelname || upnp.modelName || recordArg.modelNumber || upnp.modelnumber || upnp.modelNumber);
    const location = stringValue(recordArg.ssdp_location || recordArg.location || headers.location);
    const locationUrl = parseEndpoint(location);
    const jsonPort = numberValue(ports.jsonserver || ports.jsonServer || headers['hyperion-jss-port']) || hyperionDefaultJsonPort;
    const webPort = locationUrl?.port || hyperionDefaultWebPort;
    const sslPort = numberValue(ports.sslserver || ports.sslServer || headers['hyperion-ssl-port']) || hyperionDefaultSslWebPort;
    const name = stringValue(recordArg.friendlyName || upnp.friendlyname || upnp.friendlyName || headers['hyperion-name']) || hyperionDisplayName;
    const id = normalizeId(stringValue(recordArg.serialNumber || upnp.serialnumber || upnp.serialNumber || recordArg.UDN || recordArg.ssdp_usn || recordArg.usn || headers.usn));
    const matched = st === hyperionSsdpServiceType
      || manufacturer === hyperionSsdpManufacturer
      || Boolean(server?.toLowerCase().includes('hyperion'))
      || Boolean(model?.toLowerCase().includes('hyperion'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record is not a Hyperion basic device advertisement.' };
    }
    const host = locationUrl?.host;
    return {
      matched: true,
      confidence: host && id ? 'certain' : host ? 'high' : 'medium',
      reason: 'SSDP record matches Hyperion manufacturer/service metadata.',
      normalizedDeviceId: id || (host ? `${host}:${jsonPort}` : undefined),
      candidate: {
        source: 'ssdp',
        integrationDomain: hyperionDomain,
        id: id || (host ? `${host}:${jsonPort}` : undefined),
        host,
        port: jsonPort,
        name,
        manufacturer: 'Hyperion',
        model: model || 'Hyperion-NG',
        serialNumber: id,
        metadata: {
          ...recordArg.metadata,
          hyperion: true,
          discoveryProtocol: 'ssdp',
          ssdpServiceType: st,
          ssdpLocation: location,
          ssdpManufacturer: manufacturer,
          ssdpServer: server,
          jsonPort,
          webPort,
          sslPort,
          transport: 'tcp' satisfies THyperionTransport,
          rawUpnp: upnp,
          headers,
        },
      },
    };
  }
}

export class HyperionManualMatcher implements IDiscoveryMatcher<IHyperionManualEntry> {
  public id = 'hyperion-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Hyperion host, URL, token, snapshot, client, and executor setup entries.';

  public async matches(inputArg: IHyperionManualEntry, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const parsed = parseEndpoint(inputArg.url || inputArg.host);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || metadata.rawData);
    const hasData = 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 || inputArg.token || metadata.hyperion || hasData || text.includes('hyperion'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Hyperion setup hints.' };
    }
    const transport = inputArg.transport || transportValue(metadata.transport) || parsed?.transport || 'tcp';
    const host = parsed?.host || (inputArg.url ? undefined : inputArg.host) || snapshot?.device.host;
    const webPort = inputArg.webPort || numberValue(metadata.webPort) || snapshot?.device.webPort || (transport === 'http' || transport === 'websocket' ? parsed?.port : undefined) || hyperionDefaultWebPort;
    const jsonPort = inputArg.jsonPort || numberValue(metadata.jsonPort) || snapshot?.device.jsonPort || (transport === 'tcp' ? parsed?.port : undefined) || hyperionDefaultJsonPort;
    const port = inputArg.port || parsed?.port || (transport === 'tcp' ? jsonPort : webPort);
    const id = inputArg.id || inputArg.uniqueId || snapshot?.device.id || (host ? `${host}:${port}` : undefined);
    return {
      matched: true,
      confidence: host || hasData ? 'high' : 'medium',
      reason: 'Manual entry can start Hyperion setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: hyperionDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.device.name || hyperionDisplayName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || 'Hyperion',
        model: inputArg.model || snapshot?.device.model || 'Hyperion-NG',
        metadata: {
          ...metadata,
          hyperion: true,
          discoveryProtocol: 'manual',
          transport,
          ssl: inputArg.ssl ?? inputArg.tls ?? parsed?.ssl ?? metadata.ssl,
          jsonPort,
          webPort,
          token: inputArg.token || metadata.token,
          priority: inputArg.priority || metadata.priority,
          origin: inputArg.origin || metadata.origin,
          effectHideList: inputArg.effectHideList || metadata.effectHideList,
          snapshot,
          rawData,
          client: inputArg.client || metadata.client,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
        },
      },
    };
  }
}

export class HyperionCandidateValidator implements IDiscoveryValidator {
  public id = 'hyperion-candidate-validator';
  public description = 'Validate Hyperion candidates from SSDP 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 text = [candidateArg.integrationDomain, candidateArg.name, candidateArg.manufacturer, candidateArg.model, metadata.ssdpServiceType].filter(Boolean).join(' ').toLowerCase();
    const matched = candidateArg.integrationDomain === hyperionDomain
      || metadata.hyperion === true
      || text.includes('hyperion')
      || metadata.ssdpServiceType === hyperionSsdpServiceType;
    const hasUsableSource = Boolean(candidateArg.host || snapshot || rawData || metadata.client);
    const normalizedDeviceId = candidateArg.id || snapshot?.device.id || (candidateArg.host ? `${candidateArg.host}:${candidateArg.port || hyperionDefaultJsonPort}` : undefined);
    if (!matched || !hasUsableSource) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Hyperion candidate lacks a host, injected client, snapshot, or raw data.' : 'Candidate is not Hyperion.',
        normalizedDeviceId,
      };
    }
    return {
      matched: true,
      confidence: candidateArg.host && normalizedDeviceId ? 'certain' : candidateArg.host ? 'high' : 'medium',
      reason: 'Candidate has Hyperion metadata and a usable local endpoint, client, snapshot, or raw data.',
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: hyperionDomain,
        port: candidateArg.port || numberValue(metadata.jsonPort) || hyperionDefaultJsonPort,
        manufacturer: candidateArg.manufacturer || 'Hyperion',
      },
    };
  }
}

export const createHyperionDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: hyperionDomain, displayName: hyperionDisplayName })
    .addProbe(new HyperionSsdpDiscoveryProbe())
    .addMatcher(new HyperionSsdpMatcher())
    .addMatcher(new HyperionManualMatcher())
    .addValidator(new HyperionCandidateValidator());
};

const parseSsdpHeaders = (valueArg: string): Record<string, string> => {
  const headers: Record<string, string> = {};
  for (const line of valueArg.split(/\r?\n/)) {
    const index = line.indexOf(':');
    if (index <= 0) {
      continue;
    }
    headers[line.slice(0, index).trim()] = line.slice(index + 1).trim();
  }
  return headers;
};

const normalizeHeaders = (headersArg: Record<string, string | string[] | undefined>): Record<string, string | undefined> => {
  const normalized: Record<string, string | undefined> = {};
  for (const [key, value] of Object.entries(headersArg)) {
    normalized[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
  }
  return normalized;
};

const normalizeObject = (valueArg: unknown): Record<string, unknown> => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return {};
  }
  const normalized: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(valueArg)) {
    normalized[key] = value;
    normalized[key.toLowerCase()] = value;
  }
  return normalized;
};

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

const normalizeId = (valueArg: string | undefined): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  return valueArg.replace(/^uuid:/i, '').split('::')[0].trim() || undefined;
};

const snapshotValue = (valueArg: unknown): IHyperionSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'instances' in valueArg ? valueArg as IHyperionSnapshot : undefined;
};

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

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? Math.round(valueArg) : typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg)) ? Math.round(Number(valueArg)) : undefined;
const transportValue = (valueArg: unknown): THyperionTransport | undefined => valueArg === 'tcp' || valueArg === 'http' || valueArg === 'websocket' ? valueArg : undefined;
