import type {
  IDlnaDmsBrowseResult,
  IDlnaDmsClientLike,
  IDlnaDmsConfig,
  IDlnaDmsContentDirectorySnapshot,
  IDlnaDmsContentObject,
  IDlnaDmsDeviceDescription,
  IDlnaDmsDeviceIcon,
  IDlnaDmsResource,
  IDlnaDmsServiceDescription,
  IDlnaDmsSnapshot,
  IDlnaDmsStatus,
  TDlnaDmsServiceKey,
  TDlnaDmsSnapshotSource,
} from './dlna_dms.types.js';
import { dlnaDmsDefaultPort, dlnaDmsDefaultTimeoutMs, dlnaDmsRootObjectId } from './dlna_dms.types.js';

const dlnaDmsDeviceTypes = [
  'urn:schemas-upnp-org:device:MediaServer:1',
  'urn:schemas-upnp-org:device:MediaServer:2',
  'urn:schemas-upnp-org:device:MediaServer:3',
  'urn:schemas-upnp-org:device:MediaServer:4',
];

const serviceKeyById: Record<string, TDlnaDmsServiceKey> = {
  'urn:upnp-org:serviceId:ContentDirectory': 'ContentDirectory',
  'urn:upnp-org:serviceId:ConnectionManager': 'ConnectionManager',
};

const mediaClassMap: Record<string, string> = {
  object: 'url',
  'object.item': 'url',
  'object.item.imageItem': 'image',
  'object.item.imageItem.photo': 'image',
  'object.item.audioItem': 'music',
  'object.item.audioItem.musicTrack': 'music',
  'object.item.audioItem.audioBroadcast': 'music',
  'object.item.audioItem.audioBook': 'podcast',
  'object.item.videoItem': 'video',
  'object.item.videoItem.movie': 'movie',
  'object.item.videoItem.videoBroadcast': 'tvshow',
  'object.item.videoItem.musicVideoClip': 'video',
  'object.item.playlistItem': 'track',
  'object.item.textItem': 'url',
  'object.item.bookmarkItem': 'url',
  'object.item.epgItem': 'episode',
  'object.item.epgItem.audioProgram': 'music',
  'object.item.epgItem.videoProgram': 'video',
  'object.container': 'directory',
  'object.container.person': 'artist',
  'object.container.person.musicArtist': 'artist',
  'object.container.playlistContainer': 'playlist',
  'object.container.album': 'album',
  'object.container.album.musicAlbum': 'album',
  'object.container.album.photoAlbum': 'album',
  'object.container.genre': 'genre',
  'object.container.genre.musicGenre': 'genre',
  'object.container.genre.movieGenre': 'genre',
  'object.container.channelGroup': 'channel',
  'object.container.channelGroup.audioChannelGroup': 'channel',
  'object.container.channelGroup.videoChannelGroup': 'channel',
  'object.container.epgContainer': 'directory',
  'object.container.storageSystem': 'directory',
  'object.container.storageVolume': 'directory',
  'object.container.storageFolder': 'directory',
  'object.container.bookmarkFolder': 'directory',
};

const streamableProtocols = new Set(['http-get', 'rtsp-rtp-udp', '*']);
const browseFilter = ['id', 'upnp:class', 'dc:title', 'res', '@childCount', 'upnp:albumArtURI'].join(',');

export class DlnaDmsClient {
  private currentSnapshot?: IDlnaDmsSnapshot;

  constructor(private readonly config: IDlnaDmsConfig) {}

