import * as plugins from '../plugins.js';
import { BaseIntegration } from './classes.baseintegration.js';
import { DiscoveryDescriptor } from './classes.discoverydescriptor.js';
import type {
  IConfigFlow,
  IConfigFlowContext,
  IConfigFlowField,
  IConfigFlowStep,
  IDiscoveryCandidate,
  IDiscoveryContext,
  IDiscoveryMatch,
  IDiscoveryMatcher,
  IDiscoveryValidator,
  IIntegrationEntity,
  IIntegrationRuntime,
  IIntegrationSetupContext,
  IServiceCallRequest,
  IServiceCallResult,
  TDiscoverySource,
  TEntityPlatform,
  TIntegrationStatus,
} from './types.js';

export type TSimpleLocalProtocol = 'http' | 'https' | 'tcp' | 'telnet' | 'websocket' | 'mqtt' | 'upnp' | 'local' | 'unknown';
export type TSimpleLocalSnapshotSource = 'snapshot' | 'manual' | 'client' | 'provider' | 'http' | 'tcp' | 'file' | 'runtime';
export type TSimpleLocalRawData = string | number | boolean | null | Record<string, unknown> | Array<Record<string, unknown>>;

export interface ISimpleLocalDeviceInfo {
  id?: string;
  name?: string;
  manufacturer?: string;
  model?: string;
  serialNumber?: string;
  host?: string;
  port?: number;
  protocol?: TSimpleLocalProtocol;
  configurationUrl?: string;
  attributes?: Record<string, unknown>;
}

export interface ISimpleLocalEntitySnapshot {
  id?: string;
  uniqueId?: string;
  name: string;
  platform?: TEntityPlatform;
  state: unknown;
  available?: boolean;
  writable?: boolean;
  unit?: string;
  deviceClass?: string;
  stateClass?: string;
  attributes?: Record<string, unknown>;
}

export interface ISimpleLocalSnapshot {
  device: ISimpleLocalDeviceInfo;
  entities: ISimpleLocalEntitySnapshot[];
  online: boolean;
  updatedAt: string;
  source: TSimpleLocalSnapshotSource;
  rawData?: unknown;
  error?: string;
  capabilities?: Record<string, unknown>;
}

export interface ISimpleLocalClientLike {
  getSnapshot?(): Promise<ISimpleLocalSnapshot | TSimpleLocalRawData> | ISimpleLocalSnapshot | TSimpleLocalRawData;
  getState?(): Promise<TSimpleLocalRawData> | TSimpleLocalRawData;
  getRawData?(): Promise<TSimpleLocalRawData> | TSimpleLocalRawData;
  execute?(requestArg: IServiceCallRequest): Promise<IServiceCallResult | unknown> | IServiceCallResult | unknown;
  destroy?(): Promise<void> | void;
}

export interface ISimpleLocalCommandExecutor {
  execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult | unknown> | IServiceCallResult | unknown;
}

export interface ISimpleLocalConfig {
  host?: string;
  port?: number;
  path?: string;
  useTls?: boolean;
  username?: string;
  password?: string;
  token?: string;
  apiKey?: string;
  name?: string;
  uniqueId?: string;
  transport?: 'http' | 'tcp' | 'snapshot';
  timeoutMs?: number;
  tcpCommand?: string;
  snapshot?: ISimpleLocalSnapshot;
  rawData?: TSimpleLocalRawData;
  rawText?: string;
  entities?: ISimpleLocalEntitySnapshot[];
  online?: boolean;
  client?: ISimpleLocalClientLike;
  snapshotProvider?: () => Promise<ISimpleLocalSnapshot | TSimpleLocalRawData | undefined> | ISimpleLocalSnapshot | TSimpleLocalRawData | undefined;
  commandExecutor?: ISimpleLocalCommandExecutor;
  metadata?: Record<string, unknown>;
  [key: string]: unknown;
}

export interface ISimpleLocalDiscoveryRecord extends ISimpleLocalConfig {
  source?: TDiscoverySource;
  integrationDomain?: string;
  id?: string;
  manufacturer?: string;
  model?: string;
  serialNumber?: string;
  macAddress?: string;
  url?: string;
  location?: string;
  ssdpLocation?: string;
  ssdp_location?: string;
  st?: string;
  usn?: string;
  type?: string;
  serviceType?: string;
  fullname?: string;
  txt?: Record<string, string | undefined>;
  properties?: Record<string, string | undefined>;
}

export interface ISimpleLocalIntegrationProfile {
  domain: string;
  displayName: string;
  manufacturer?: string;
  model?: string;
  defaultName?: string;
  defaultPort?: number;
  defaultHttpPath?: string;
  defaultTcpCommand?: string;
  defaultProtocol?: TSimpleLocalProtocol;
  status: TIntegrationStatus;
  platforms: TEntityPlatform[];
  serviceDomains?: string[];
  controlServices?: string[];
  discoverySources?: TDiscoverySource[];
  discoveryKeywords?: string[];
  metadata: Record<string, unknown>;
}

