import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { IOpenhomeConfig } from './openhome.types.js';
import { openhomeDefaultName, openhomeProfile } from './openhome.types.js';

interface IOpenhomeServiceDescription {
  serviceId: string;
  serviceType: string;
  controlUrl: string;
}

interface IOpenhomeDeviceDescription {
  location: string;
  baseUrl: string;
  uuid?: string;
  friendlyName?: string;
  manufacturer?: string;
  modelName?: string;
  modelNumber?: string;
  serialNumber?: string;
  services: IOpenhomeServiceDescription[];
}

interface IOpenhomeSourceInfo {
  index: number;
  type?: string;
  name?: string;
}

interface IOpenhomeTrackInfo {
  type?: string;
  title?: string;
  uri?: string;
  artist?: string[];
  albumTitle?: string;
  albumArtwork?: string;
  genre?: string[];
  duration?: number;
  mimeType?: string;
}

interface IOpenhomeLiveData {
  product: Record<string, unknown>;
  standby?: boolean;
  transportState?: string;
  source?: IOpenhomeSourceInfo;
  sources?: IOpenhomeSourceInfo[];
  trackInfo?: IOpenhomeTrackInfo;
  volume?: number;
  muted?: boolean;
  softwareStatus?: Record<string, unknown>;
  services: string[];
  fetchedAt: string;
}

