import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { IEmbyManualEntry, IEmbyMdnsRecord, IEmbySsdpRecord } from './emby.types.js';
import { embyDefaultHttpPort, embyDefaultHttpsPort, embyDisplayName, embyDomain } from './emby.types.js';

export class EmbyManualMatcher implements IDiscoveryMatcher<IEmbyManualEntry> {
  public id = 'emby-manual-match';
  public source = 'manual' as const;
  public description = 'Recognize manually supplied local Emby server endpoints, snapshots, clients, and executors.';

  public async matches(inputArg: IEmbyManualEntry): Promise<IDiscoveryMatch> {
    const parsedUrl = parseUrl(inputArg.url);
    const metadata = inputArg.metadata || {};
    const snapshot = inputArg.snapshot;
    const haystack = `${inputArg.name || ''} ${inputArg.model || ''} ${inputArg.manufacturer || ''} ${metadata.name || ''}`.toLowerCase();
    const matched = Boolean(inputArg.host || parsedUrl || inputArg.apiKey || inputArg.api_key || inputArg.apiToken || inputArg.accessToken || snapshot || inputArg.sessions || inputArg.client || inputArg.commandExecutor || metadata.emby || haystack.includes('emby'));

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

    const ssl = inputArg.ssl ?? parsedUrl?.ssl ?? (inputArg.protocol === 'https');
    const host = inputArg.host || parsedUrl?.host;
    const port = inputArg.port || parsedUrl?.port || snapshot?.server.HttpServerPortNumber || (ssl ? embyDefaultHttpsPort : embyDefaultHttpPort);
    const id = inputArg.id || inputArg.uniqueId || snapshot?.server.Id || snapshot?.server.ServerId || host;

    return {
      matched: true,
      confidence: host || snapshot ? 'high' : 'medium',
      reason: 'Manual entry can start Emby setup.',
      normalizedDeviceId: id,
      candidate: {
        source: 'manual',
        integrationDomain: embyDomain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.server.ServerName || snapshot?.server.Name,
        manufacturer: inputArg.manufacturer || embyDisplayName,
        model: inputArg.model || 'Emby Server',
        metadata: {
          ...metadata,
          url: inputArg.url,
          ssl,
          snapshot,
          sessions: inputArg.sessions,
          server: inputArg.server,
          apiKey: inputArg.apiKey || inputArg.api_key || inputArg.apiToken || inputArg.accessToken,
          client: inputArg.client,
          commandExecutor: inputArg.commandExecutor,
        },
      },
    };
  }
}

export class EmbyMdnsMatcher implements IDiscoveryMatcher<IEmbyMdnsRecord> {
  public id = 'emby-mdns-match';
  public source = 'mdns' as const;
  public description = 'Recognize Emby-like mDNS HTTP advertisements when present.';

  public async matches(recordArg: IEmbyMdnsRecord): Promise<IDiscoveryMatch> {
    const properties = { ...recordArg.txt, ...recordArg.properties };
    const haystack = `${recordArg.type || ''} ${recordArg.name || ''} ${recordArg.hostname || ''} ${Object.values(properties).join(' ')}`.toLowerCase();
    const matched = haystack.includes('emby') || properties.emby === 'true';
    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'mDNS record does not advertise Emby metadata.' };
    }

    const id = valueForKey(properties, 'id') || valueForKey(properties, 'serverid') || valueForKey(properties, 'serverId');
    return {
      matched: true,
      confidence: id ? 'certain' : 'medium',
      reason: 'mDNS record contains Emby metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'mdns',
        integrationDomain: embyDomain,
        id,
        host: recordArg.host || recordArg.addresses?.[0],
        port: recordArg.port || embyDefaultHttpPort,
        name: cleanName(recordArg.name),
        manufacturer: embyDisplayName,
        model: 'Emby Server',
        metadata: {
          mdnsType: recordArg.type,
          txt: properties,
        },
      },
    };
  }
}

export class EmbySsdpMatcher implements IDiscoveryMatcher<IEmbySsdpRecord> {
  public id = 'emby-ssdp-match';
  public source = 'ssdp' as const;
  public description = 'Recognize Emby DLNA/UPnP SSDP advertisements.';