export interface ISimpleLocalSnapshotOptions<TConfig extends ISimpleLocalConfig = ISimpleLocalConfig> {
  profile: ISimpleLocalIntegrationProfile;
  config: TConfig;
  rawData?: unknown;
  online?: boolean;
  source?: TSimpleLocalSnapshotSource;
  error?: string;
}

export class SimpleLocalIntegration<TConfig extends ISimpleLocalConfig = ISimpleLocalConfig> extends BaseIntegration<TConfig> {
  public readonly domain: string;
  public readonly displayName: string;
  public readonly status: TIntegrationStatus;
  public readonly discoveryDescriptor: DiscoveryDescriptor;
  public readonly configFlow: IConfigFlow<TConfig>;
  public readonly metadata: Record<string, unknown>;

  constructor(protected readonly profile: ISimpleLocalIntegrationProfile) {
    super();
    this.domain = profile.domain;
    this.displayName = profile.displayName;
    this.status = profile.status;
    this.discoveryDescriptor = createSimpleLocalDiscoveryDescriptor(profile);
    this.configFlow = new SimpleLocalConfigFlow<TConfig>(profile);
    this.metadata = profile.metadata;
  }

  public async setup(configArg: TConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
    void contextArg;
    return new SimpleLocalRuntime(this.profile, new SimpleLocalClient<TConfig>(this.profile, configArg));
  }

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

export class SimpleLocalClient<TConfig extends ISimpleLocalConfig = ISimpleLocalConfig> {
  private currentSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly profile: ISimpleLocalIntegrationProfile, private readonly config: TConfig) {}

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

