import type {
  IIppAttributeRecord,
  IIppClientLike,
  IIppConfig,
  IIppJobInfo,
  IIppMarkerInfo,
  IIppParsedResponse,
  IIppPrinterInfo,
  IIppSnapshot,
  IIppStatusInfo,
  TIppMarkerKind,
  TIppPrinterState,
  TIppSnapshotSource,
} from './ipp.types.js';
import { ippDefaultBasePath, ippDefaultPort, ippDefaultTimeoutMs } from './ipp.types.js';

const operationGetPrinterAttributes = 0x000b;
const groupOperationAttributes = 0x01;
const endOfAttributes = 0x03;
const valueTagInteger = 0x21;
const valueTagBoolean = 0x22;
const valueTagEnum = 0x23;
const valueTagDateTime = 0x31;
const valueTagKeyword = 0x44;
const valueTagCharset = 0x47;
const valueTagNaturalLanguage = 0x48;

const requestedPrinterAttributes = [
  'printer-name',
  'printer-info',
  'printer-location',
  'printer-make-and-model',
  'printer-device-id',
  'printer-uuid',
  'printer-serial-number',
  'printer-more-info',
  'printer-uri-supported',
  'document-format-supported',
  'printer-state',
  'printer-state-message',
  'printer-state-reasons',
  'printer-is-accepting-jobs',
  'queued-job-count',
  'printer-up-time',
  'printer-current-time',
  'marker-names',
  'marker-types',
  'marker-colors',
  'marker-levels',
  'marker-low-levels',
  'marker-high-levels',
];

export class IppClient {
  constructor(private readonly config: IIppConfig) {}

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

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

    if (this.config.attributes) {
      return this.normalizeSnapshot(this.snapshotFromAttributes(this.config.attributes, this.config.online ?? true, 'manual'), 'manual');
    }

    if (this.config.host) {
      try {
        return this.normalizeSnapshot(await this.fetchSnapshot(), 'ipp');
      } catch (errorArg) {
        return this.normalizeSnapshot(this.snapshotFromConfig(false, errorArg instanceof Error ? errorArg.message : String(errorArg)), 'runtime');
      }
    }