  public async getSnapshot(): Promise<IDlnaDmsSnapshot> {
    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot), 'snapshot');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.client) {
      this.currentSnapshot = this.normalizeSnapshot(await this.snapshotFromClient(this.config.client), 'client');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.location()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('DLNA DMS refresh requires a device description URL, snapshot, or client.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IDlnaDmsSnapshot> {
    return this.getSnapshot();
  }

  public async ping(): Promise<boolean> {
    const snapshot = await this.getSnapshot();
    return snapshot.online && snapshot.source !== 'runtime' && !snapshot.error;
  }

  public async destroy(): Promise<void> {}

  public parseDeviceDescription(xmlArg: string, locationArg: string): IDlnaDmsDeviceDescription {
    const deviceBlock = this.findMediaServerDeviceBlock(xmlArg) || this.extractBlocks(xmlArg, 'device')[0] || xmlArg;
    const services: Partial<Record<TDlnaDmsServiceKey, IDlnaDmsServiceDescription>> = {};
    for (const serviceBlock of this.extractBlocks(deviceBlock, 'service')) {
      const serviceId = this.firstTagByLocalName(serviceBlock, 'serviceId') || '';
      const key = serviceKeyById[serviceId];
      if (!key) {
        continue;
      }
      services[key] = {
        serviceId,
        serviceType: this.firstTagByLocalName(serviceBlock, 'serviceType') || '',
        controlUrl: this.absoluteUrl(locationArg, this.firstTagByLocalName(serviceBlock, 'controlURL') || '') || '',
        eventSubUrl: this.absoluteUrl(locationArg, this.firstTagByLocalName(serviceBlock, 'eventSubURL') || ''),
        scpdUrl: this.absoluteUrl(locationArg, this.firstTagByLocalName(serviceBlock, 'SCPDURL') || ''),
      };
    }

    const udn = this.firstTagByLocalName(deviceBlock, 'UDN') || this.config.udn || this.config.deviceId || locationArg;
    return {
      location: locationArg,
      udn,
      usn: this.config.deviceId,
      deviceType: this.firstTagByLocalName(deviceBlock, 'deviceType') || this.config.deviceType || 'urn:schemas-upnp-org:device:MediaServer:1',
      friendlyName: this.firstTagByLocalName(deviceBlock, 'friendlyName') || this.config.name || 'DLNA Digital Media Server',
      manufacturer: this.firstTagByLocalName(deviceBlock, 'manufacturer') || this.config.manufacturer,
      modelName: this.firstTagByLocalName(deviceBlock, 'modelName') || this.config.modelName || this.config.model,
      modelNumber: this.firstTagByLocalName(deviceBlock, 'modelNumber') || this.config.modelNumber,
      serialNumber: this.firstTagByLocalName(deviceBlock, 'serialNumber') || this.config.serialNumber,
      sourceId: this.config.sourceId || this.slug(this.firstTagByLocalName(deviceBlock, 'friendlyName') || this.config.name || 'dlna_dms'),
      services,
      icons: this.iconsFromDeviceBlock(deviceBlock, locationArg),
    };
  }

  public parseDidlLite(xmlArg: string, baseUrlArg?: string): IDlnaDmsContentObject[] {
    const objects: IDlnaDmsContentObject[] = [];
    const objectRegex = /<((?:[A-Za-z0-9_]+:)?(?:container|item))\b[\s\S]*?<\/\1>/gi;
    for (const match of xmlArg.matchAll(objectRegex)) {
      const block = match[0];
      const tagName = match[1];
      const type = tagName.toLowerCase().endsWith('container') ? 'container' : 'item';
      const attrs = this.tagAttributes(block);
      const resources = this.resourcesFromObject(block, baseUrlArg);
      const upnpClass = this.firstTagByLocalName(block, 'class');
      const albumArtUri = this.firstTagByLocalName(block, 'albumArtURI');
      const thumbnail = albumArtUri ? this.absoluteUrl(baseUrlArg, albumArtUri) : this.thumbnailFromResources(resources);
      const childCount = this.numberValue(attrs.childCount ?? attrs['childcount']);
      objects.push({
        id: attrs.id || '',
        parentId: attrs.parentID ?? attrs.parentId,
        type,
        title: this.firstTagByLocalName(block, 'title') || attrs.id || 'Untitled',
        upnpClass,
        mediaClass: upnpClass ? mediaClassMap[upnpClass] || 'url' : undefined,
        childCount,
        restricted: this.booleanValue(attrs.restricted),
        searchable: this.booleanValue(attrs.searchable),
        albumArtUri: albumArtUri ? this.absoluteUrl(baseUrlArg, albumArtUri) || albumArtUri : undefined,
        thumbnail,
        canPlay: resources.some((resourceArg) => this.resourceIsStreamable(resourceArg)),
        canExpand: type === 'container' || Boolean(childCount && childCount > 0),
        resources,
      });
    }
    return objects;
  }

  private async snapshotFromClient(clientArg: IDlnaDmsClientLike): Promise<IDlnaDmsSnapshot> {
    const result = clientArg.getSnapshot ? await clientArg.getSnapshot() : clientArg.snapshot ? await clientArg.snapshot() : undefined;
    if (!result) {
      throw new Error('DLNA DMS client must expose getSnapshot() or snapshot().');
    }
    return result;
  }

  private async fetchSnapshot(): Promise<IDlnaDmsSnapshot> {
    const location = this.location();
    if (!location) {
      throw new Error('DLNA DMS device description URL is required for live refresh.');
    }

    const descriptionResponse = await this.fetchWithTimeout(location, { method: 'GET', headers: { accept: 'text/xml, application/xml' } });
    const descriptionXml = await descriptionResponse.text();
    if (!descriptionResponse.ok) {
      throw new Error(`Failed to fetch DLNA DMS description ${location}: HTTP ${descriptionResponse.status}`);
    }

    const device = this.parseDeviceDescription(descriptionXml, location);
    const contentDirectory = device.services.ContentDirectory;
    const connectionManager = device.services.ConnectionManager;
    const updatedAt = new Date().toISOString();

    const [systemUpdateId, searchCapabilities, sortCapabilities, protocolInfo, currentConnectionIds, rootMetadata, rootChildren] = await Promise.all([
      this.safeSoap(contentDirectory, 'GetSystemUpdateID', {}),
      this.safeSoap(contentDirectory, 'GetSearchCapabilities', {}),
      this.safeSoap(contentDirectory, 'GetSortCapabilities', {}),
      this.safeSoap(connectionManager, 'GetProtocolInfo', {}),
      this.safeSoap(connectionManager, 'GetCurrentConnectionIDs', {}),
      this.safeSoap(contentDirectory, 'Browse', {
        ObjectID: dlnaDmsRootObjectId,
        BrowseFlag: 'BrowseMetadata',
        Filter: browseFilter,
        StartingIndex: 0,
        RequestedCount: 1,
        SortCriteria: '',
      }),
      this.safeSoap(contentDirectory, 'Browse', {
        ObjectID: dlnaDmsRootObjectId,
        BrowseFlag: 'BrowseDirectChildren',
        Filter: browseFilter,
        StartingIndex: 0,
        RequestedCount: this.config.maxBrowseChildren ?? 50,
        SortCriteria: '',
      }),
    ]);

    const rootBrowse = this.parseBrowseResult(rootMetadata, location);
    const childBrowse = this.parseBrowseResult(rootChildren, location);
    const root = rootBrowse.objects[0] || this.rootObject(device, childBrowse.totalMatches);
    const contentSnapshot: IDlnaDmsContentDirectorySnapshot = {
      root,
      children: childBrowse.objects,
      totalMatches: childBrowse.totalMatches,
      numberReturned: childBrowse.numberReturned,
      updateId: childBrowse.updateId,
    };
    const status: IDlnaDmsStatus = {
      online: true,
      contentDirectoryAvailable: Boolean(contentDirectory),
      connectionManagerAvailable: Boolean(connectionManager),
      systemUpdateId: this.numberValue(systemUpdateId.SystemUpdateID),
      searchCapabilities: this.splitList(searchCapabilities.SearchCaps),
      sortCapabilities: this.splitList(sortCapabilities.SortCaps),
      sourceProtocolInfo: this.splitList(protocolInfo.Source),
      sinkProtocolInfo: this.splitList(protocolInfo.Sink),
      currentConnectionIds: this.splitList(currentConnectionIds.ConnectionIDs),
      updatedAt,
    };

    return this.normalizeSnapshot({
      device,
      status,
      contentDirectory: contentSnapshot,
      libraries: childBrowse.objects.filter((objectArg) => objectArg.type === 'container'),
      online: true,
      updatedAt,
      source: 'upnp',
    }, 'upnp');
  }

  private async safeSoap(serviceArg: IDlnaDmsServiceDescription | undefined, actionArg: string, argsArg: Record<string, string | number | boolean | undefined>): Promise<Record<string, string>> {
    if (!serviceArg) {
      return {};
    }
    try {
      return await this.soap(serviceArg, actionArg, argsArg);
    } catch {
      return {};
    }
  }

  private async soap(serviceArg: IDlnaDmsServiceDescription, actionArg: string, argsArg: Record<string, string | number | boolean | undefined>): Promise<Record<string, string>> {
    const response = await this.fetchWithTimeout(serviceArg.controlUrl, {
      method: 'POST',
      headers: {
        SOAPAction: `"${serviceArg.serviceType}#${actionArg}"`,
        'Content-Type': 'text/xml; charset="utf-8"',
      },
      body: this.soapBody(serviceArg.serviceType, actionArg, argsArg),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`DLNA DMS SOAP ${actionArg} failed with HTTP ${response.status}: ${text}`);
    }
    return this.parseSoapResponse(text);
  }

  private soapBody(serviceTypeArg: string, actionArg: string, argsArg: Record<string, string | number | boolean | undefined>): string {
    const args = Object.entries(argsArg)
      .filter((entryArg): entryArg is [string, string | number | boolean] => entryArg[1] !== undefined)
      .map(([keyArg, valueArg]) => `<${keyArg}>${this.escapeXml(String(valueArg))}</${keyArg}>`)
      .join('');
    return `<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:${actionArg} xmlns:u="${serviceTypeArg}">${args}</u:${actionArg}></s:Body></s:Envelope>`;
  }

  private parseSoapResponse(xmlArg: string): Record<string, string> {
    const body = this.firstTagByLocalName(xmlArg, 'Body') || xmlArg;
    const responseBlock = this.extractFirstChildBlock(body) || body;
    const responseInner = this.stripOuterTag(responseBlock) || responseBlock;
    const result: Record<string, string> = {};
    const tagRegex = /<([A-Za-z0-9_:-]+)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g;
    for (const match of responseInner.matchAll(tagRegex)) {
      const localName = this.localName(match[1]);
      if (localName.endsWith('Response')) {
        continue;
      }
      result[localName] = this.decodeXml(match[2].trim());
    }
    return result;
  }

  private parseBrowseResult(valueArg: Record<string, string>, baseUrlArg: string): IDlnaDmsBrowseResult {
    return {
      objects: valueArg.Result ? this.parseDidlLite(valueArg.Result, baseUrlArg) : [],
      totalMatches: this.numberValue(valueArg.TotalMatches),
      numberReturned: this.numberValue(valueArg.NumberReturned),
      updateId: this.numberValue(valueArg.UpdateID),
    };
  }

  private normalizeSnapshot(snapshotArg: IDlnaDmsSnapshot, sourceArg: TDlnaDmsSnapshotSource): IDlnaDmsSnapshot {
    const online = snapshotArg.online ?? snapshotArg.status?.online ?? false;
    const updatedAt = snapshotArg.updatedAt || snapshotArg.status?.updatedAt || new Date().toISOString();
    const device = {
      ...this.deviceFromConfig(),
      ...snapshotArg.device,
      services: {
        ...this.config.services,
        ...snapshotArg.device?.services,
      },
    };
    const root = snapshotArg.contentDirectory?.root || this.rootObject(device, snapshotArg.contentDirectory?.totalMatches);
    const children = snapshotArg.contentDirectory?.children || [];
    const libraries = snapshotArg.libraries?.length ? snapshotArg.libraries : children.filter((objectArg) => objectArg.type === 'container');
    const status: IDlnaDmsStatus = {
      ...snapshotArg.status,
      online,
      contentDirectoryAvailable: snapshotArg.status.contentDirectoryAvailable ?? (online && Boolean(device.services.ContentDirectory)),
      connectionManagerAvailable: snapshotArg.status.connectionManagerAvailable ?? (online && Boolean(device.services.ConnectionManager)),
      updatedAt,
      error: snapshotArg.error || snapshotArg.status.error,
    };

    return {
      ...snapshotArg,
      device,
      status,
      contentDirectory: {
        root,
        children,
        totalMatches: snapshotArg.contentDirectory?.totalMatches ?? children.length,
        numberReturned: snapshotArg.contentDirectory?.numberReturned ?? children.length,
        updateId: snapshotArg.contentDirectory?.updateId,
      },
      libraries,
      online,
      updatedAt,
      source: snapshotArg.source || sourceArg,
      error: snapshotArg.error || status.error,
    };
  }

  private offlineSnapshot(errorArg: string): IDlnaDmsSnapshot {
    const device = this.deviceFromConfig();
    return this.normalizeSnapshot({
      device,
      status: {
        online: false,
        contentDirectoryAvailable: false,
        connectionManagerAvailable: false,
        updatedAt: new Date().toISOString(),
        error: errorArg,
      },
      contentDirectory: {
        root: this.rootObject(device),
        children: [],
        totalMatches: 0,
        numberReturned: 0,
      },
      libraries: [],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    }, 'runtime');
  }

  private deviceFromConfig(): IDlnaDmsDeviceDescription {
    const location = this.location();
    return {
      location,
      udn: this.config.udn || this.config.deviceId || location || 'manual-dlna-dms',
      usn: this.config.deviceId,
      deviceType: this.config.deviceType || 'urn:schemas-upnp-org:device:MediaServer:1',
      friendlyName: this.config.name || 'DLNA Digital Media Server',
      manufacturer: this.config.manufacturer,
      modelName: this.config.modelName || this.config.model,
      modelNumber: this.config.modelNumber,
      serialNumber: this.config.serialNumber,
      sourceId: this.config.sourceId || this.slug(this.config.name || 'dlna_dms'),
      services: this.config.services || {},
      icons: [],
    };
  }

  private rootObject(deviceArg: IDlnaDmsDeviceDescription, childCountArg?: number): IDlnaDmsContentObject {
    return {
      id: dlnaDmsRootObjectId,
      type: 'container',
      title: deviceArg.friendlyName,
      upnpClass: 'object.container',
      mediaClass: 'directory',
      childCount: childCountArg,
      canPlay: false,
      canExpand: true,
      resources: [],
    };
  }

  private resourcesFromObject(xmlArg: string, baseUrlArg?: string): IDlnaDmsResource[] {
    const resources: IDlnaDmsResource[] = [];
    const resourceRegex = /<((?:[A-Za-z0-9_]+:)?res)\b([^>]*)>([\s\S]*?)<\/\1>/gi;
    for (const match of xmlArg.matchAll(resourceRegex)) {
      const attrs = this.parseAttributes(match[2]);
      const uri = this.decodeXml(match[3].trim());
      const protocolInfo = attrs.protocolInfo ?? attrs.protocolinfo;
      resources.push({
        uri: this.absoluteUrl(baseUrlArg, uri) || uri,
        protocolInfo,
        mimeType: this.resourceMimeType(protocolInfo),
        size: this.numberValue(attrs.size),
        duration: attrs.duration,
        bitrate: this.numberValue(attrs.bitrate),
        resolution: attrs.resolution,
      });
    }
    return resources;
  }

  private iconsFromDeviceBlock(deviceBlockArg: string, baseUrlArg: string): IDlnaDmsDeviceIcon[] {
    return this.extractBlocks(deviceBlockArg, 'icon').map((iconBlockArg) => ({
      mimeType: this.firstTagByLocalName(iconBlockArg, 'mimetype') || this.firstTagByLocalName(iconBlockArg, 'mimeType'),
      width: this.numberValue(this.firstTagByLocalName(iconBlockArg, 'width')),
      height: this.numberValue(this.firstTagByLocalName(iconBlockArg, 'height')),
      depth: this.numberValue(this.firstTagByLocalName(iconBlockArg, 'depth')),
      url: this.absoluteUrl(baseUrlArg, this.firstTagByLocalName(iconBlockArg, 'url') || ''),
    }));
  }

  private thumbnailFromResources(resourcesArg: IDlnaDmsResource[]): string | undefined {
    return resourcesArg.find((resourceArg) => resourceArg.protocolInfo?.toLowerCase().startsWith('http-get:*:image/') && resourceArg.uri)?.uri;
  }

  private resourceIsStreamable(resourceArg: IDlnaDmsResource): boolean {
    if (!resourceArg.protocolInfo) {
      return Boolean(resourceArg.uri);
    }
    const protocol = resourceArg.protocolInfo.split(':')[0].toLowerCase();
    return Boolean(resourceArg.uri) && streamableProtocols.has(protocol);
  }

  private resourceMimeType(protocolInfoArg: string | undefined): string | undefined {
    if (!protocolInfoArg) {
      return undefined;
    }
    const parts = protocolInfoArg.split(':');
    if (parts.length < 3 || !streamableProtocols.has(parts[0].toLowerCase())) {
      return undefined;
    }
    return parts[2];
  }

  private location(): string | undefined {
    if (this.config.location || this.config.url) {
      return this.config.location || this.config.url;
    }
    if (!this.config.host) {
      return undefined;
    }
    const path = this.config.path || '/description.xml';
    return `http://${this.formatHost(this.config.host)}:${this.config.port || dlnaDmsDefaultPort}${path.startsWith('/') ? path : `/${path}`}`;
  }

  private findMediaServerDeviceBlock(xmlArg: string): string | undefined {
    const blocks = this.extractBlocks(xmlArg, 'device');
    for (const block of blocks) {
      const deviceType = this.firstTagByLocalName(block, 'deviceType');
      if (deviceType && dlnaDmsDeviceTypes.includes(deviceType)) {
        return block;
      }
    }
    for (const block of blocks) {
      const inner = this.innerBlock(block, 'device');
      const nested = inner ? this.findMediaServerDeviceBlock(inner) : undefined;
      if (nested) {
        return nested;
      }
    }
    return undefined;
  }

  private extractBlocks(xmlArg: string, localNameArg: string): string[] {
    const regex = new RegExp(`<\\/?(?:[A-Za-z0-9_]+:)?${this.escapeRegex(localNameArg)}\\b[^>]*>`, 'gi');
    const blocks: string[] = [];
    let depth = 0;
    let start = -1;
    for (const match of xmlArg.matchAll(regex)) {
      const token = match[0];
      if (!token.startsWith('</')) {
        if (depth === 0) {
          start = match.index || 0;
        }
        if (!token.endsWith('/>')) {
          depth += 1;
        }
      } else {
        depth -= 1;
        if (depth === 0 && start >= 0) {
          blocks.push(xmlArg.slice(start, (match.index || 0) + token.length));
          start = -1;
        }
      }
    }
    return blocks;
  }

  private innerBlock(xmlArg: string, localNameArg: string): string | undefined {
    const start = xmlArg.match(new RegExp(`<(?:[A-Za-z0-9_]+:)?${this.escapeRegex(localNameArg)}\\b[^>]*>`, 'i'));
    const end = xmlArg.match(new RegExp(`</(?:[A-Za-z0-9_]+:)?${this.escapeRegex(localNameArg)}>`, 'i'));
    if (!start || !end || start.index === undefined || end.index === undefined) {
      return undefined;
    }
    return xmlArg.slice(start.index + start[0].length, end.index);
  }

  private firstTagByLocalName(xmlArg: string, localNameArg: string): string | undefined {
    const escaped = this.escapeRegex(localNameArg);
    const match = xmlArg.match(new RegExp(`<(?:[A-Za-z0-9_]+:)?${escaped}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[A-Za-z0-9_]+:)?${escaped}>`, 'i'));
    return match ? this.decodeXml(match[1].trim()) : undefined;
  }

  private extractFirstChildBlock(xmlArg: string): string | undefined {
    const match = xmlArg.match(/<([A-Za-z0-9_:-]+)(?:\s[^>]*)?>[\s\S]*<\/\1>/);
    return match?.[0];
  }

  private stripOuterTag(xmlArg: string): string | undefined {
    const match = xmlArg.match(/^<([A-Za-z0-9_:-]+)(?:\s[^>]*)?>([\s\S]*)<\/\1>$/);
    return match?.[2];
  }

  private tagAttributes(blockArg: string): Record<string, string> {
    const start = blockArg.match(/^<[^>]+>/);
    return start ? this.parseAttributes(start[0]) : {};
  }

  private parseAttributes(valueArg: string): Record<string, string> {
    const attrs: Record<string, string> = {};
    const attrRegex = /([A-Za-z0-9_:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
    for (const match of valueArg.matchAll(attrRegex)) {
      attrs[this.localName(match[1])] = this.decodeXml(match[2] ?? match[3] ?? '');
    }
    return attrs;
  }

  private absoluteUrl(baseArg: string | undefined, valueArg: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    try {
      return baseArg ? new URL(valueArg, baseArg).toString() : new URL(valueArg).toString();
    } catch {
      return undefined;
    }
  }

  private splitList(valueArg: string | undefined): string[] {
    return valueArg ? valueArg.split(',').map((itemArg) => itemArg.trim()).filter(Boolean) : [];
  }

  private 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;
  }

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

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || dlnaDmsDefaultTimeoutMs);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private formatHost(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

  private escapeRegex(valueArg: string): string {
    return valueArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  private escapeXml(valueArg: string): string {
    return valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
  }

  private decodeXml(valueArg: string): string {
    return valueArg.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
  }

  private localName(valueArg: string): string {
    return valueArg.includes(':') ? valueArg.split(':').pop() || valueArg : valueArg;
  }

  private slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'dlna_dms';
  }

  private cloneSnapshot(snapshotArg: IDlnaDmsSnapshot): IDlnaDmsSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IDlnaDmsSnapshot;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}