    this.currentSnapshot = await this.readSnapshot(forceRefreshArg);
    return SimpleLocalMapper.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.config.client?.execute) {
      return normalizeServiceResult(await this.config.client.execute(requestArg));
    }
    if (this.config.commandExecutor) {
      return normalizeServiceResult(await this.config.commandExecutor.execute(requestArg));
    }
    return {
      success: false,
      error: `${this.profile.displayName} service ${requestArg.service} requires an injected client.execute() or commandExecutor; no live control is reported without a real executor.`,
    };
  }

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

  private async readSnapshot(forceRefreshArg: boolean): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.config.snapshot) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, source: 'snapshot', online: this.config.snapshot.online });
    }

    if (this.config.client) {
      try {
        return await this.snapshotFromClient();
      } catch (errorArg) {
        return this.offlineSnapshot(errorMessage(errorArg));
      }
    }

    if (this.config.snapshotProvider) {
      try {
        const value = await this.config.snapshotProvider();
        if (value !== undefined) {
          return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData: value, source: 'provider', online: true });
        }
      } catch (errorArg) {
        return this.offlineSnapshot(errorMessage(errorArg));
      }
    }

    if (this.config.host && this.shouldUseHttp()) {
      try {
        const rawData = await this.requestHttpData();
        return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData, source: 'http', online: true });
      } catch (errorArg) {
        return this.offlineSnapshot(errorMessage(errorArg));
      }
    }

    if (this.config.host && this.shouldUseTcp()) {
      try {
        const rawData = await this.requestTcpData();
        return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData, source: 'tcp', online: true });
      } catch (errorArg) {
        return this.offlineSnapshot(errorMessage(errorArg));
      }
    }

    const rawData = this.config.rawData ?? this.config.rawText;
    if (rawData !== undefined || this.config.entities) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData, source: 'manual', online: this.config.online ?? true });
    }

    if (this.config.snapshot) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, source: 'snapshot', online: this.config.snapshot.online });
    }

    return this.offlineSnapshot(`${this.profile.displayName} snapshots require config.host with a path/transport, an injected client, snapshotProvider, snapshot, entities, or rawData.`);
  }

  private async snapshotFromClient(): Promise<ISimpleLocalSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new Error('No native client configured.');
    }
    if (client.getSnapshot) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData: await client.getSnapshot(), source: 'client', online: true });
    }
    if (client.getState) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData: await client.getState(), source: 'client', online: true });
    }
    if (client.getRawData) {
      return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, rawData: await client.getRawData(), source: 'client', online: true });
    }
    throw new Error(`${this.profile.displayName} client must expose getSnapshot(), getState(), or getRawData().`);
  }

  private shouldUseHttp(): boolean {
    return this.config.transport === 'http' || Boolean(this.httpPath() && this.config.transport !== 'tcp');
  }

  private shouldUseTcp(): boolean {
    return this.config.transport === 'tcp' || this.profile.defaultProtocol === 'tcp' || this.profile.defaultProtocol === 'telnet';
  }

  private httpPath(): string | undefined {
    return stringValue(this.config.path) || this.profile.defaultHttpPath;
  }

  private async requestHttpData(): Promise<unknown> {
    const host = this.config.host;
    const path = this.httpPath();
    if (!host || !path) {
      throw new Error(`${this.profile.displayName} HTTP snapshot requires config.host and config.path or a documented default HTTP path.`);
    }

    const useTls = this.config.useTls || this.profile.defaultProtocol === 'https';
    const port = this.config.port || this.profile.defaultPort;
    const pathPart = path.startsWith('/') ? path : `/${path}`;
    const portPart = port && !((useTls && port === 443) || (!useTls && port === 80)) ? `:${port}` : '';
    const url = `${useTls ? 'https' : 'http'}://${host}${portPart}${pathPart}`;
    const timeoutMs = this.config.timeoutMs || 5000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    try {
      const headers: Record<string, string> = { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' };
      if (this.config.token) {
        headers.authorization = `Bearer ${this.config.token}`;
      } else if (this.config.username) {
        headers.authorization = `Basic ${Buffer.from(`${this.config.username}:${this.config.password || ''}`).toString('base64')}`;
      }
      const response = await globalThis.fetch(url, { headers, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`${this.profile.displayName} HTTP snapshot request failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return parseMaybeJson(text);
    } finally {
      clearTimeout(timer);
    }
  }

  private async requestTcpData(): Promise<string> {
    const host = this.config.host;
    if (!host) {
      throw new Error(`${this.profile.displayName} TCP snapshot requires config.host.`);
    }
    const port = this.config.port || this.profile.defaultPort;
    if (!port) {
      throw new Error(`${this.profile.displayName} TCP snapshot requires config.port or a documented default port.`);
    }
    const command = this.config.tcpCommand ?? this.profile.defaultTcpCommand;
    const timeoutMs = this.config.timeoutMs || 5000;

    return new Promise<string>((resolve, reject) => {
      const chunks: Buffer[] = [];
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(Buffer.concat(chunks).toString('utf8'));
      };

      socket.setTimeout(timeoutMs, () => chunks.length ? finish() : finish(new Error(`${this.profile.displayName} TCP snapshot request to ${host}:${port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        if (command) {
          socket.write(command.endsWith('\n') ? command : `${command}\n`);
        }
      });
      socket.on('data', (chunkArg) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private offlineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: this.profile, config: this.config, online: false, source: 'runtime', error: errorArg });
  }
}

export class SimpleLocalMapper {
  public static toSnapshot<TConfig extends ISimpleLocalConfig>(optionsArg: ISimpleLocalSnapshotOptions<TConfig>): ISimpleLocalSnapshot {
    const rawSnapshot = snapshotValue(optionsArg.rawData) || (optionsArg.rawData === undefined ? optionsArg.config.snapshot : undefined);
    if (rawSnapshot) {
      return this.normalizeSnapshot(rawSnapshot, optionsArg);
    }

    const parsedRawData = typeof optionsArg.rawData === 'string' ? parseMaybeJson(optionsArg.rawData) : optionsArg.rawData;
    const online = optionsArg.online ?? optionsArg.config.online ?? (parsedRawData !== undefined || Boolean(optionsArg.config.entities));
    const device = this.deviceInfo(optionsArg.profile, optionsArg.config, parsedRawData);
    const entities = this.entitiesFromData(optionsArg.profile, optionsArg.config, parsedRawData, online, optionsArg.source || 'manual');

    return {
      device,
      entities,
      online,
      updatedAt: new Date().toISOString(),
      source: optionsArg.source || (parsedRawData === undefined ? 'runtime' : 'manual'),
      rawData: parsedRawData,
      error: optionsArg.error,
      capabilities: {
        localRead: true,
        localControl: (optionsArg.profile.controlServices || []).length > 0,
        liveControlRequiresExecutor: true,
      },
    };
  }

  public static toDevices(profileArg: ISimpleLocalIntegrationProfile, snapshotArg: ISimpleLocalSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const deviceId = this.deviceId(profileArg, snapshotArg);
    return [{
      id: deviceId,
      integrationDomain: profileArg.domain,
      name: snapshotArg.device.name || profileArg.defaultName || profileArg.displayName,
      protocol: (snapshotArg.device.protocol || profileArg.defaultProtocol || (snapshotArg.device.host ? 'http' : 'unknown')) as plugins.shxInterfaces.data.TDeviceProtocol,
      manufacturer: snapshotArg.device.manufacturer || profileArg.manufacturer || profileArg.displayName,
      model: snapshotArg.device.model || profileArg.model || profileArg.displayName,
      online: snapshotArg.online,
      features: snapshotArg.entities.map((entityArg) => ({
        id: entityArg.id || this.slug(entityArg.name),
        capability: this.capabilityForPlatform(entityArg.platform || profileArg.platforms[0]),
        name: entityArg.name,
        readable: true,
        writable: entityArg.writable ?? this.isWritablePlatform(entityArg.platform || profileArg.platforms[0]),
        unit: entityArg.unit,
      })),
      state: snapshotArg.entities.map((entityArg) => ({
        featureId: entityArg.id || this.slug(entityArg.name),
        value: this.deviceStateValue(entityArg.state),
        updatedAt: snapshotArg.updatedAt,
      })),
      metadata: cleanAttributes({
        host: snapshotArg.device.host,
        port: snapshotArg.device.port,
        source: snapshotArg.source,
        configurationUrl: snapshotArg.device.configurationUrl,
        error: snapshotArg.error,
        ...snapshotArg.device.attributes,
      }),
    }];
  }

  public static toEntities(profileArg: ISimpleLocalIntegrationProfile, snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
    const deviceId = this.deviceId(profileArg, snapshotArg);
    const base = this.uniqueBase(profileArg, snapshotArg);
    return snapshotArg.entities.map((entityArg) => {
      const entitySlug = this.slug(entityArg.id || entityArg.name);
      const platform = entityArg.platform || this.platformForState(profileArg, entityArg.state);
      return {
        id: `${platform}.${this.slug(`${snapshotArg.device.name || profileArg.displayName}_${entitySlug}`)}`,
        uniqueId: entityArg.uniqueId || `${profileArg.domain}_${base}_${entitySlug}`,
        integrationDomain: profileArg.domain,
        deviceId,
        platform,
        name: entityArg.name,
        state: entityArg.state,
        available: snapshotArg.online && (entityArg.available ?? true),
        attributes: cleanAttributes({
          unit: entityArg.unit,
          deviceClass: entityArg.deviceClass,
          stateClass: entityArg.stateClass,
          source: snapshotArg.source,
          error: snapshotArg.error,
          ...entityArg.attributes,
        }),
      };
    });
  }

  public static deviceId(profileArg: ISimpleLocalIntegrationProfile, snapshotArg: ISimpleLocalSnapshot): string {
    return `${profileArg.domain}.device.${this.uniqueBase(profileArg, snapshotArg)}`;
  }

  public static uniqueBase(profileArg: ISimpleLocalIntegrationProfile, snapshotArg: ISimpleLocalSnapshot): string {
    return this.slug(snapshotArg.device.id || snapshotArg.device.serialNumber || (snapshotArg.device.host ? `${snapshotArg.device.host}_${snapshotArg.device.port || ''}` : undefined) || snapshotArg.device.name || profileArg.domain);
  }

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

  public static clone<TValue>(valueArg: TValue): TValue {
    return JSON.parse(JSON.stringify(valueArg)) as TValue;
  }

  private static normalizeSnapshot<TConfig extends ISimpleLocalConfig>(snapshotArg: ISimpleLocalSnapshot, optionsArg: ISimpleLocalSnapshotOptions<TConfig>): ISimpleLocalSnapshot {
    const device = {
      ...this.deviceInfo(optionsArg.profile, optionsArg.config),
      ...snapshotArg.device,
      attributes: cleanAttributes({
        ...optionsArg.config.metadata,
        ...snapshotArg.device.attributes,
      }),
    };
    const online = optionsArg.online ?? snapshotArg.online;
    return {
      ...snapshotArg,
      device,
      entities: snapshotArg.entities.map((entityArg) => this.normalizeEntity(optionsArg.profile, entityArg, online)),
      online,
      updatedAt: optionsArg.error ? new Date().toISOString() : snapshotArg.updatedAt || new Date().toISOString(),
      source: optionsArg.source || snapshotArg.source || 'snapshot',
      error: optionsArg.error ?? snapshotArg.error,
    };
  }

  private static deviceInfo(profileArg: ISimpleLocalIntegrationProfile, configArg: ISimpleLocalConfig, rawDataArg?: unknown): ISimpleLocalDeviceInfo {
    const rawObject = rawDataArg && typeof rawDataArg === 'object' && !Array.isArray(rawDataArg) ? rawDataArg as Record<string, unknown> : undefined;
    const rawDevice = rawObject?.device && typeof rawObject.device === 'object' ? rawObject.device as Record<string, unknown> : undefined;
    const host = configArg.host || stringValue(rawDevice?.host);
    const port = configArg.port || numberValue(rawDevice?.port) || (host ? profileArg.defaultPort : undefined);
    const protocol = configArg.useTls ? 'https' : profileArg.defaultProtocol || (host ? 'http' : 'unknown');
    return {
      id: configArg.uniqueId || stringValue(rawDevice?.id) || (host ? `${host}:${port || ''}` : undefined) || configArg.name || profileArg.domain,
      name: configArg.name || stringValue(rawDevice?.name) || profileArg.defaultName || profileArg.displayName,
      manufacturer: stringValue(rawDevice?.manufacturer) || profileArg.manufacturer || profileArg.displayName,
      model: stringValue(rawDevice?.model) || profileArg.model || profileArg.displayName,
      serialNumber: stringValue(rawDevice?.serialNumber),
      host,
      port,
      protocol,
      configurationUrl: host ? `${protocol === 'https' ? 'https' : 'http'}://${host}${port ? `:${port}` : ''}` : undefined,
      attributes: cleanAttributes({ ...configArg.metadata }),
    };
  }

  private static entitiesFromData(profileArg: ISimpleLocalIntegrationProfile, configArg: ISimpleLocalConfig, rawDataArg: unknown, onlineArg: boolean, sourceArg: TSimpleLocalSnapshotSource): ISimpleLocalEntitySnapshot[] {
    const configured = configArg.entities;
    if (configured?.length) {
      return configured.map((entityArg) => this.normalizeEntity(profileArg, entityArg, onlineArg));
    }

    if (rawDataArg && typeof rawDataArg === 'object' && !Array.isArray(rawDataArg)) {
      const rawObject = rawDataArg as Record<string, unknown>;
      if (Array.isArray(rawObject.entities)) {
        return rawObject.entities
          .filter((entityArg): entityArg is ISimpleLocalEntitySnapshot => Boolean(entityArg && typeof entityArg === 'object' && 'name' in entityArg))
          .map((entityArg) => this.normalizeEntity(profileArg, entityArg, onlineArg));
      }
      const primitiveEntities = Object.entries(rawObject)
        .filter(([keyArg, valueArg]) => !ignoredRawKeys.has(keyArg) && isPrimitive(valueArg))
        .slice(0, 24)
        .map(([keyArg, valueArg]) => this.normalizeEntity(profileArg, {
          id: keyArg,
          name: titleCase(keyArg),
          platform: typeof valueArg === 'boolean' ? 'binary_sensor' : 'sensor',
          state: valueArg,
          available: true,
          attributes: { source: sourceArg },
        }, onlineArg));
      if (primitiveEntities.length) {
        return primitiveEntities;
      }
    }

    if (Array.isArray(rawDataArg)) {
      return [{
        id: 'items',
        name: 'Items',
        platform: 'sensor',
        state: rawDataArg.length,
        available: onlineArg,
        attributes: { source: sourceArg },
      }];
    }

    if (rawDataArg !== undefined) {
      return [{
        id: 'status',
        name: 'Status',
        platform: typeof rawDataArg === 'boolean' ? 'binary_sensor' : 'sensor',
        state: rawDataArg,
        available: onlineArg,
        attributes: { source: sourceArg },
      }];
    }

    return [{
      id: 'online',
      name: 'Online',
      platform: 'binary_sensor',
      state: onlineArg,
      available: true,
      attributes: { source: sourceArg },
    }];
  }

  private static normalizeEntity(profileArg: ISimpleLocalIntegrationProfile, entityArg: ISimpleLocalEntitySnapshot, onlineArg: boolean): ISimpleLocalEntitySnapshot {
    const platform = entityArg.platform || this.platformForState(profileArg, entityArg.state);
    return {
      ...entityArg,
      id: entityArg.id || this.slug(entityArg.name),
      platform,
      available: onlineArg && (entityArg.available ?? true),
      writable: entityArg.writable ?? this.isWritablePlatform(platform),
      attributes: cleanAttributes(entityArg.attributes),
    };
  }

  private static platformForState(profileArg: ISimpleLocalIntegrationProfile, stateArg: unknown): TEntityPlatform {
    if (profileArg.platforms.length === 1 && profileArg.platforms[0] !== 'sensor') {
      return profileArg.platforms[0];
    }
    return typeof stateArg === 'boolean' ? 'binary_sensor' : 'sensor';
  }

  private static isWritablePlatform(platformArg?: TEntityPlatform): boolean {
    return Boolean(platformArg && ['button', 'climate', 'cover', 'fan', 'light', 'media_player', 'number', 'select', 'switch', 'text'].includes(platformArg));
  }

  private static capabilityForPlatform(platformArg?: TEntityPlatform): plugins.shxInterfaces.data.TDeviceCapability {
    if (platformArg === 'media_player') {
      return 'media';
    }
    if (platformArg === 'binary_sensor' || platformArg === 'number' || platformArg === 'select' || platformArg === 'text' || platformArg === 'update') {
      return 'sensor';
    }
    if (platformArg === 'button') {
      return 'switch';
    }
    return (platformArg || 'sensor') as plugins.shxInterfaces.data.TDeviceCapability;
  }

  private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    if (valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg)) {
      return valueArg as plugins.shxInterfaces.data.TDeviceStateValue;
    }
    if (valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)) {
      return valueArg as Record<string, unknown>;
    }
    return JSON.stringify(valueArg ?? null);
  }
}