    return this.normalizeSnapshot(this.snapshotFromConfig(false, 'IPP refresh requires config.host, config.snapshot, config.attributes, or config.client.'), 'runtime');
  }

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

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

  public hasUsableSource(): boolean {
    return Boolean(this.config.host || this.config.snapshot || this.config.attributes || this.config.client);
  }

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

  public static parseResponse(dataArg: Uint8Array): IIppParsedResponse {
    if (dataArg.length < 8) {
      throw new Error('IPP response is too short.');
    }

    const data = Buffer.from(dataArg.buffer, dataArg.byteOffset, dataArg.byteLength);
    const attributes: IIppAttributeRecord = {};
    const version = `${data[0]}.${data[1]}`;
    const statusCode = data.readUInt16BE(2);
    const requestId = data.readUInt32BE(4);
    let offset = 8;
    let lastName = '';

    while (offset < data.length) {
      const tag = data[offset++];
      if (tag === endOfAttributes) {
        break;
      }
      if (isDelimiterTag(tag)) {
        lastName = '';
        continue;
      }

      if (offset + 2 > data.length) {
        throw new Error('IPP response ended while reading attribute name length.');
      }
      const nameLength = data.readUInt16BE(offset);
      offset += 2;
      if (offset + nameLength + 2 > data.length) {
        throw new Error('IPP response ended while reading attribute name.');
      }
      const name = nameLength ? data.subarray(offset, offset + nameLength).toString('utf8') : lastName;
      offset += nameLength;
      const valueLength = data.readUInt16BE(offset);
      offset += 2;
      if (offset + valueLength > data.length) {
        throw new Error('IPP response ended while reading attribute value.');
      }
      const valueBytes = data.subarray(offset, offset + valueLength);
      offset += valueLength;

      if (!name) {
        continue;
      }
      lastName = name;
      addAttributeValue(attributes, name, parseIppValue(tag, valueBytes));
    }

    return { version, statusCode, requestId, attributes };
  }

  public static attributesToSnapshot(attributesArg: IIppAttributeRecord, configArg: Partial<IIppConfig> = {}, onlineArg = true, sourceArg: TIppSnapshotSource = 'manual', statusCodeArg?: number): IIppSnapshot {
    return new IppClient(configArg).snapshotFromAttributes(attributesArg, onlineArg, sourceArg, statusCodeArg);
  }

  private async snapshotFromClient(clientArg: IIppClientLike): Promise<IIppSnapshot> {
    const result = clientArg.getSnapshot ? await clientArg.getSnapshot() : clientArg.printer ? await clientArg.printer() : undefined;
    if (!result) {
      throw new Error('IPP client must expose getSnapshot() or printer().');
    }
    if (isIppSnapshot(result)) {
      return result;
    }
    return this.snapshotFromAttributes(result, true, 'client');
  }

  private async fetchSnapshot(): Promise<IIppSnapshot> {
    const requestBody = this.buildGetPrinterAttributesRequest();
    const response = await this.fetchWithTimeout(this.endpointUrl(), {
      method: 'POST',
      headers: {
        accept: 'application/ipp',
        'content-type': 'application/ipp',
      },
      body: requestBody.buffer.slice(requestBody.byteOffset, requestBody.byteOffset + requestBody.byteLength) as ArrayBuffer,
    });
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new Error(`IPP printer request failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }

    const parsed = IppClient.parseResponse(new Uint8Array(await response.arrayBuffer()));
    if (parsed.statusCode < 0x0000 || parsed.statusCode >= 0x0400) {
      throw new Error(`IPP printer returned status 0x${parsed.statusCode.toString(16).padStart(4, '0')}.`);
    }
    return this.snapshotFromAttributes(parsed.attributes, true, 'ipp', parsed.statusCode);
  }

  private snapshotFromAttributes(attributesArg: IIppAttributeRecord, onlineArg: boolean, sourceArg: TIppSnapshotSource, statusCodeArg?: number): IIppSnapshot {
    const attributes = normalizeAttributes(attributesArg);
    const updatedAt = new Date().toISOString();
    const deviceId = stringValue(firstValue(attributes['printer-device-id']));
    const deviceInfo = parseDeviceId(deviceId);
    const makeAndModel = stringValue(firstValue(attributes['printer-make-and-model']));
    const uuid = this.config.uuid || stringValue(firstValue(attributes['printer-uuid']));
    const serialNumber = this.config.serial || stringValue(firstValue(attributes['printer-serial-number'])) || deviceInfo.SERIALNUMBER || deviceInfo.SN || deviceInfo.SERN;
    const printerState = printerStateValue(firstValue(attributes['printer-state']));
    const currentTime = dateString(firstValue(attributes['printer-current-time']));
    const uptimeSeconds = numberValue(firstValue(attributes['printer-up-time']));
    const bootedAt = uptimeSeconds !== undefined ? new Date(Date.parse(currentTime || updatedAt) - uptimeSeconds * 1000).toISOString() : undefined;
    const parsed = parsedHost(this.config.host);
    const host = parsed?.host || this.config.host;
    const port = this.config.port || parsed?.port || (host ? ippDefaultPort : undefined);
    const basePath = this.basePath();
    const name = this.config.name || stringValue(firstValue(attributes['printer-name'])) || stringValue(firstValue(attributes['printer-info'])) || makeAndModel || host || 'IPP printer';
    const printer: IIppPrinterInfo = {
      id: this.config.uniqueId || uuid || serialNumber || (host ? `${host}:${port}${basePath}` : undefined) || name,
      name,
      manufacturer: this.config.manufacturer || deviceInfo.MFG || deviceInfo.MANUFACTURER || manufacturerFromMakeAndModel(makeAndModel),
      model: this.config.model || deviceInfo.MDL || deviceInfo.MODEL || makeAndModel,
      serialNumber,
      uuid,
      version: stringValue(firstValue(attributes['printer-firmware-name'])) || deviceInfo.VERSION,
      location: stringValue(firstValue(attributes['printer-location'])),
      info: stringValue(firstValue(attributes['printer-info'])),
      moreInfo: stringValue(firstValue(attributes['printer-more-info'])),
      makeAndModel,
      deviceId,
      commandSet: splitList(deviceInfo.CMD || deviceInfo.COMMANDSET || deviceInfo['COMMAND SET']),
      uriSupported: stringValues(attributes['printer-uri-supported']),
      host,
      port,
      basePath,
      tls: this.tls(),
    };
    const status: IIppStatusInfo = {
      printerState,
      stateMessage: stringValue(firstValue(attributes['printer-state-message'])),
      stateReasons: stringValues(attributes['printer-state-reasons']),
      acceptingJobs: booleanValue(firstValue(attributes['printer-is-accepting-jobs'])),
      queuedJobCount: numberValue(firstValue(attributes['queued-job-count'])),
      uptimeSeconds,
      bootedAt,
      currentTime,
    };

    return {
      printer,
      status,
      markers: this.markersFromAttributes(attributes),
      jobs: this.jobsFromAttributes(attributes),
      attributes,
      online: onlineArg,
      updatedAt,
      source: sourceArg,
      rawStatusCode: statusCodeArg,
    };
  }

  private snapshotFromConfig(onlineArg: boolean, errorArg?: string): IIppSnapshot {
    const parsed = parsedHost(this.config.host);
    const host = parsed?.host || this.config.host;
    const port = this.config.port || parsed?.port || (host ? ippDefaultPort : undefined);
    const basePath = this.basePath();
    const name = this.config.name || this.config.model || host || 'IPP printer';
    return {
      printer: {
        id: this.config.uniqueId || this.config.uuid || this.config.serial || (host ? `${host}:${port}${basePath}` : undefined) || name,
        name,
        manufacturer: this.config.manufacturer,
        model: this.config.model,
        serialNumber: this.config.serial,
        uuid: this.config.uuid,
        host,
        port,
        basePath,
        tls: this.tls(),
      },
      status: {
        printerState: 'unknown',
        stateReasons: [],
      },
      markers: [],
      jobs: [],
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }

  private normalizeSnapshot(snapshotArg: IIppSnapshot, sourceArg: TIppSnapshotSource): IIppSnapshot {
    const derived = snapshotArg.attributes ? this.snapshotFromAttributes(snapshotArg.attributes, snapshotArg.online, sourceArg, snapshotArg.rawStatusCode) : undefined;
    const printer = {
      ...derived?.printer,
      ...snapshotArg.printer,
    };
    printer.name = printer.name || this.config.name || this.config.host || 'IPP printer';
    printer.id = printer.id || this.config.uniqueId || printer.uuid || printer.serialNumber || printer.name;
    printer.host = printer.host || this.config.host;
    printer.port = printer.port || (printer.host ? this.config.port || ippDefaultPort : this.config.port);
    printer.basePath = printer.basePath || this.basePath();
    printer.tls = printer.tls ?? this.tls();

    return {
      ...snapshotArg,
      printer,
      status: this.normalizeStatus(derived?.status, snapshotArg.status),
      markers: snapshotArg.markers?.length ? snapshotArg.markers : derived?.markers || [],
      jobs: snapshotArg.jobs?.length ? snapshotArg.jobs : derived?.jobs || [],
      attributes: snapshotArg.attributes || derived?.attributes,
      online: snapshotArg.online,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: snapshotArg.source || sourceArg,
    };
  }

  private markersFromAttributes(attributesArg: IIppAttributeRecord): IIppMarkerInfo[] {
    const names = stringValues(attributesArg['marker-names']);
    const types = stringValues(attributesArg['marker-types']);
    const colors = stringValues(attributesArg['marker-colors']);
    const levels = numberValues(attributesArg['marker-levels']);
    const lowLevels = numberValues(attributesArg['marker-low-levels']);
    const highLevels = numberValues(attributesArg['marker-high-levels']);
    const count = Math.max(names.length, types.length, colors.length, levels.length, lowLevels.length, highLevels.length);
    const markers: IIppMarkerInfo[] = [];
    for (let index = 0; index < count; index++) {
      const type = types[index];
      const level = levels[index];
      markers.push({
        id: `marker_${index}`,
        index,
        name: names[index] || markerName(type, colors[index], index),
        kind: markerKind(type, names[index]),
        type,
        color: colors[index],
        level: level !== undefined && level >= 0 ? level : level !== undefined ? null : undefined,
        lowLevel: lowLevels[index],
        highLevel: highLevels[index],
      });
    }
    return markers;
  }

  private jobsFromAttributes(attributesArg: IIppAttributeRecord): IIppJobInfo[] {
    const ids = stringValues(attributesArg['job-id']);
    const names = stringValues(attributesArg['job-name']);
    const states = values(attributesArg['job-state']).map((valueArg) => jobStateValue(valueArg));
    const owners = stringValues(attributesArg['job-originating-user-name']);
    const impressionsCompleted = numberValues(attributesArg['job-impressions-completed']);
    const createdAt = values(attributesArg['time-at-creation']).map((valueArg) => dateString(valueArg));
    const processingAt = values(attributesArg['time-at-processing']).map((valueArg) => dateString(valueArg));
    const completedAt = values(attributesArg['time-at-completed']).map((valueArg) => dateString(valueArg));
    const count = Math.max(ids.length, names.length, states.length, owners.length, impressionsCompleted.length, createdAt.length, processingAt.length, completedAt.length);
    const jobs: IIppJobInfo[] = [];
    for (let index = 0; index < count; index++) {
      jobs.push({
        id: ids[index] || String(index + 1),
        name: names[index],
        state: states[index],
        owner: owners[index],
        impressionsCompleted: impressionsCompleted[index],
        createdAt: createdAt[index],
        processingAt: processingAt[index],
        completedAt: completedAt[index],
      });
    }
    return jobs;
  }

  private buildGetPrinterAttributesRequest(): Buffer {
    const chunks: Buffer[] = [];
    chunks.push(Buffer.from([0x01, 0x01]));
    chunks.push(uint16(operationGetPrinterAttributes));
    chunks.push(uint32(Math.floor(Math.random() * 0x7fffffff) + 1));
    chunks.push(Buffer.from([groupOperationAttributes]));
    writeAttribute(chunks, valueTagCharset, 'attributes-charset', 'utf-8');
    writeAttribute(chunks, valueTagNaturalLanguage, 'attributes-natural-language', 'en');
    writeAttribute(chunks, 0x45, 'printer-uri', this.printerUri());
    requestedPrinterAttributes.forEach((attributeArg, indexArg) => writeAttribute(chunks, valueTagKeyword, indexArg === 0 ? 'requested-attributes' : '', attributeArg));
    chunks.push(Buffer.from([endOfAttributes]));
    return Buffer.concat(chunks);
  }

  private endpointUrl(): string {
    const host = this.config.host;
    if (!host) {
      throw new Error('IPP host is required for live printer refresh.');
    }
    const parsed = parsedHost(host);
    const tls = parsed?.tls ?? this.tls();
    const endpointHost = parsed?.host || host;
    const port = this.config.port || parsed?.port || ippDefaultPort;
    const basePath = this.config.basePath || parsed?.basePath || ippDefaultBasePath;
    return `${tls ? 'https' : 'http'}://${formatHost(endpointHost)}:${port}${normalizeBasePath(basePath)}`;
  }

  private printerUri(): string {
    const host = this.config.host;
    if (!host) {
      throw new Error('IPP host is required for printer-uri.');
    }
    const parsed = parsedHost(host);
    const tls = parsed?.tls ?? this.tls();
    const endpointHost = parsed?.host || host;
    const port = this.config.port || parsed?.port || ippDefaultPort;
    const basePath = this.config.basePath || parsed?.basePath || ippDefaultBasePath;
    return `${tls ? 'ipps' : 'ipp'}://${formatHost(endpointHost)}:${port}${normalizeBasePath(basePath)}`;
  }

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

  private basePath(): string {
    return normalizeBasePath(this.config.basePath || parsedHost(this.config.host)?.basePath || ippDefaultBasePath);
  }

  private tls(): boolean {
    return this.config.tls ?? this.config.ssl ?? parsedHost(this.config.host)?.tls ?? false;
  }

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

  private normalizeStatus(derivedArg: IIppStatusInfo | undefined, snapshotArg: IIppStatusInfo): IIppStatusInfo {
    return {
      ...derivedArg,
      ...snapshotArg,
      printerState: snapshotArg.printerState || derivedArg?.printerState || 'unknown',
      stateReasons: snapshotArg.stateReasons || derivedArg?.stateReasons || [],
    };
  }
}