  public async matches(recordArg: IEmbySsdpRecord): Promise<IDiscoveryMatch> {
    const headers = recordArg.headers || {};
    const st = recordArg.st || headerValue(headers, 'st');
    const usn = recordArg.usn || headerValue(headers, 'usn');
    const location = recordArg.location || headerValue(headers, 'location');
    const server = recordArg.server || headerValue(headers, 'server');
    const friendlyName = recordArg.friendlyName || headerValue(headers, 'friendlyName') || headerValue(headers, 'friendlyname');
    const model = headerValue(headers, 'modelName') || headerValue(headers, 'modelname') || headerValue(headers, 'model');
    const manufacturer = headerValue(headers, 'manufacturer');
    const haystack = `${st || ''} ${usn || ''} ${location || ''} ${server || ''} ${friendlyName || ''} ${model || ''} ${manufacturer || ''}`.toLowerCase();
    const matched = haystack.includes('emby');

    if (!matched) {
      return { matched: false, confidence: 'low', reason: 'SSDP record does not advertise Emby metadata.' };
    }

    const parsedUrl = parseUrl(location);
    const id = usn?.replace(/^uuid:/i, '').split('::')[0];
    const ssl = parsedUrl?.ssl ?? false;

    return {
      matched: true,
      confidence: id ? 'certain' : 'high',
      reason: 'SSDP record matches Emby server metadata.',
      normalizedDeviceId: id,
      candidate: {
        source: 'ssdp',
        integrationDomain: embyDomain,
        id,
        host: parsedUrl?.host,
        port: parsedUrl?.port || (ssl ? embyDefaultHttpsPort : embyDefaultHttpPort),
        name: friendlyName,
        manufacturer: manufacturer || embyDisplayName,
        model: model || 'Emby Server',
        metadata: {
          url: parsedUrl ? `${ssl ? 'https' : 'http'}://${parsedUrl.host}:${parsedUrl.port || (ssl ? embyDefaultHttpsPort : embyDefaultHttpPort)}` : location,
          ssdpSt: st,
          ssdpUsn: usn,
          server,
          ssl,
        },
      },
    };
  }
}

export class EmbyCandidateValidator implements IDiscoveryValidator {
  public id = 'emby-candidate-validator';
  public description = 'Validate Emby server candidate metadata.';

  public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
    const metadata = candidateArg.metadata || {};
    const haystack = `${candidateArg.integrationDomain || ''} ${candidateArg.name || ''} ${candidateArg.manufacturer || ''} ${candidateArg.model || ''} ${metadata.url || ''}`.toLowerCase();
    const matched = candidateArg.integrationDomain === embyDomain || haystack.includes('emby') || Boolean(metadata.emby || metadata.snapshot || metadata.server || metadata.sessions);

    return {
      matched,
      confidence: matched && candidateArg.id ? 'certain' : matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
      reason: matched ? 'Candidate has Emby metadata.' : 'Candidate is not Emby.',
      candidate: matched ? candidateArg : undefined,
      normalizedDeviceId: candidateArg.id || candidateArg.host,
    };
  }
}

export const createEmbyDiscoveryDescriptor = (): DiscoveryDescriptor => {
  return new DiscoveryDescriptor({ integrationDomain: embyDomain, displayName: embyDisplayName })
    .addMatcher(new EmbyManualMatcher())
    .addMatcher(new EmbyMdnsMatcher())
    .addMatcher(new EmbySsdpMatcher())
    .addValidator(new EmbyCandidateValidator());
};

const parseUrl = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean } | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    return {
      host: url.hostname,
      port: url.port ? Number(url.port) : undefined,
      ssl: url.protocol === 'https:',
    };
  } catch {
    return undefined;
  }
};

const headerValue = (headersArg: Record<string, string | undefined>, keyArg: string): string | undefined => {
  const lowerKey = keyArg.toLowerCase();
  for (const [key, value] of Object.entries(headersArg)) {
    if (key.toLowerCase() === lowerKey) {
      return value;
    }
  }
  return undefined;
};

const valueForKey = (recordArg: Record<string, string | undefined>, keyArg: string): string | undefined => {
  const lowerKey = keyArg.toLowerCase();
  for (const [key, value] of Object.entries(recordArg)) {
    if (key.toLowerCase() === lowerKey) {
      return value;
    }
  }
  return undefined;
};

const cleanName = (valueArg: string | undefined): string | undefined => {
  return valueArg?.replace(/\._http\._tcp\.local\.?$/i, '').replace(/\.local\.?$/i, '').trim() || undefined;
};