export class SimpleLocalConfigFlow<TConfig extends ISimpleLocalConfig = ISimpleLocalConfig> implements IConfigFlow<TConfig> {
  constructor(private readonly profile: ISimpleLocalIntegrationProfile) {}

  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<TConfig>> {
    void contextArg;
    return {
      kind: 'form',
      title: `Connect ${this.profile.displayName}`,
      description: `Configure a local ${this.profile.displayName} endpoint, injected client, snapshot, or raw data source.`,
      fields: this.fields(),
      submit: async (valuesArg) => this.submit(candidateArg, valuesArg),
    };
  }

  private async submit(candidateArg: IDiscoveryCandidate, valuesArg: Record<string, unknown>): Promise<IConfigFlowStep<TConfig>> {
    const metadata = candidateArg.metadata || {};
    const endpoint = parseEndpoint(stringValue(valuesArg.host) || stringValue(metadata.url) || stringValue(metadata.location));
    const snapshot = snapshotValue(valuesArg.snapshot) || snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(valuesArg.rawData) ?? rawDataValue(metadata.rawData) ?? rawDataValue(metadata.rawText) ?? rawDataValue(metadata.banner);
    const entities = entitiesValue(valuesArg.entities) || entitiesValue(metadata.entities);
    const host = endpoint?.host || stringValue(valuesArg.host) || candidateArg.host || snapshot?.device.host;
    const port = numberValue(valuesArg.port) || endpoint?.port || candidateArg.port || snapshot?.device.port || (host ? this.profile.defaultPort : undefined);
    const path = stringValue(valuesArg.path) || stringValue(metadata.path) || this.profile.defaultHttpPath;
    const hasOfflineData = Boolean(snapshot || rawData !== undefined || entities?.length || metadata.client || metadata.snapshotProvider);

    if (!host && !hasOfflineData) {
      return { kind: 'error', title: `${this.profile.displayName} setup failed`, error: `${this.profile.displayName} host, injected client, snapshotProvider, snapshot, entities, or rawData is required.` };
    }
    if (port !== undefined && !validPort(port)) {
      return { kind: 'error', title: `${this.profile.displayName} setup failed`, error: `${this.profile.displayName} port must be between 1 and 65535.` };
    }

    return {
      kind: 'done',
      title: `${this.profile.displayName} configured`,
      config: {
        host,
        port,
        path,
        name: stringValue(valuesArg.name) || candidateArg.name || snapshot?.device.name || stringValue(metadata.name) || this.profile.defaultName || this.profile.displayName,
        uniqueId: candidateArg.id || snapshot?.device.id || (host ? `${host}:${port || ''}` : undefined),
        username: stringValue(valuesArg.username) || stringValue(metadata.username),
        password: stringValue(valuesArg.password) || stringValue(metadata.password),
        token: stringValue(valuesArg.token) || stringValue(metadata.token),
        apiKey: stringValue(valuesArg.apiKey) || stringValue(metadata.apiKey),
        timeoutMs: numberValue(valuesArg.timeoutMs) || numberValue(metadata.timeoutMs) || 5000,
        transport: host && path ? 'http' : host && (this.profile.defaultProtocol === 'tcp' || this.profile.defaultProtocol === 'telnet') ? 'tcp' : 'snapshot',
        snapshot,
        rawData,
        entities,
        client: metadata.client as ISimpleLocalClientLike | undefined,
        snapshotProvider: metadata.snapshotProvider as ISimpleLocalConfig['snapshotProvider'],
        commandExecutor: metadata.commandExecutor as ISimpleLocalCommandExecutor | undefined,
        metadata: cleanAttributes({ ...metadata, client: undefined, snapshotProvider: undefined, commandExecutor: undefined }),
      } as TConfig,
    };
  }