const isDelimiterTag = (tagArg: number): boolean => tagArg >= 0x01 && tagArg <= 0x0f;

const writeAttribute = (chunksArg: Buffer[], tagArg: number, nameArg: string, valueArg: string): void => {
  const name = Buffer.from(nameArg, 'utf8');
  const value = Buffer.from(valueArg, 'utf8');
  chunksArg.push(Buffer.from([tagArg]));
  chunksArg.push(uint16(name.length));
  chunksArg.push(name);
  chunksArg.push(uint16(value.length));
  chunksArg.push(value);
};

const uint16 = (valueArg: number): Buffer => {
  const buffer = Buffer.alloc(2);
  buffer.writeUInt16BE(valueArg, 0);
  return buffer;
};

const uint32 = (valueArg: number): Buffer => {
  const buffer = Buffer.alloc(4);
  buffer.writeUInt32BE(valueArg, 0);
  return buffer;
};

const parseIppValue = (tagArg: number, valueArg: Buffer): unknown => {
  if (tagArg === 0x10 || tagArg === 0x12 || tagArg === 0x13) {
    return null;
  }
  if ((tagArg === valueTagInteger || tagArg === valueTagEnum) && valueArg.length >= 4) {
    return valueArg.readInt32BE(0);
  }
  if (tagArg === valueTagBoolean && valueArg.length >= 1) {
    return valueArg[0] !== 0;
  }
  if (tagArg === valueTagDateTime && valueArg.length >= 11) {
    return parseIppDateTime(valueArg);
  }
  if (tagArg === 0x33 && valueArg.length >= 8) {
    return { lower: valueArg.readInt32BE(0), upper: valueArg.readInt32BE(4) };
  }
  if (tagArg === 0x32 && valueArg.length >= 9) {
    return { x: valueArg.readInt32BE(0), y: valueArg.readInt32BE(4), units: valueArg[8] === 3 ? 'dpi' : 'dpcm' };
  }
  if (tagArg >= 0x40 || tagArg === 0x30 || tagArg === 0x35 || tagArg === 0x36) {
    return valueArg.toString('utf8');
  }
  return valueArg.toString('hex');
};

