import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import { ciscoIosAttribution, ciscoIosDefaultArpCommand, ciscoIosDefaultName, ciscoIosDefaultPresentAgeMinutes, ciscoIosDefaultSshPort, ciscoIosDefaultTerminalLengthCommand, ciscoIosDefaultUsername, ciscoIosDisplayName, ciscoIosDomain, ciscoIosManufacturer } from './cisco_ios.constants.js';
import type { ICiscoIosArpEntry, ICiscoIosCommand, ICiscoIosConfig, ICiscoIosEvent, ICiscoIosSnapshot, ICiscoIosTrackedDevice } from './cisco_ios.types.js';

export class CiscoIosMapper {
  public static toSnapshot(configArg: ICiscoIosConfig = {}, overridesArg: Partial<ICiscoIosSnapshot> = {}, eventsArg: ICiscoIosEvent[] = []): ICiscoIosSnapshot {
    const source = configArg.snapshot;
    const rawArpOutput = overridesArg.rawArpOutput ?? configArg.arpOutput ?? configArg.rawArpOutput ?? source?.rawArpOutput;
    const presentAgeMinutes = configArg.presentAgeMinutes ?? this.numberValue(source?.metadata?.presentAgeMinutes) ?? ciscoIosDefaultPresentAgeMinutes;
    const parsedEntries = rawArpOutput ? this.parseArpOutput(rawArpOutput, presentAgeMinutes) : [];
    const arpEntries = overridesArg.arpEntries || source?.arpEntries || parsedEntries;
    const trackedDevices = overridesArg.trackedDevices || source?.trackedDevices || this.trackedDevicesFromEntries(arpEntries);
    const host = overridesArg.device?.host || configArg.host || configArg.ipAddress || configArg.address || configArg.ip || source?.device.host;
    const port = overridesArg.device?.port ?? configArg.port ?? source?.device.port ?? ciscoIosDefaultSshPort;
    const name = overridesArg.device?.name || configArg.name || source?.device.name || host || ciscoIosDefaultName;
    const id = overridesArg.device?.id || configArg.id || source?.device.id || this.normalizeMac(configArg.macAddress || configArg.mac || source?.device.macAddress) || host || name;
    const connected = overridesArg.connected ?? configArg.connected ?? source?.connected ?? Boolean(rawArpOutput || arpEntries.length || trackedDevices.length);
    const sourceName = overridesArg.source || source?.source || (rawArpOutput ? 'manual' : 'runtime');
    const updatedAt = overridesArg.updatedAt || source?.updatedAt || new Date().toISOString();
    const terminalLength = this.terminalLengthCommand(configArg, source);
    const arpCommand = overridesArg.commands?.arp || configArg.arpCommand || source?.commands.arp || ciscoIosDefaultArpCommand;

    return {
      connected,
      source: sourceName,
      device: {
        id,
        name,
        host,
        port,
        username: overridesArg.device?.username || configArg.username || source?.device.username || ciscoIosDefaultUsername,
        manufacturer: overridesArg.device?.manufacturer || configArg.manufacturer || source?.device.manufacturer || ciscoIosManufacturer,
        modelName: overridesArg.device?.modelName || configArg.modelName || configArg.model || source?.device.modelName || ciscoIosDisplayName,
        modelId: overridesArg.device?.modelId || configArg.modelId || source?.device.modelId,
        firmwareVersion: overridesArg.device?.firmwareVersion || configArg.firmwareVersion || source?.device.firmwareVersion,
        macAddress: this.normalizeMac(overridesArg.device?.macAddress || configArg.macAddress || configArg.mac || source?.device.macAddress),
        configurationUrl: overridesArg.device?.configurationUrl || (host ? `ssh://${host}:${port}` : source?.device.configurationUrl),
        available: overridesArg.device?.available ?? source?.device.available ?? connected,
        authenticated: overridesArg.device?.authenticated ?? source?.device.authenticated,
        metadata: this.cleanAttributes({
          ...(source?.device.metadata || {}),
          ...(configArg.metadata || {}),
          ...(overridesArg.device?.metadata || {}),
          protocol: 'ssh',
          nativeSshImplemented: false,
        }),
      },
      commands: {
        terminalLength,
        arp: arpCommand,
      },
      arpEntries,
      trackedDevices,
      rawArpOutput,
      updatedAt,
      events: [...(source?.events || []), ...eventsArg, ...(overridesArg.events || [])],
      metadata: this.cleanAttributes({
        ...(source?.metadata || {}),
        ...(configArg.metadata || {}),
        ...(overridesArg.metadata || {}),
        presentAgeMinutes,
        upstreamScannerRule: 'Only ARP rows with numeric age less than 1 minute are considered present by Home Assistant.',
        nativeSshImplemented: false,
        commandExecutorConfigured: Boolean(configArg.commandExecutor || configArg.transport?.execute || configArg.nativeClient?.executeCommand || configArg.nativeClient?.execute),
        nativeClientConfigured: Boolean(configArg.nativeClient),
      }),
      error: overridesArg.error || source?.error,
    };
  }

