import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { ISnapcastManualEntry, ISnapcastMdnsRecord, TSnapcastRpcTransport } from './snapcast.types.js';
import { snapcastHttpControlPort, snapcastTcpControlPort } from './snapcast.types.js';

const snapcastStreamTypes = new Set(['_snapcast._tcp', '_snapcast-stream._tcp']);
const snapcastTcpTypes = new Set(['_snapcast-ctrl._tcp', '_snapcast-jsonrpc._tcp']);
const snapcastHttpTypes = new Set(['_snapcast-http._tcp']);
const snapcastHttpsTypes = new Set(['_snapcast-https._tcp']);

export class SnapcastMdnsMatcher implements IDiscoveryMatcher<ISnapcastMdnsRecord> {
  public id = 'snapcast-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Snapcast mDNS records for stream, TCP control, and HTTP JSON-RPC services.';

  public async matches(recordArg: ISnapcastMdnsRecord): Promise<IDiscoveryMatch> {
    const type = normalizeMdnsType(recordArg.type || recordArg.serviceType || '');
    const name = recordArg.name || 'Snapcast';
    const serviceMatch = snapcastStreamTypes.has(type) || snapcastTcpTypes.has(type) || snapcastHttpTypes.has(type) || snapcastHttpsTypes.has(type);
    const nameMatch = name.toLowerCase().includes('snapcast');
    if (!serviceMatch && !nameMatch) {
      return { matched: false, confidence: 'low', reason: 'mDNS record is not a Snapcast service.' };
    }

    const transport = transportForMdnsType(type);
    const host = recordArg.host || recordArg.addresses?.[0];
    const port = controlPortForMdnsType(type, recordArg.port);
    const id = recordArg.txt?.id || recordArg.txt?.server || (host ? `${host}:${port}` : name);

    return {
      matched: true,
      confidence: serviceMatch && host ? 'certain' : serviceMatch ? 'high' : 'medium',
      reason: serviceMatch ? `mDNS service ${type} is a Snapcast service.` : 'mDNS name contains Snapcast.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: 'snapcast',
        id,
        host,
        port,
        name,
        manufacturer: 'Snapcast',
        model: 'Snapserver',
        metadata: {
          transport,
          mdnsType: type,
          txt: recordArg.txt,
          streamPort: snapcastStreamTypes.has(type) ? recordArg.port : undefined,
        },
      },
      metadata: {
        transport,
        mdnsType: type,
      },
    };
  }
}

export class SnapcastManualMatcher implements IDiscoveryMatcher<ISnapcastManualEntry> {
  public id = 'snapcast-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual Snapcast setup entries.';

  public async matches(inputArg: ISnapcastManualEntry): Promise<IDiscoveryMatch> {
    const manufacturer = inputArg.manufacturer?.toLowerCase() || '';
    const model = inputArg.model?.toLowerCase() || '';
    const matched = Boolean(inputArg.host || inputArg.metadata?.snapcast || manufacturer.includes('snapcast') || model.includes('snapserver'));
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Snapcast setup hints.' };
    }

    const transport = inputArg.transport || transportFromPort(inputArg.port);
    const port = inputArg.port || defaultPortForTransport(transport);
    return {
      matched: true,
      confidence: inputArg.host ? 'high' : 'medium',
      reason: 'Manual entry can start Snapcast setup.',
      normalizedDeviceId: inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : undefined),
      candidate: {
        source: 'manual',
        integrationDomain: 'snapcast',
        id: inputArg.id || (inputArg.host ? `${inputArg.host}:${port}` : undefined),
        host: inputArg.host,
        port,
        name: inputArg.name,
        manufacturer: 'Snapcast',
        model: inputArg.model || 'Snapserver',
        metadata: {
          ...inputArg.metadata,
          transport,
        },
      },
    };
  }
}

export class SnapcastCandidateValidator implements IDiscoveryValidator {
  public id = 'snapcast-candidate-validator';
  public description = 'Validate Snapcast candidates have a host and Snapcast service metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
    const model = candidateArg.model?.toLowerCase() || '';
    const mdnsType = typeof candidateArg.metadata?.mdnsType === 'string' ? normalizeMdnsType(candidateArg.metadata.mdnsType) : '';
    const matched = candidateArg.integrationDomain === 'snapcast'
      || manufacturer.includes('snapcast')
      || model.includes('snapserver')
      || model.includes('snapcast')
      || snapcastStreamTypes.has(mdnsType)
      || snapcastTcpTypes.has(mdnsType)
      || snapcastHttpTypes.has(mdnsType)
      || snapcastHttpsTypes.has(mdnsType);

    if (!matched || !candidateArg.host) {
      return {
        matched: false,
        confidence: matched ? 'medium' : 'low',
        reason: matched ? 'Snapcast candidate lacks host information.' : 'Candidate is not Snapcast.',
      };
    }

    return {
      matched: true,
      confidence: candidateArg.id ? 'certain' : 'high',
      reason: 'Candidate has Snapcast metadata and host information.',
      candidate: {
        ...candidateArg,
        port: candidateArg.port || defaultPortForTransport(transportFromCandidate(candidateArg)),
      },
      normalizedDeviceId: candidateArg.id || `${candidateArg.host}:${candidateArg.port || snapcastTcpControlPort}`,
    };
  }
}

export const createSnapcastDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'snapcast', displayName: 'Snapcast' })
    .addMatcher(new SnapcastMdnsMatcher())
    .addMatcher(new SnapcastManualMatcher())
    .addValidator(new SnapcastCandidateValidator());
};

const normalizeMdnsType = (valueArg: string): string => valueArg.toLowerCase().replace(/\.local\.?$/, '').replace(/\.$/, '');

const transportForMdnsType = (typeArg: string): TSnapcastRpcTransport => {
  if (snapcastHttpTypes.has(typeArg)) {
    return 'http';
  }
  if (snapcastHttpsTypes.has(typeArg)) {
    return 'https';
  }
  return 'tcp';
};

const controlPortForMdnsType = (typeArg: string, portArg?: number): number => {
  if (snapcastHttpTypes.has(typeArg) || snapcastHttpsTypes.has(typeArg)) {
    return portArg || snapcastHttpControlPort;
  }
  if (snapcastTcpTypes.has(typeArg)) {
    return portArg || snapcastTcpControlPort;
  }
  return snapcastTcpControlPort;
};

const transportFromPort = (portArg?: number): TSnapcastRpcTransport => {
  if (portArg === snapcastHttpControlPort) {
    return 'http';
  }
  return 'tcp';
};

const transportFromCandidate = (candidateArg: IDiscoveryCandidate): TSnapcastRpcTransport => {
  const transport = candidateArg.metadata?.transport;
  if (transport === 'http' || transport === 'https' || transport === 'snapshot' || transport === 'tcp') {
    return transport;
  }
  return transportFromPort(candidateArg.port);
};

const defaultPortForTransport = (transportArg: TSnapcastRpcTransport): number => {
  if (transportArg === 'http' || transportArg === 'https') {
    return snapcastHttpControlPort;
  }
  return snapcastTcpControlPort;
};