const parseIppDateTime = (valueArg: Buffer): string | undefined => {
  const year = valueArg.readUInt16BE(0);
  const month = valueArg[2];
  const day = valueArg[3];
  const hour = valueArg[4];
  const minute = valueArg[5];
  const second = valueArg[6];
  const decisecond = valueArg[7];
  const direction = String.fromCharCode(valueArg[8]);
  const offsetMinutes = valueArg[9] * 60 + valueArg[10];
  const local = Date.UTC(year, Math.max(0, month - 1), day, hour, minute, second, decisecond * 100);
  const adjustment = direction === '+' ? -offsetMinutes : direction === '-' ? offsetMinutes : 0;
  const timestamp = local + adjustment * 60_000;
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
};

const addAttributeValue = (attributesArg: IIppAttributeRecord, nameArg: string, valueArg: unknown): void => {
  const existing = attributesArg[nameArg];
  if (existing === undefined) {
    attributesArg[nameArg] = valueArg;
    return;
  }
  if (Array.isArray(existing)) {
    existing.push(valueArg);
    return;
  }
  attributesArg[nameArg] = [existing, valueArg];
};

const normalizeAttributes = (attributesArg: IIppAttributeRecord): IIppAttributeRecord => {
  const normalized: IIppAttributeRecord = {};
  for (const [key, value] of Object.entries(attributesArg || {})) {
    normalized[key.trim()] = value;
  }
  return normalized;
};