  public static mergeSnapshot(baseArg: ICiscoIosSnapshot, patchArg: Partial<ICiscoIosSnapshot>): ICiscoIosSnapshot {
    return {
      ...baseArg,
      ...patchArg,
      device: { ...baseArg.device, ...patchArg.device, metadata: this.cleanAttributes({ ...(baseArg.device.metadata || {}), ...(patchArg.device?.metadata || {}) }) },
      commands: { ...baseArg.commands, ...patchArg.commands },
      arpEntries: patchArg.arpEntries || baseArg.arpEntries,
      trackedDevices: patchArg.trackedDevices || baseArg.trackedDevices,
      events: [...baseArg.events, ...(patchArg.events || [])],
      metadata: this.cleanAttributes({ ...(baseArg.metadata || {}), ...(patchArg.metadata || {}) }),
      updatedAt: patchArg.updatedAt || baseArg.updatedAt || new Date().toISOString(),
    };
  }

  public static parseArpOutput(outputArg: string, presentAgeMinutesArg = ciscoIosDefaultPresentAgeMinutes): ICiscoIosArpEntry[] {
    const entries: ICiscoIosArpEntry[] = [];
    for (const rawLine of outputArg.split(/\r?\n/)) {
      const line = rawLine.trim();
      if (!line) {
        continue;
      }
      const parts = line.split(/\s+/);
      if (parts.length < 6 || parts[0].toLowerCase() === 'protocol') {
        continue;
      }
      const [protocol, ipAddress, rawAge, hardwareAddress, type] = parts;
      const macAddress = this.parseCiscoMacAddress(hardwareAddress);
      if (!macAddress) {
        continue;
      }
      const ageMinutes = rawAge === '-' ? null : this.numberValue(rawAge);
      if (rawAge !== '-' && ageMinutes === undefined) {
        continue;
      }
      entries.push({
        protocol,
        ipAddress,
        ageMinutes,
        rawAge,
        hardwareAddress,
        macAddress,
        type,
        interfaceName: parts.slice(5).join(' '),
        present: ageMinutes !== null && ageMinutes !== undefined && ageMinutes < presentAgeMinutesArg,
        rawLine,
      });
    }
    return entries;
  }