  private fields(): IConfigFlowField[] {
    return [
      { name: 'host', label: 'Host', type: 'text' },
      { name: 'port', label: 'Port', type: 'number' },
      { name: 'path', label: 'HTTP path', type: 'text' },
      { name: 'username', label: 'Username', type: 'text' },
      { name: 'password', label: 'Password', type: 'password' },
      { name: 'token', label: 'Token', type: 'password' },
      { name: 'name', label: 'Name', type: 'text' },
      { name: 'rawData', label: 'Raw snapshot data', type: 'text' },
    ];
  }
}

export class SimpleLocalRuntime<TConfig extends ISimpleLocalConfig = ISimpleLocalConfig> implements IIntegrationRuntime {
  public readonly domain: string;

  constructor(private readonly profile: ISimpleLocalIntegrationProfile, private readonly client: SimpleLocalClient<TConfig>) {
    this.domain = profile.domain;
  }

  public async devices(): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
    return SimpleLocalMapper.toDevices(this.profile, await this.client.getSnapshot());
  }

  public async entities(): Promise<IIntegrationEntity[]> {
    return SimpleLocalMapper.toEntities(this.profile, await this.client.getSnapshot());
  }

  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.acceptsDomain(requestArg.domain)) {
      return { success: false, error: `Unsupported ${this.profile.displayName} service domain: ${requestArg.domain}` };
    }
    if (requestArg.service === 'snapshot' || requestArg.service === 'status') {
      return { success: true, data: await this.client.getSnapshot() };
    }
    if (requestArg.service === 'refresh' || requestArg.service === 'reload') {
      return this.client.refresh();
    }
    if (this.isControlService(requestArg.service)) {
      return this.client.execute(requestArg);
    }
    return { success: false, error: `Unsupported ${this.profile.displayName} service: ${requestArg.service}` };
  }

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

  private acceptsDomain(domainArg: string): boolean {
    return domainArg === this.profile.domain || Boolean(this.profile.serviceDomains?.includes(domainArg));
  }

  private isControlService(serviceArg: string): boolean {
    return Boolean(this.profile.controlServices?.includes(serviceArg)) || commonControlServices.has(serviceArg);
  }
}