export class OpenhomeClient extends SimpleLocalClient<IOpenhomeConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;
  private cachedDescription?: IOpenhomeDeviceDescription;
  private readonly soapClient = new plugins.ecobridgeDeviceManager.UpnpSoapClient('');

  constructor(private readonly configArg: IOpenhomeConfig) {
    super(openhomeProfile, configArg);
  }

  public override async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.configArg.client || this.configArg.snapshotProvider) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.descriptionUrl()) {
      try {
        this.cachedSnapshot = await this.fetchLiveSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = this.buildOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public override async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = Boolean(this.descriptionUrl()) && !this.configArg.client && !this.configArg.snapshotProvider;
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.descriptionUrl()) {
      return super.execute(requestArg);
    }

    try {
      const description = await this.getDeviceDescription();
      const data = await this.executeOpenhome(description, requestArg);
      this.cachedSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public override async destroy(): Promise<void> {
    await this.configArg.client?.destroy?.();
  }

  private async fetchLiveSnapshot(): Promise<ISimpleLocalSnapshot> {
    const description = await this.getDeviceDescription();
    const product = await this.productInfo(description);
    const standby = await this.optionalBoolean(description, 'Product', 'Standby', 'Value');
    const source = await this.currentSource(description).catch(() => undefined);
    const [sources, transportState, trackInfo, volume, muted, softwareStatus] = await Promise.all([
      this.sources(description).catch(() => undefined),
      this.transportState(description, source).catch(() => undefined),
      this.trackInfo(description).catch(() => undefined),
      this.optionalNumber(description, 'Volume', 'Volume', 'Value'),
      this.optionalBoolean(description, 'Volume', 'Mute', 'Value'),
      this.softwareStatus(description).catch(() => undefined),
    ]);

    const liveData: IOpenhomeLiveData = {
      product,
      standby,
      transportState,
      source,
      sources,
      trackInfo,
      volume,
      muted,
      softwareStatus,
      services: description.services.map((serviceArg) => serviceArg.serviceId),
      fetchedAt: new Date().toISOString(),
    };

    const entities: ISimpleLocalEntitySnapshot[] = [
      {
        id: 'media_player',
        name: 'Media Player',
        platform: 'media_player',
        state: mediaState(standby, transportState),
        available: true,
        writable: true,
        attributes: cleanAttributes({
          standby,
          transportState,
          source: source?.name,
          sourceType: source?.type,
          sourceIndex: source?.index,
          sourceList: sources?.map((sourceArg) => sourceArg.name || sourceArg.type || String(sourceArg.index)),
          volumeLevel: typeof volume === 'number' ? volume / 100 : undefined,
          isVolumeMuted: muted,
          mediaTitle: trackInfo?.title,
          mediaArtist: trackInfo?.artist?.[0],
          mediaAlbumName: trackInfo?.albumTitle,
          mediaImageUrl: trackInfo?.albumArtwork,
          trackInfo,
        }),
      },
    ];

    if (softwareStatus) {
      entities.push({
        id: 'firmware_update',
        name: 'Firmware Update',
        platform: 'update',
        state: stringValue(softwareStatus.status) || 'unknown',
        available: true,
        writable: Boolean(this.service(description, 'Update')),
        attributes: cleanAttributes({
          installedVersion: currentSoftwareVersion(softwareStatus),
          latestVersion: latestSoftwareVersion(softwareStatus),
          releaseSummary: latestSoftwareSummary(softwareStatus),
          releaseUrl: latestSoftwareReleaseUrl(softwareStatus),
          softwareStatus,
        }),
      });
    }

    return {
      device: {
        id: this.configArg.uniqueId || description.uuid || this.descriptionUrl() || openhomeProfile.domain,
        name: stringValue(product.Room) || stringValue(product.Name) || this.configArg.name || description.friendlyName || openhomeDefaultName,
        manufacturer: description.manufacturer || openhomeProfile.manufacturer,
        model: description.modelName || openhomeProfile.model,
        serialNumber: description.serialNumber || description.uuid,
        host: this.endpointHost(description.location),
        port: this.endpointPort(description.location),
        protocol: 'upnp',
        configurationUrl: description.location,
        attributes: cleanAttributes({
          friendlyName: description.friendlyName,
          modelNumber: description.modelNumber,
          productName: product.Name,
          productRoom: product.Room,
          serviceTypes: description.services.map((serviceArg) => serviceArg.serviceType),
        }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: liveData,
      capabilities: {
        localRead: true,
        localControl: true,
        protocol: 'openhome-upnp-soap',
      },
    };
  }

  private async executeOpenhome(descriptionArg: IOpenhomeDeviceDescription, requestArg: IServiceCallRequest): Promise<unknown> {
    const data = requestArg.data || {};
    if (requestArg.service === 'turn_on') {
      await this.soap(descriptionArg, 'Product', 'SetStandby', { Value: 'false' });
      return { action: 'SetStandby', value: false };
    }
    if (requestArg.service === 'turn_off') {
      await this.soap(descriptionArg, 'Product', 'SetStandby', { Value: 'true' });
      return { action: 'SetStandby', value: true };
    }
    if (requestArg.service === 'media_play') {
      await this.soapTransportAction(descriptionArg, 'Play');
      return { action: 'Play' };
    }
    if (requestArg.service === 'media_pause') {
      await this.soapTransportAction(descriptionArg, 'Pause');
      return { action: 'Pause' };
    }
    if (requestArg.service === 'media_stop') {
      await this.soapTransportAction(descriptionArg, 'Stop');
      return { action: 'Stop' };
    }
    if (requestArg.service === 'media_next_track') {
      await this.soapSkipAction(descriptionArg, 1);
      return { action: 'SkipNext' };
    }
    if (requestArg.service === 'media_previous_track') {
      await this.soapSkipAction(descriptionArg, -1);
      return { action: 'SkipPrevious' };
    }
    if (requestArg.service === 'volume_up') {
      await this.soap(descriptionArg, 'Volume', 'VolumeInc');
      return { action: 'VolumeInc' };
    }
    if (requestArg.service === 'volume_down') {
      await this.soap(descriptionArg, 'Volume', 'VolumeDec');
      return { action: 'VolumeDec' };
    }
    if (requestArg.service === 'volume_mute') {
      const mute = booleanValue(data.mute ?? data.is_volume_muted ?? data.muted) ?? true;
      await this.soap(descriptionArg, 'Volume', 'SetMute', { Value: String(mute).toLowerCase() });
      return { action: 'SetMute', value: mute };
    }
    if (requestArg.service === 'set_volume_level' || requestArg.service === 'volume_set' || requestArg.service === 'set_level') {
      const volume = volumeFromServiceData(data);
      await this.soap(descriptionArg, 'Volume', 'SetVolume', { Value: volume });
      return { action: 'SetVolume', value: volume };
    }
    if (requestArg.service === 'select_source') {
      const index = await this.sourceIndexFromServiceData(descriptionArg, data);
      await this.soap(descriptionArg, 'Product', 'SetSourceIndex', { Value: index });
      return { action: 'SetSourceIndex', value: index };
    }
    if (requestArg.service === 'invoke_pin') {
      const pin = numberValue(data.pin ?? data.index ?? data.pinIndex);
      if (pin === undefined) {
        throw new Error('OpenHome invoke_pin requires data.pin.');
      }
      await this.soap(descriptionArg, 'Pins', 'InvokeIndex', { Index: Math.max(0, pin - 1) });
      return { action: 'InvokeIndex', value: pin };
    }
    if (requestArg.service === 'play_media') {
      const uri = stringValue(data.uri ?? data.url ?? data.media_id ?? data.media_content_id);
      if (!uri) {
        throw new Error('OpenHome play_media requires data.uri or data.media_content_id.');
      }
      const title = stringValue(data.title ?? data.media_title) || 'Home Assistant';
      const albumArtwork = stringValue(data.albumArtwork ?? data.album_artwork ?? data.media_image_url) || '';
      await this.soap(descriptionArg, 'Radio', 'SetChannel', { Uri: uri, Metadata: generateDidlMetadata({ uri, title, albumArtwork }) });
      await this.soap(descriptionArg, 'Radio', 'Play');
      return { action: 'SetChannel', uri };
    }
    if (requestArg.service === 'check_latest_firmware') {
      await this.soap(descriptionArg, 'Update', 'CheckNow');
      return { action: 'CheckNow' };
    }
    if (requestArg.service === 'install_update' || requestArg.service === 'update_install' || requestArg.service === 'install') {
      await this.soap(descriptionArg, 'Update', 'Apply');
      return { action: 'Apply' };
    }
    throw new Error(`Unsupported OpenHome service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async getDeviceDescription(): Promise<IOpenhomeDeviceDescription> {
    if (this.cachedDescription) {
      return this.cachedDescription;
    }
    const location = this.descriptionUrl();
    if (!location) {
      throw new Error('OpenHome live HTTP/SOAP support requires config.url, config.host containing the SSDP location URL, or config.host plus config.path.');
    }
    const timeoutMs = this.configArg.timeoutMs || 5000;
    const abortController = new AbortController();
    const timer = setTimeout(() => abortController.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(location, { headers: { accept: 'application/xml, text/xml;q=0.9, */*;q=0.8' }, signal: abortController.signal });
      const xml = await response.text();
      if (!response.ok) {
        throw new Error(`OpenHome device description ${location} failed with HTTP ${response.status}: ${xml.slice(0, 160)}`);
      }
      this.cachedDescription = parseDeviceDescription(location, xml);
      return this.cachedDescription;
    } finally {
      clearTimeout(timer);
    }
  }

  private async productInfo(descriptionArg: IOpenhomeDeviceDescription): Promise<Record<string, unknown>> {
    const values = await this.soapValues(descriptionArg, 'Product', 'Product', ['ManufacturerName', 'ManufacturerInfo', 'ManufacturerUrl', 'ManufacturerImageUri', 'ModelName', 'ModelInfo', 'ModelUrl', 'ModelImageUri', 'ProductRoom', 'Room', 'Name', 'ProductInfo', 'ProductUrl', 'ProductImageUri']);
    return cleanAttributes({
      ...values,
      Name: values.Name || values.ProductRoom || descriptionArg.friendlyName,
      Room: values.Room || values.ProductRoom,
    }) || {};
  }

  private async currentSource(descriptionArg: IOpenhomeDeviceDescription): Promise<IOpenhomeSourceInfo | undefined> {
    const indexValue = await this.optionalNumber(descriptionArg, 'Product', 'SourceIndex', 'Value');
    if (indexValue === undefined) {
      return undefined;
    }
    const sourceValues = await this.soapValues(descriptionArg, 'Product', 'Source', ['SystemName', 'Type', 'Name', 'Visible'], { Index: indexValue });
    return {
      index: indexValue,
      type: stringValue(sourceValues.Type),
      name: stringValue(sourceValues.Name) || stringValue(sourceValues.SystemName),
    };
  }

  private async sources(descriptionArg: IOpenhomeDeviceDescription): Promise<IOpenhomeSourceInfo[] | undefined> {
    const values = await this.soapValues(descriptionArg, 'Product', 'SourceXml', ['Value']);
    const xml = unescapeXml(stringValue(values.Value) || '');
    if (!xml) {
      return undefined;
    }
    const sources: IOpenhomeSourceInfo[] = [];
    let index = 0;
    for (const sourceXml of blocks(xml, 'Source')) {
      const visible = (tag(sourceXml, 'Visible') || '').toLowerCase() === 'true';
      if (visible) {
        sources.push({ index, type: tag(sourceXml, 'Type'), name: tag(sourceXml, 'Name') });
      }
      index += 1;
    }
    return sources;
  }

  private async transportState(descriptionArg: IOpenhomeDeviceDescription, sourceArg?: IOpenhomeSourceInfo): Promise<string | undefined> {
    const serviceName = this.service(descriptionArg, 'Transport') ? 'Transport' : sourceArg?.type === 'Radio' && this.service(descriptionArg, 'Radio') ? 'Radio' : 'Playlist';
    const tagName = serviceName === 'Transport' ? 'State' : 'Value';
    const values = await this.soapValues(descriptionArg, serviceName, 'TransportState', [tagName]);
    return stringValue(values[tagName]);
  }

  private async trackInfo(descriptionArg: IOpenhomeDeviceDescription): Promise<IOpenhomeTrackInfo | undefined> {
    if (!this.service(descriptionArg, 'Info')) {
      return undefined;
    }
    const values = await this.soapValues(descriptionArg, 'Info', 'Track', ['Uri', 'Metadata']);
    const metadata = stringValue(values.Metadata);
    const parsed = parseDidlLite(metadata ? unescapeXml(metadata) : '');
    return cleanAttributes({ ...(parsed || {}), uri: parsed?.uri || stringValue(values.Uri) }) as IOpenhomeTrackInfo | undefined;
  }

  private async softwareStatus(descriptionArg: IOpenhomeDeviceDescription): Promise<Record<string, unknown> | undefined> {
    if (!this.service(descriptionArg, 'Update')) {
      return undefined;
    }
    const values = await this.soapValues(descriptionArg, 'Update', 'GetSoftwareStatus', ['SoftwareStatus']);
    const rawStatus = stringValue(values.SoftwareStatus);
    if (!rawStatus) {
      return undefined;
    }
    const parsed = parseJson(unescapeXml(rawStatus));
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
  }

  private async soapTransportAction(descriptionArg: IOpenhomeDeviceDescription, actionArg: 'Play' | 'Pause' | 'Stop'): Promise<void> {
    const source = await this.currentSource(descriptionArg).catch(() => undefined);
    const serviceName = this.service(descriptionArg, 'Transport') ? 'Transport' : source?.type === 'Radio' && this.service(descriptionArg, 'Radio') ? 'Radio' : 'Playlist';
    await this.soap(descriptionArg, serviceName, actionArg);
  }

  private async soapSkipAction(descriptionArg: IOpenhomeDeviceDescription, offsetArg: number): Promise<void> {
    if (this.service(descriptionArg, 'Transport')) {
      await this.soap(descriptionArg, 'Transport', offsetArg > 0 ? 'SkipNext' : 'SkipPrevious');
      return;
    }
    await this.soap(descriptionArg, 'Playlist', offsetArg > 0 ? 'Next' : 'Previous');
  }

  private async sourceIndexFromServiceData(descriptionArg: IOpenhomeDeviceDescription, dataArg: Record<string, unknown>): Promise<number> {
    const explicitIndex = numberValue(dataArg.index ?? dataArg.sourceIndex ?? dataArg.source_id);
    if (explicitIndex !== undefined) {
      return explicitIndex;
    }
    const sourceName = stringValue(dataArg.source ?? dataArg.sourceName ?? dataArg.name ?? dataArg.input);
    if (!sourceName) {
      throw new Error('OpenHome select_source requires data.index or data.source.');
    }
    const candidates = await this.sources(descriptionArg);
    const matched = candidates?.find((sourceArg) => [sourceArg.name, sourceArg.type, String(sourceArg.index)].some((valueArg) => valueArg?.toLowerCase() === sourceName.toLowerCase()));
    if (!matched) {
      throw new Error(`OpenHome source not found: ${sourceName}`);
    }
    return matched.index;
  }

  private async optionalBoolean(descriptionArg: IOpenhomeDeviceDescription, serviceNameArg: string, actionArg: string, tagArg: string): Promise<boolean | undefined> {
    if (!this.service(descriptionArg, serviceNameArg)) {
      return undefined;
    }
    try {
      const values = await this.soapValues(descriptionArg, serviceNameArg, actionArg, [tagArg]);
      return booleanValue(values[tagArg]);
    } catch {
      return undefined;
    }
  }

  private async optionalNumber(descriptionArg: IOpenhomeDeviceDescription, serviceNameArg: string, actionArg: string, tagArg: string): Promise<number | undefined> {
    if (!this.service(descriptionArg, serviceNameArg)) {
      return undefined;
    }
    try {
      const values = await this.soapValues(descriptionArg, serviceNameArg, actionArg, [tagArg]);
      return numberValue(values[tagArg]);
    } catch {
      return undefined;
    }
  }

  private async soapValues(descriptionArg: IOpenhomeDeviceDescription, serviceNameArg: string, actionArg: string, tagsArg: string[], argsArg: Record<string, string | number> = {}): Promise<Record<string, string>> {
    const response = await this.soap(descriptionArg, serviceNameArg, actionArg, argsArg);
    return Object.fromEntries(tagsArg.map((tagArg) => [tagArg, unescapeXml(this.soapClient.extractValue(response, tagArg))]));
  }

  private async soap(descriptionArg: IOpenhomeDeviceDescription, serviceNameArg: string, actionArg: string, argsArg: Record<string, string | number> = {}): Promise<string> {
    const service = this.service(descriptionArg, serviceNameArg);
    if (!service) {
      throw new Error(`OpenHome service ${serviceNameArg} is not available on ${descriptionArg.location}.`);
    }
    return await this.soapClient.soapAction(service.controlUrl, service.serviceType, actionArg, argsArg);
  }

  private service(descriptionArg: IOpenhomeDeviceDescription, serviceNameArg: string): IOpenhomeServiceDescription | undefined {
    const normalized = serviceNameArg.toLowerCase();
    return descriptionArg.services.find((serviceArg) => {
      const serviceId = serviceArg.serviceId.toLowerCase();
      const serviceType = serviceArg.serviceType.toLowerCase();
      return serviceId.endsWith(`serviceid:${normalized}`) || serviceType.includes(`service:${normalized}:`) || serviceId.includes(`:${normalized}`);
    });
  }

  private descriptionUrl(): string | undefined {
    const metadata = this.configArg.metadata || {};
    const rawUrl = stringValue(this.configArg.url)
      || stringValue(this.configArg.location)
      || stringValue(this.configArg.ssdpLocation)
      || stringValue(this.configArg.ssdp_location)
      || stringValue(metadata.url)
      || stringValue(metadata.location)
      || stringValue(metadata.ssdpLocation)
      || stringValue(metadata.ssdp_location);
    if (rawUrl) {
      return rawUrl.includes('://') ? rawUrl : `http://${rawUrl}`;
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    if (host.includes('://')) {
      return host;
    }
    const path = stringValue(this.configArg.path);
    if (!path) {
      return undefined;
    }
    const useTls = this.configArg.useTls === true;
    const port = this.configArg.port;
    const portPart = port && !((useTls && port === 443) || (!useTls && port === 80)) ? `:${port}` : '';
    const pathPart = path.startsWith('/') ? path : `/${path}`;
    return `${useTls ? 'https' : 'http'}://${hostForUrl(host)}${portPart}${pathPart}`;
  }

  private endpointHost(locationArg: string): string | undefined {
    try {
      return new URL(locationArg).hostname;
    } catch {
      return this.configArg.host;
    }
  }

  private endpointPort(locationArg: string): number | undefined {
    try {
      const url = new URL(locationArg);
      return url.port ? Number(url.port) : url.protocol === 'https:' ? 443 : 80;
    } catch {
      return this.configArg.port;
    }
  }

  private buildOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return {
      device: {
        id: this.configArg.uniqueId || this.configArg.name || openhomeProfile.domain,
        name: this.configArg.name || openhomeDefaultName,
        manufacturer: openhomeProfile.manufacturer,
        model: openhomeProfile.model,
        host: this.configArg.host,
        port: this.configArg.port,
        protocol: 'upnp',
      },
      entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { source: 'runtime' } }],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }
}

const parseDeviceDescription = (locationArg: string, xmlArg: string): IOpenhomeDeviceDescription => {
  const urlBase = tag(xmlArg, 'URLBase');
  const baseUrl = urlBase || new URL('.', locationArg).toString();
  const services = blocks(xmlArg, 'service').map((serviceXmlArg) => ({
    serviceId: tag(serviceXmlArg, 'serviceId') || '',
    serviceType: tag(serviceXmlArg, 'serviceType') || '',
    controlUrl: new URL(tag(serviceXmlArg, 'controlURL') || '', baseUrl).toString(),
  })).filter((serviceArg) => serviceArg.serviceId && serviceArg.serviceType && serviceArg.controlUrl);

  return {
    location: locationArg,
    baseUrl,
    uuid: tag(xmlArg, 'UDN')?.replace(/^uuid:/i, ''),
    friendlyName: tag(xmlArg, 'friendlyName'),
    manufacturer: tag(xmlArg, 'manufacturer'),
    modelName: tag(xmlArg, 'modelName'),
    modelNumber: tag(xmlArg, 'modelNumber'),
    serialNumber: tag(xmlArg, 'serialNumber'),
    services,
  };
};

const blocks = (xmlArg: string, tagNameArg: string): string[] => {
  const result: string[] = [];
  const regex = new RegExp(`<(?:[^:>]+:)?${tagNameArg}\\b[^>]*>[\\s\\S]*?<\\/(?:[^:>]+:)?${tagNameArg}>`, 'gi');
  let match: RegExpExecArray | null;
  while ((match = regex.exec(xmlArg)) !== null) {
    result.push(match[0]);
  }
  return result;
};

const tag = (xmlArg: string, tagNameArg: string): string | undefined => {
  const regex = new RegExp(`<(?:[^:>]+:)?${tagNameArg}\\b[^>]*>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tagNameArg}>`, 'i');
  const match = xmlArg.match(regex);
  return match?.[1]?.trim();
};

const parseDidlLite = (metadataArg: string): IOpenhomeTrackInfo | undefined => {
  if (!metadataArg) {
    return undefined;
  }
  const resXml = blocks(metadataArg, 'res')[0] || '';
  const trackInfo: IOpenhomeTrackInfo = {
    type: tag(metadataArg, 'class'),
    title: tag(metadataArg, 'title'),
    uri: tag(metadataArg, 'res'),
    artist: uniqueValues([...tags(metadataArg, 'artist'), ...tags(metadataArg, 'creator')]),
    albumTitle: tag(metadataArg, 'album'),
    albumArtwork: tag(metadataArg, 'albumArtURI') || tag(metadataArg, 'artworkURI'),
    genre: uniqueValues(tags(metadataArg, 'genre')),
    duration: durationToSeconds(attribute(resXml, 'duration')),
    mimeType: attribute(resXml, 'protocolInfo'),
  };
  return cleanAttributes({ ...trackInfo }) as IOpenhomeTrackInfo | undefined;
};

const tags = (xmlArg: string, tagNameArg: string): string[] => blocks(xmlArg, tagNameArg).map((blockArg) => tag(blockArg, tagNameArg)).filter((valueArg): valueArg is string => Boolean(valueArg));

const attribute = (xmlArg: string, nameArg: string): string | undefined => xmlArg.match(new RegExp(`${nameArg}="([^"]*)"`, 'i'))?.[1];

const uniqueValues = (valuesArg: string[]): string[] | undefined => {
  const values = [...new Set(valuesArg.filter(Boolean))];
  return values.length ? values : undefined;
};

const generateDidlMetadata = (trackArg: { uri: string; title: string; albumArtwork?: string }): string => `<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="" parentID="" restricted="True"><dc:title>${escapeXml(trackArg.title)}</dc:title><res protocolInfo="*:*:*:*">${escapeXml(trackArg.uri)}</res><upnp:albumArtURI>${escapeXml(trackArg.albumArtwork || '')}</upnp:albumArtURI><upnp:class>object.item.audioItem</upnp:class></item></DIDL-Lite>`;

const mediaState = (standbyArg: boolean | undefined, transportStateArg: string | undefined): string => {
  if (standbyArg) {
    return 'off';
  }
  const state = (transportStateArg || '').toLowerCase();
  if (state === 'paused') {
    return 'paused';
  }
  if (state === 'stopped') {
    return 'idle';
  }
  if (state === 'playing' || state === 'buffering') {
    return 'playing';
  }
  return standbyArg === false ? 'playing' : 'unknown';
};

const volumeFromServiceData = (dataArg: Record<string, unknown>): number => {
  const value = numberValue(dataArg.volume_level ?? dataArg.volume ?? dataArg.level ?? dataArg.percentage);
  if (value === undefined) {
    throw new Error('OpenHome volume service requires data.volume_level, data.volume, data.level, or data.percentage.');
  }
  return clamp(Math.round(value <= 1 ? value * 100 : value), 0, 100);
};

const durationToSeconds = (valueArg: string | undefined): number | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const [hours, minutes, seconds] = valueArg.split('.')[0].split(':').map((partArg) => Number(partArg));
  if ([hours, minutes, seconds].some((partArg) => !Number.isFinite(partArg))) {
    return undefined;
  }
  return hours * 3600 + minutes * 60 + seconds;
};

const currentSoftwareVersion = (statusArg: Record<string, unknown>): string | undefined => {
  const current = statusArg.current_software;
  return current && typeof current === 'object' ? stringValue((current as Record<string, unknown>).version) : undefined;
};

const latestSoftwareVersion = (statusArg: Record<string, unknown>): string | undefined => {
  const updates = updateList(statusArg);
  return updates?.[0] ? stringValue(updates[0].version) : undefined;
};

const latestSoftwareSummary = (statusArg: Record<string, unknown>): string | undefined => {
  const updates = updateList(statusArg);
  return updates?.[0] ? stringValue(updates[0].description) : undefined;
};

const latestSoftwareReleaseUrl = (statusArg: Record<string, unknown>): string | undefined => {
  const updateInfo = statusArg.update_info;
  return updateInfo && typeof updateInfo === 'object' ? stringValue((updateInfo as Record<string, unknown>).releasenotesuri) : undefined;
};

const updateList = (statusArg: Record<string, unknown>): Array<Record<string, unknown>> | undefined => {
  const updateInfo = statusArg.update_info;
  if (!updateInfo || typeof updateInfo !== 'object' || !Array.isArray((updateInfo as Record<string, unknown>).updates)) {
    return undefined;
  }
  return (updateInfo as Record<string, unknown>).updates as Array<Record<string, unknown>>;
};

const parseJson = (valueArg: string): unknown => {
  try {
    return JSON.parse(valueArg) as unknown;
  } catch {
    return undefined;
  }
};

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

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

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'number') {
    return valueArg !== 0;
  }
  if (typeof valueArg === 'string') {
    const normalized = valueArg.trim().toLowerCase();
    if (['1', 'true', 'yes', 'on'].includes(normalized)) {
      return true;
    }
    if (['0', 'false', 'no', 'off'].includes(normalized)) {
      return false;
    }
  }
  return undefined;
};

const cleanAttributes = <TValue extends Record<string, unknown>>(attributesArg: TValue): TValue | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined)) as TValue;
  return Object.keys(clean).length ? clean : undefined;
};

const escapeXml = (valueArg: string): string => valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');

const unescapeXml = (valueArg: string): string => valueArg.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');

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

const clamp = (valueArg: number, minArg: number, maxArg: number): number => Math.max(minArg, Math.min(maxArg, valueArg));

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