  public static parseCiscoMacAddress(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^0-9a-f]/gi, '').toUpperCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : undefined;
  }

  public static trackedDevicesFromEntries(entriesArg: ICiscoIosArpEntry[]): ICiscoIosTrackedDevice[] {
    const devices = new Map<string, ICiscoIosTrackedDevice>();
    for (const entry of entriesArg) {
      if (!entry.present || !entry.macAddress) {
        continue;
      }
      const previous = devices.get(entry.macAddress);
      if (previous && (previous.ageMinutes ?? Number.POSITIVE_INFINITY) <= (entry.ageMinutes ?? Number.POSITIVE_INFINITY)) {
        continue;
      }
      devices.set(entry.macAddress, {
        id: entry.macAddress,
        macAddress: entry.macAddress,
        ipAddress: entry.ipAddress,
        interfaceName: entry.interfaceName,
        ageMinutes: entry.ageMinutes,
        connected: true,
        attributes: this.cleanAttributes({ protocol: entry.protocol, type: entry.type, rawAge: entry.rawAge }),
      });
    }
    return [...devices.values()];
  }

  public static toDevices(snapshotArg: ICiscoIosSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return [this.routerDevice(snapshotArg, updatedAt), ...snapshotArg.trackedDevices.map((deviceArg) => this.clientDevice(deviceArg, snapshotArg, updatedAt))];
  }

  public static toEntities(snapshotArg: ICiscoIosSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();
    const routerDeviceId = this.routerDeviceId(snapshotArg);
    const routerName = this.routerName(snapshotArg);
    entities.push(this.entity('binary_sensor', `${routerName} Connected`, routerDeviceId, `${this.uniqueBase(snapshotArg)}_connected`, snapshotArg.connected ? 'on' : 'off', usedIds, {
      deviceClass: 'connectivity',
      host: snapshotArg.device.host,
      port: snapshotArg.device.port,
      protocol: 'ssh',
      error: snapshotArg.error,
    }, true));
    entities.push(this.entity('sensor', `${routerName} Tracked Devices`, routerDeviceId, `${this.uniqueBase(snapshotArg)}_tracked_devices`, snapshotArg.trackedDevices.length, usedIds, {
      nativeType: 'tracked_devices',
      unit: 'devices',
      attribution: ciscoIosAttribution,
    }, snapshotArg.connected));
    entities.push(this.entity('sensor', `${routerName} ARP Entries`, routerDeviceId, `${this.uniqueBase(snapshotArg)}_arp_entries`, snapshotArg.arpEntries.length, usedIds, {
      nativeType: 'arp_entries',
      unit: 'entries',
      attribution: ciscoIosAttribution,
    }, snapshotArg.connected));

    for (const device of snapshotArg.trackedDevices) {
      this.pushTrackedDeviceEntities(entities, snapshotArg, device, usedIds);
    }
    return entities;
  }

  public static commandForService(snapshotArg: ICiscoIosSnapshot, requestArg: IServiceCallRequest): ICiscoIosCommand | undefined {
    if (requestArg.domain !== ciscoIosDomain) {
      return undefined;
    }
    const targetMatchesDevice = !requestArg.target.deviceId || requestArg.target.deviceId === this.routerDeviceId(snapshotArg);
    if (!targetMatchesDevice) {
      return undefined;
    }
    if (requestArg.service === 'show_ip_arp') {
      return this.command(snapshotArg, requestArg, 'show_ip_arp', snapshotArg.commands.arp);
    }
    if (requestArg.service === 'run_command') {
      const command = this.stringValue(requestArg.data?.command);
      return command ? this.command(snapshotArg, requestArg, 'run_command', command) : undefined;
    }
    return undefined;
  }

  public static toIntegrationEvent(eventArg: ICiscoIosEvent): IIntegrationEvent {
    return {
      type: eventArg.type === 'command_failed' ? 'error' : 'state_changed',
      integrationDomain: ciscoIosDomain,
      deviceId: eventArg.deviceId,
      entityId: eventArg.entityId,
      data: eventArg,
      timestamp: eventArg.timestamp || Date.now(),
    };
  }

  public static routerDeviceId(snapshotArg: ICiscoIosSnapshot): string {
    return `cisco_ios.router.${this.uniqueBase(snapshotArg)}`;
  }

  public static clientDeviceId(deviceArg: ICiscoIosTrackedDevice): string {
    return `cisco_ios.client.${this.slug(deviceArg.macAddress || deviceArg.id || deviceArg.ipAddress || 'device')}`;
  }

  public static normalizeMac(valueArg?: string): string | undefined {
    return this.parseCiscoMacAddress(valueArg);
  }

  public static slug(valueArg: unknown): string {
    const slug = String(valueArg || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
    return slug || 'cisco_ios';
  }

  public static cleanAttributes<TValue extends Record<string, unknown>>(valueArg: TValue): TValue {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(valueArg)) {
      if (value !== undefined) {
        result[key] = value;
      }
    }
    return result as TValue;
  }

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

  private static terminalLengthCommand(configArg: ICiscoIosConfig, sourceArg?: ICiscoIosSnapshot): string | undefined {
    if (configArg.terminalLengthCommand === false) {
      return undefined;
    }
    return configArg.terminalLengthCommand || sourceArg?.commands.terminalLength || ciscoIosDefaultTerminalLengthCommand;
  }

  private static routerDevice(snapshotArg: ICiscoIosSnapshot, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    return {
      id: this.routerDeviceId(snapshotArg),
      integrationDomain: ciscoIosDomain,
      name: this.routerName(snapshotArg),
      protocol: 'unknown',
      manufacturer: snapshotArg.device.manufacturer || ciscoIosManufacturer,
      model: snapshotArg.device.modelName || ciscoIosDisplayName,
      online: snapshotArg.connected && snapshotArg.device.available !== false,
      features: [
        { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
        { id: 'tracked_devices', capability: 'sensor', name: 'Tracked Devices', readable: true, writable: false },
        { id: 'arp_entries', capability: 'sensor', name: 'ARP Entries', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connectivity', value: snapshotArg.connected ? 'online' : 'offline', updatedAt: updatedAtArg },
        { featureId: 'tracked_devices', value: snapshotArg.trackedDevices.length, updatedAt: updatedAtArg },
        { featureId: 'arp_entries', value: snapshotArg.arpEntries.length, updatedAt: updatedAtArg },
      ],
      metadata: this.cleanAttributes({
        host: snapshotArg.device.host,
        port: snapshotArg.device.port,
        username: snapshotArg.device.username,
        protocol: 'ssh',
        configurationUrl: snapshotArg.device.configurationUrl,
        source: snapshotArg.source,
        error: snapshotArg.error,
        nativeSshImplemented: false,
        ...snapshotArg.metadata,
      }),
    };
  }

  private static clientDevice(deviceArg: ICiscoIosTrackedDevice, snapshotArg: ICiscoIosSnapshot, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    return {
      id: this.clientDeviceId(deviceArg),
      integrationDomain: ciscoIosDomain,
      name: this.clientName(deviceArg),
      protocol: 'unknown',
      manufacturer: 'Unknown',
      model: 'Network client',
      online: snapshotArg.connected && deviceArg.connected,
      features: [
        { id: 'presence', capability: 'sensor', name: 'Presence', readable: true, writable: false },
        { id: 'ip_address', capability: 'sensor', name: 'IP Address', readable: true, writable: false },
        { id: 'arp_age', capability: 'sensor', name: 'ARP Age', readable: true, writable: false, unit: 'min' },
      ],
      state: [
        { featureId: 'presence', value: deviceArg.connected, updatedAt: updatedAtArg },
        { featureId: 'ip_address', value: deviceArg.ipAddress || null, updatedAt: updatedAtArg },
        { featureId: 'arp_age', value: deviceArg.ageMinutes ?? null, updatedAt: updatedAtArg },
      ],
      metadata: this.cleanAttributes({
        macAddress: deviceArg.macAddress,
        ipAddress: deviceArg.ipAddress,
        interfaceName: deviceArg.interfaceName,
        sourceRouter: this.routerDeviceId(snapshotArg),
        ...deviceArg.attributes,
      }),
    };
  }

  private static pushTrackedDeviceEntities(entitiesArg: IIntegrationEntity[], snapshotArg: ICiscoIosSnapshot, deviceArg: ICiscoIosTrackedDevice, usedIdsArg: Map<string, number>): void {
    const deviceId = this.clientDeviceId(deviceArg);
    const base = this.slug(deviceArg.macAddress);
    entitiesArg.push(this.entity('binary_sensor', `${this.clientName(deviceArg)} Present`, deviceId, `${base}_present`, deviceArg.connected ? 'on' : 'off', usedIdsArg, {
      deviceClass: 'presence',
      macAddress: deviceArg.macAddress,
      ipAddress: deviceArg.ipAddress,
      interfaceName: deviceArg.interfaceName,
      attribution: ciscoIosAttribution,
    }, snapshotArg.connected));
    entitiesArg.push(this.entity('sensor', `${this.clientName(deviceArg)} IP Address`, deviceId, `${base}_ip_address`, deviceArg.ipAddress || null, usedIdsArg, {
      nativeType: 'ip_address',
      macAddress: deviceArg.macAddress,
    }, snapshotArg.connected));
    entitiesArg.push(this.entity('sensor', `${this.clientName(deviceArg)} ARP Age`, deviceId, `${base}_arp_age`, deviceArg.ageMinutes ?? null, usedIdsArg, {
      nativeType: 'arp_age',
      unit: 'min',
      macAddress: deviceArg.macAddress,
      interfaceName: deviceArg.interfaceName,
    }, snapshotArg.connected));
  }

  private static command(snapshotArg: ICiscoIosSnapshot, requestArg: IServiceCallRequest, actionArg: ICiscoIosCommand['action'], commandArg: string): ICiscoIosCommand {
    return {
      action: actionArg,
      command: commandArg,
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      deviceId: this.routerDeviceId(snapshotArg),
      host: snapshotArg.device.host,
      transmitted: false,
      metadata: this.cleanAttributes({ source: snapshotArg.source, presentAgeMinutes: snapshotArg.metadata?.presentAgeMinutes }),
    };
  }

  private static entity(platformArg: TEntityPlatform, nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: unknown, usedIdsArg: Map<string, number>, attributesArg: Record<string, unknown> = {}, availableArg = true): IIntegrationEntity {
    const baseId = `${platformArg}.${this.slug(nameArg)}`;
    const used = usedIdsArg.get(baseId) || 0;
    usedIdsArg.set(baseId, used + 1);
    return {
      id: used ? `${baseId}_${used + 1}` : baseId,
      uniqueId: `${ciscoIosDomain}_${this.slug(uniqueIdArg)}`,
      integrationDomain: ciscoIosDomain,
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static routerName(snapshotArg: ICiscoIosSnapshot): string {
    return snapshotArg.device.name || snapshotArg.device.host || ciscoIosDefaultName;
  }

  private static clientName(deviceArg: ICiscoIosTrackedDevice): string {
    return deviceArg.name || deviceArg.hostname || deviceArg.macAddress || deviceArg.ipAddress || 'Network client';
  }

  private static uniqueBase(snapshotArg: ICiscoIosSnapshot): string {
    return this.slug(snapshotArg.device.macAddress || snapshotArg.device.id || snapshotArg.device.host || this.routerName(snapshotArg));
  }

  private static stringValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string' && valueArg.trim()) {
      return valueArg.trim();
    }
    return undefined;
  }
}