const isIppSnapshot = (valueArg: IIppSnapshot | IIppAttributeRecord): valueArg is IIppSnapshot => {
  return Boolean(valueArg && typeof valueArg === 'object' && 'printer' in valueArg && 'status' in valueArg && 'markers' in valueArg && 'online' in valueArg);
};

const values = (valueArg: unknown): unknown[] => Array.isArray(valueArg) ? valueArg : valueArg === undefined ? [] : [valueArg];

const firstValue = (valueArg: unknown): unknown => values(valueArg)[0];

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

const stringValues = (valueArg: unknown): string[] => values(valueArg).map((itemArg) => stringValue(itemArg)).filter((itemArg): itemArg is string => Boolean(itemArg));

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg.match(/[-+]?\d+(?:\.\d+)?/)?.[0]);
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

const numberValues = (valueArg: unknown): number[] => values(valueArg).map((itemArg) => numberValue(itemArg)).filter((itemArg): itemArg is number => itemArg !== undefined);

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

const dateString = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'string') {
    const parsed = Date.parse(valueArg);
    return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
  }
  return undefined;
};

const printerStateValue = (valueArg: unknown): TIppPrinterState => {
  if (typeof valueArg === 'number') {
    return valueArg === 3 ? 'idle' : valueArg === 4 ? 'printing' : valueArg === 5 ? 'stopped' : 'unknown';
  }
  const value = stringValue(valueArg)?.toLowerCase();
  return value === 'processing' ? 'printing' : value === 'idle' || value === 'printing' || value === 'stopped' ? value : value || 'unknown';
};