export class SimpleLocalMatcher implements IDiscoveryMatcher<ISimpleLocalDiscoveryRecord> {
  public readonly id: string;
  public readonly description: string;

  constructor(private readonly profile: ISimpleLocalIntegrationProfile, public readonly source: TDiscoverySource) {
    this.id = `${profile.domain}-${source}-match`;
    this.description = `Recognize ${profile.displayName} ${source} discovery records, local endpoints, snapshots, and injected native clients.`;
  }

  public async matches(inputArg: ISimpleLocalDiscoveryRecord, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = inputArg.metadata || {};
    const endpoint = parseEndpoint(inputArg.url || inputArg.location || inputArg.ssdpLocation || inputArg.ssdp_location || inputArg.host);
    const snapshot = snapshotValue(inputArg.snapshot || metadata.snapshot);
    const rawData = rawDataValue(inputArg.rawData || inputArg.rawText || metadata.rawData || metadata.rawText || metadata.banner);
    const entities = entitiesValue(inputArg.entities || metadata.entities);
    const host = endpoint?.host || (inputArg.host && !inputArg.host.includes('://') ? inputArg.host : undefined) || snapshot?.device.host;
    const port = inputArg.port || endpoint?.port || snapshot?.device.port || (host ? this.profile.defaultPort : undefined);
    const haystack = discoveryHaystack(inputArg, metadata);
    const explicit = inputArg.integrationDomain === this.profile.domain || metadata.integrationDomain === this.profile.domain || metadata.domain === this.profile.domain || metadata[this.profile.domain] === true;
    const keyword = this.keywords().some((keywordArg) => haystack.includes(keywordArg.toLowerCase()));
    const portMatch = Boolean(this.profile.defaultPort && port === this.profile.defaultPort);
    const hasData = Boolean(snapshot || rawData !== undefined || entities?.length || inputArg.client || metadata.client || metadata.snapshotProvider);
    const matched = explicit || hasData || keyword || (Boolean(host) && portMatch);

    if (!matched) {
      return { matched: false, confidence: 'low', reason: `${this.profile.displayName} discovery record does not contain matching local hints.` };
    }

    const id = inputArg.id || snapshot?.device.id || (host ? `${host}:${port || ''}` : undefined) || stringValue(metadata.id);
    return {
      matched: true,
      confidence: explicit || hasData ? 'high' : host && portMatch ? 'high' : 'medium',
      reason: `${this.profile.displayName} discovery record can start local setup.`,
      normalizedDeviceId: id,
      candidate: {
        source: inputArg.source || this.source,
        integrationDomain: this.profile.domain,
        id,
        host,
        port,
        name: inputArg.name || snapshot?.device.name || this.profile.defaultName || this.profile.displayName,
        manufacturer: inputArg.manufacturer || snapshot?.device.manufacturer || this.profile.manufacturer || this.profile.displayName,
        model: inputArg.model || snapshot?.device.model || this.profile.model || this.profile.displayName,
        serialNumber: inputArg.serialNumber || snapshot?.device.serialNumber,
        macAddress: inputArg.macAddress,
        metadata: cleanAttributes({
          ...metadata,
          [this.profile.domain]: true,
          discoveryProtocol: inputArg.source || this.source,
          path: inputArg.path || metadata.path || this.profile.defaultHttpPath,
          snapshot,
          rawData,
          entities,
          client: inputArg.client || metadata.client,
          snapshotProvider: inputArg.snapshotProvider || metadata.snapshotProvider,
          commandExecutor: inputArg.commandExecutor || metadata.commandExecutor,
        }),
      },
    };
  }

