import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IGo2rtcHttpCandidateRecord, IGo2rtcManualEntry, TGo2rtcProtocol } from './go2rtc.types.js';
import { go2rtcDefaultApiPort, go2rtcDefaultRtspPort, go2rtcDefaultWebRtcPort } from './go2rtc.types.js';

export class Go2rtcManualMatcher implements IDiscoveryMatcher<IGo2rtcManualEntry> {
  public id = 'go2rtc-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manual go2rtc HTTP API endpoint entries.';

  public async matches(inputArg: IGo2rtcManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(inputArg);
    const snapshot = inputArg.snapshot;
    const hint = hasGo2rtcHint(inputArg.name, endpoint.url) || Boolean(inputArg.metadata?.go2rtc || snapshot);
    if (!endpoint.host && !hint) {
      return { matched: false, confidence: 'low', reason: 'Manual go2rtc entries require a URL, host, cached snapshot, or go2rtc metadata.' };
    }
    const normalizedDeviceId = inputArg.id || snapshot?.id || endpoint.host || endpoint.url || 'go2rtc';
    return {
      matched: true,
      confidence: snapshot ? 'certain' : endpoint.host ? 'high' : 'medium',
      reason: snapshot ? 'Manual entry contains a go2rtc snapshot.' : 'Manual entry contains a local go2rtc endpoint.',
      normalizedDeviceId,
      candidate: {
        source: 'manual',
        integrationDomain: 'go2rtc',
        id: normalizedDeviceId,
        host: endpoint.host,
        port: endpoint.port,
        name: inputArg.name || endpoint.host || snapshot?.name || 'go2rtc',
        manufacturer: 'go2rtc',
        metadata: {
          ...inputArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          username: inputArg.username,
          password: inputArg.password,
          snapshot,
          streams: inputArg.streams,
          rtspPort: go2rtcDefaultRtspPort,
          webRtcPort: go2rtcDefaultWebRtcPort,
          discoveryProtocol: 'manual',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class Go2rtcHttpMatcher implements IDiscoveryMatcher<IGo2rtcHttpCandidateRecord> {
  public id = 'go2rtc-http-match';
  public source = 'http' as const;
  public description = 'Recognize local HTTP URL candidates that point at go2rtc.';

  public async matches(recordArg: IGo2rtcHttpCandidateRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const endpoint = endpointFromInput(recordArg);
    const metadata = recordArg.metadata || {};
    const hint = hasGo2rtcHint(recordArg.name, recordArg.manufacturer, recordArg.model, recordArg.url, endpoint.url) || Boolean(metadata.go2rtc || metadata.version || metadata.serverInfo);
    const defaultPortHint = endpoint.port === go2rtcDefaultApiPort;
    if (!endpoint.host || (!hint && !defaultPortHint)) {
      return { matched: false, confidence: 'low', reason: 'HTTP candidate does not look like a go2rtc API endpoint.' };
    }
    return {
      matched: true,
      confidence: hint ? 'high' : 'medium',
      reason: hint ? 'HTTP candidate contains go2rtc metadata.' : 'HTTP candidate uses the default go2rtc API port.',
      normalizedDeviceId: endpoint.host,
      candidate: {
        source: 'http',
        integrationDomain: 'go2rtc',
        id: endpoint.host,
        host: endpoint.host,
        port: endpoint.port,
        name: recordArg.name || endpoint.host,
        manufacturer: 'go2rtc',
        model: recordArg.model,
        metadata: {
          ...metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          rtspPort: numberValue(metadata.rtspPort) || go2rtcDefaultRtspPort,
          webRtcPort: numberValue(metadata.webRtcPort ?? metadata.webrtcPort) || go2rtcDefaultWebRtcPort,
          discoveryProtocol: 'http',
        },
      },
      metadata: {
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export class Go2rtcCandidateValidator implements IDiscoveryValidator {
  public id = 'go2rtc-candidate-validator';
  public description = 'Validate that a candidate can be configured as a local go2rtc API server.';

  public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    if (candidateArg.integrationDomain && candidateArg.integrationDomain !== 'go2rtc') {
      return { matched: false, confidence: 'low', reason: `Candidate belongs to ${candidateArg.integrationDomain}, not go2rtc.` };
    }
    const endpoint = endpointFromCandidate(candidateArg);
    const snapshot = candidateArg.metadata?.snapshot;
    if (!endpoint.host && !snapshot) {
      return { matched: false, confidence: 'low', reason: 'go2rtc candidates require a local URL, host, or cached snapshot.' };
    }
    if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65535) {
      return { matched: false, confidence: 'low', reason: 'go2rtc candidate has an invalid API port.' };
    }
    const hasHint = candidateArg.integrationDomain === 'go2rtc'
      || candidateArg.source === 'manual'
      || candidateArg.source === 'http'
      || endpoint.port === go2rtcDefaultApiPort
      || hasGo2rtcHint(candidateArg.name, candidateArg.manufacturer, candidateArg.model)
      || Boolean(candidateArg.metadata?.go2rtc || candidateArg.metadata?.serverInfo || snapshot);
    if (!hasHint) {
      return { matched: false, confidence: 'low', reason: 'Candidate does not contain go2rtc metadata.' };
    }
    return {
      matched: true,
      confidence: snapshot || candidateArg.integrationDomain === 'go2rtc' || candidateArg.source === 'manual' ? 'high' : 'medium',
      reason: 'Candidate has enough local go2rtc metadata to start configuration.',
      normalizedDeviceId: candidateArg.id || endpoint.host || 'go2rtc',
      candidate: {
        ...candidateArg,
        integrationDomain: 'go2rtc',
        id: candidateArg.id || endpoint.host || 'go2rtc',
        host: endpoint.host,
        port: endpoint.port,
        manufacturer: candidateArg.manufacturer || 'go2rtc',
        metadata: {
          ...candidateArg.metadata,
          protocol: endpoint.protocol,
          url: endpoint.url,
          rtspPort: numberValue(candidateArg.metadata?.rtspPort) || go2rtcDefaultRtspPort,
          webRtcPort: numberValue(candidateArg.metadata?.webRtcPort ?? candidateArg.metadata?.webrtcPort) || go2rtcDefaultWebRtcPort,
        },
      },
      metadata: {
        manualSupported: candidateArg.source === 'manual',
        protocol: endpoint.protocol,
        url: endpoint.url,
      },
    };
  }
}

export const createGo2rtcDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: 'go2rtc', displayName: 'go2rtc' })
    .addMatcher(new Go2rtcManualMatcher())
    .addMatcher(new Go2rtcHttpMatcher())
    .addValidator(new Go2rtcCandidateValidator());
};

const endpointFromInput = (inputArg: IGo2rtcManualEntry | IGo2rtcHttpCandidateRecord): { protocol: TGo2rtcProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof inputArg.metadata?.url === 'string' ? inputArg.metadata.url : undefined;
  const parsed = safeUrl(inputArg.url || metadataUrl || inputArg.host);
  if (parsed) {
    const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
    const port = parsed.port ? Number(parsed.port) : protocol === 'https' ? 443 : go2rtcDefaultApiPort;
    const path = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname.replace(/\/$/, '') : '';
    return { protocol, host: parsed.hostname, port, url: `${protocol}://${hostForUrl(parsed.hostname)}:${port}${path}` };
  }
  const protocol = inputArg.protocol || 'http';
  const port = inputArg.port || go2rtcDefaultApiPort;
  return { protocol, host: inputArg.host, port, url: inputArg.host ? `${protocol}://${hostForUrl(inputArg.host)}:${port}` : undefined };
};

const endpointFromCandidate = (candidateArg: IDiscoveryCandidate): { protocol: TGo2rtcProtocol; host?: string; port: number; url?: string } => {
  const metadataUrl = typeof candidateArg.metadata?.url === 'string' ? candidateArg.metadata.url : undefined;
  const parsed = safeUrl(metadataUrl || candidateArg.host);
  if (parsed) {
    const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
    const port = parsed.port ? Number(parsed.port) : protocol === 'https' ? 443 : go2rtcDefaultApiPort;
    const path = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname.replace(/\/$/, '') : '';
    return { protocol, host: parsed.hostname, port, url: `${protocol}://${hostForUrl(parsed.hostname)}:${port}${path}` };
  }
  const protocol = candidateArg.metadata?.protocol === 'https' ? 'https' : 'http';
  const port = candidateArg.port || go2rtcDefaultApiPort;
  return { protocol, host: candidateArg.host, port, url: candidateArg.host ? `${protocol}://${hostForUrl(candidateArg.host)}:${port}` : metadataUrl };
};

const hasGo2rtcHint = (...valuesArgs: Array<string | undefined>): boolean => {
  return valuesArgs.filter(Boolean).join(' ').toLowerCase().includes('go2rtc');
};

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

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

const hostForUrl = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