const jobStateValue = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'number') {
    return ({ 3: 'pending', 4: 'pending-held', 5: 'processing', 6: 'processing-stopped', 7: 'canceled', 8: 'aborted', 9: 'completed' } as Record<number, string>)[valueArg] || String(valueArg);
  }
  return stringValue(valueArg);
};

const parseDeviceId = (valueArg: string | undefined): Record<string, string> => {
  const result: Record<string, string> = {};
  for (const part of (valueArg || '').split(';')) {
    const separator = part.indexOf(':');
    if (separator <= 0) {
      continue;
    }
    const key = part.slice(0, separator).trim().toUpperCase().replace(/[\s-]+/g, '');
    const value = part.slice(separator + 1).trim();
    if (key && value) {
      result[key] = value;
    }
  }
  return result;
};

const splitList = (valueArg: string | undefined): string[] => (valueArg || '').split(/[,;]/).map((partArg) => partArg.trim()).filter(Boolean);

const manufacturerFromMakeAndModel = (valueArg: string | undefined): string | undefined => {
  const firstWord = valueArg?.trim().split(/\s+/)[0];
  return firstWord && firstWord.length > 1 ? firstWord : undefined;
};

const markerKind = (typeArg: string | undefined, nameArg: string | undefined): TIppMarkerKind => {
  const haystack = `${typeArg || ''} ${nameArg || ''}`.toLowerCase();
  if (haystack.includes('toner')) {
    return 'toner';
  }
  if (haystack.includes('ink')) {
    return 'ink';
  }
  if (haystack.includes('drum') || haystack.includes('opc')) {
    return 'drum';
  }
  if (haystack.includes('waste')) {
    return 'waste';
  }
  return 'marker';
};

const markerName = (typeArg: string | undefined, colorArg: string | undefined, indexArg: number): string => {
  const parts = [colorArg, typeArg].filter(Boolean);
  return parts.length ? parts.join(' ') : `Marker ${indexArg + 1}`;
};

const normalizeBasePath = (valueArg: string | undefined): string => {
  const value = (valueArg || ippDefaultBasePath).trim() || ippDefaultBasePath;
  return value.startsWith('/') ? value : `/${value}`;
};

const parsedHost = (valueArg: string | undefined): { host: string; port?: number; basePath?: string; tls?: boolean } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg.replace(/^ipps:/i, 'https:').replace(/^ipp:/i, 'http:'));
    return {
      host: url.hostname,
      port: url.port ? Number(url.port) : undefined,
      basePath: url.pathname && url.pathname !== '/' ? url.pathname : undefined,
      tls: url.protocol === 'https:',
    };
  } catch {
    return undefined;
  }
};

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