  private keywords(): string[] {
    return [this.profile.domain, this.profile.displayName, this.profile.manufacturer, this.profile.model, ...(this.profile.discoveryKeywords || [])]
      .filter((valueArg): valueArg is string => Boolean(valueArg));
  }
}

export class SimpleLocalCandidateValidator implements IDiscoveryValidator {
  public readonly id: string;
  public readonly description: string;

  constructor(private readonly profile: ISimpleLocalIntegrationProfile) {
    this.id = `${profile.domain}-candidate-validator`;
    this.description = `Validate ${profile.displayName} candidates have a local endpoint or offline native data source.`;
  }

  public async validate(candidateArg: IDiscoveryCandidate, contextArg?: IDiscoveryContext): Promise<IDiscoveryMatch> {
    void contextArg;
    const metadata = candidateArg.metadata || {};
    const snapshot = snapshotValue(metadata.snapshot);
    const rawData = rawDataValue(metadata.rawData || metadata.rawText || metadata.banner);
    const entities = entitiesValue(metadata.entities);
    const haystack = discoveryHaystack(candidateArg, metadata);
    const explicit = candidateArg.integrationDomain === this.profile.domain || metadata[this.profile.domain] === true;
    const keyword = [this.profile.domain, this.profile.displayName, this.profile.manufacturer, ...(this.profile.discoveryKeywords || [])]
      .filter((valueArg): valueArg is string => Boolean(valueArg))
      .some((keywordArg) => haystack.includes(keywordArg.toLowerCase()));
    const hasSource = Boolean(candidateArg.host || snapshot || rawData !== undefined || entities?.length || metadata.client || metadata.snapshotProvider);

    if ((!explicit && !keyword) || !hasSource) {
      return {
        matched: false,
        confidence: explicit || keyword ? 'medium' : 'low',
        reason: explicit || keyword ? `${this.profile.displayName} candidate lacks a host, injected client, snapshotProvider, snapshot, entities, or rawData.` : `Candidate is not ${this.profile.displayName}.`,
      };
    }

    const port = candidateArg.port || snapshot?.device.port || (candidateArg.host ? this.profile.defaultPort : undefined);
    const normalizedDeviceId = candidateArg.id || snapshot?.device.id || (candidateArg.host ? `${candidateArg.host}:${port || ''}` : undefined);
    return {
      matched: true,
      confidence: normalizedDeviceId ? 'certain' : 'high',
      reason: `${this.profile.displayName} candidate has a usable native local source.`,
      normalizedDeviceId,
      candidate: {
        ...candidateArg,
        integrationDomain: this.profile.domain,
        port,
        manufacturer: candidateArg.manufacturer || this.profile.manufacturer || this.profile.displayName,
        metadata: cleanAttributes({ ...metadata, [this.profile.domain]: true }),
      },
    };
  }
}

export const createSimpleLocalDiscoveryDescriptor = (profileArg: ISimpleLocalIntegrationProfile): DiscoveryDescriptor => {
  const sources: TDiscoverySource[] = profileArg.discoverySources?.length ? profileArg.discoverySources : ['manual', 'mdns', 'ssdp', 'http', 'custom'];
  const descriptor = new DiscoveryDescriptor({ integrationDomain: profileArg.domain, displayName: profileArg.displayName });
  for (const source of sources) {
    descriptor.addMatcher(new SimpleLocalMatcher(profileArg, source));
  }
  descriptor.addValidator(new SimpleLocalCandidateValidator(profileArg));
  return descriptor;
};

const ignoredRawKeys = new Set(['api_key', 'apiKey', 'auth', 'client', 'commandExecutor', 'device', 'entities', 'host', 'metadata', 'password', 'snapshot', 'snapshotProvider', 'token', 'username']);
const commonControlServices = new Set(['turn_on', 'turn_off', 'toggle', 'set_value', 'set_level', 'set_temperature', 'set_hvac_mode', 'open_cover', 'close_cover', 'stop_cover', 'select_source', 'volume_up', 'volume_down', 'volume_mute', 'media_play', 'media_pause', 'media_stop', 'media_next_track', 'media_previous_track', 'press']);

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const snapshotValue = (valueArg: unknown): ISimpleLocalSnapshot | undefined => {
  return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg ? valueArg as ISimpleLocalSnapshot : undefined;
};

const rawDataValue = (valueArg: unknown): TSimpleLocalRawData | undefined => {
  if (valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg)) {
    return valueArg as TSimpleLocalRawData;
  }
  if (Array.isArray(valueArg) || (valueArg && typeof valueArg === 'object')) {
    return valueArg as TSimpleLocalRawData;
  }
  return undefined;
};

const entitiesValue = (valueArg: unknown): ISimpleLocalEntitySnapshot[] | undefined => {
  if (!Array.isArray(valueArg)) {
    return undefined;
  }
  const entities = valueArg.filter((entityArg): entityArg is ISimpleLocalEntitySnapshot => Boolean(entityArg && typeof entityArg === 'object' && 'name' in entityArg && 'state' in entityArg));
  return entities.length ? entities : undefined;
};

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined };
  } catch {
    return undefined;
  }
};

const parseMaybeJson = (textArg: string): unknown => {
  const trimmed = textArg.trim();
  if (!trimmed) {
    return '';
  }
  if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
    return trimmed;
  }
  try {
    return JSON.parse(trimmed) as unknown;
  } catch {
    return textArg;
  }
};

const discoveryHaystack = (recordArg: IDiscoveryCandidate | ISimpleLocalDiscoveryRecord, metadataArg: Record<string, unknown>): string => {
  const values = [
    recordArg.integrationDomain,
    recordArg.id,
    recordArg.name,
    recordArg.manufacturer,
    recordArg.model,
    recordArg.host,
    stringValue((recordArg as ISimpleLocalDiscoveryRecord).type),
    stringValue((recordArg as ISimpleLocalDiscoveryRecord).serviceType),
    stringValue((recordArg as ISimpleLocalDiscoveryRecord).st),
    stringValue((recordArg as ISimpleLocalDiscoveryRecord).usn),
    ...Object.values(metadataArg).filter((valueArg): valueArg is string => typeof valueArg === 'string'),
  ];
  return values.filter(Boolean).join(' ').toLowerCase();
};

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

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

const validPort = (valueArg: number): boolean => {
  return Number.isInteger(valueArg) && valueArg > 0 && valueArg <= 65535;
};

const isPrimitive = (valueArg: unknown): valueArg is string | number | boolean | null => {
  return valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg);
};

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

const titleCase = (valueArg: string): string => {
  return valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());
};

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