import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { OtbrMapper } from './otbr.mapper.js';
import type { IOtbrConfig, IOtbrDatasetRaw, IOtbrRawApiData } from './otbr.types.js';
import { otbrDefaultTimeoutMs, otbrProfile } from './otbr.types.js';

interface IOtbrEndpoint {
  baseUrl: string;
  host?: string;
  port?: number;
  protocol: 'http' | 'https';
}

export class OtbrClient extends SimpleLocalClient<IOtbrConfig> {
  private currentOtbrSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IOtbrConfig) {
    super(otbrProfile, configArg);
  }

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

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentOtbrSnapshot = OtbrMapper.toSnapshot({ config: this.configArg, source: 'snapshot', online: this.configArg.snapshot.online });
      return clone(this.currentOtbrSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        const rawData = await this.fetchRawData();
        this.currentOtbrSnapshot = OtbrMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
      } catch (errorArg) {
        this.currentOtbrSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOtbrSnapshot);
    }

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

    if (this.hasManualData()) {
      this.currentOtbrSnapshot = OtbrMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData ?? this.configArg.rawText, online: this.configArg.online ?? true, source: 'manual' });
      return clone(this.currentOtbrSnapshot);
    }

    this.currentOtbrSnapshot = this.createOfflineSnapshot('Open Thread Border Router snapshots require config.url/config.host for the local REST API, injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentOtbrSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOtbrSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData() || Boolean(this.configArg.snapshot));
    return { success, error: success ? undefined : snapshot.error || 'Open Thread Border Router refresh requires a REST API URL, injected client, snapshotProvider, or manual snapshot data.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.commandExecutor) {
      return normalizeServiceResult(await this.configArg.commandExecutor.execute(requestArg));
    }
    if (this.configArg.client?.execute) {
      return normalizeServiceResult(await this.configArg.client.execute(requestArg));
    }
    if (!this.hasEndpoint()) {
      return { success: false, error: `Open Thread Border Router service ${requestArg.domain}.${requestArg.service} requires config.url/config.host for live REST writes or an injected client.execute/commandExecutor.` };
    }

    try {
      if (requestArg.service === 'set_enabled') {
        const enabled = Boolean(requestArg.data?.enabled);
        await this.requestText('/node/state', { method: 'PUT', body: JSON.stringify(enabled ? 'enable' : 'disable'), headers: { 'content-type': 'application/json' } });
        this.currentOtbrSnapshot = undefined;
        return { success: true, data: { enabled } };
      }
      if (requestArg.service === 'set_active_dataset_tlvs') {
        const tlvs = stringValue(requestArg.data?.tlvs ?? requestArg.data?.dataset ?? requestArg.data?.activeDatasetTlvs);
        if (!tlvs) {
          return { success: false, error: 'Open Thread Border Router set_active_dataset_tlvs requires data.tlvs.' };
        }
        await this.requestText('/node/dataset/active', { method: 'PUT', body: tlvs, headers: { 'content-type': 'text/plain' } });
        this.currentOtbrSnapshot = undefined;
        return { success: true, data: { tlvs } };
      }
      return { success: false, error: `Unsupported Open Thread Border Router service: ${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async fetchRawData(): Promise<IOtbrRawApiData> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Open Thread Border Router REST polling requires config.url or config.host.');
    }

    const keyFormat = await this.detectKeyFormat();
    const [borderAgentId, activeDatasetJson, activeDatasetTlvs, extendedAddress, coprocessorVersion] = await Promise.all([
      this.requestJsonString('/node/ba-id'),
      this.requestJson<Record<string, unknown> | undefined>('/node/dataset/active'),
      this.requestText('/node/dataset/active', { headers: { accept: 'text/plain' }, allowNoContent: true }),
      this.requestJsonString('/node/ext-address'),
      this.requestJsonString('/node/coprocessor/version').catch(() => undefined),
    ]);
    const activeDataset = this.dataset(activeDatasetJson, activeDatasetTlvs);
    const entities = this.entities(borderAgentId, extendedAddress, activeDataset, coprocessorVersion);

    return {
      device: {
        id: extendedAddress || borderAgentId || endpoint.baseUrl,
        name: this.configArg.name || 'Open Thread Border Router',
        manufacturer: 'OpenThread',
        model: 'OpenThread Border Router REST API',
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
      },
      borderAgentId,
      extendedAddress,
      activeDataset,
      coprocessorVersion,
      keyFormat,
      entities,
      fetchedAt: new Date().toISOString(),
    };
  }

  private async detectKeyFormat(): Promise<'camel' | 'pascal'> {
    const response = await this.fetch('/api/actions', { headers: { accept: 'application/json' }, allowStatuses: [404] });
    await response.arrayBuffer().catch(() => undefined);
    if (response.status === 404) {
      return 'pascal';
    }
    return 'camel';
  }

  private dataset(jsonArg: Record<string, unknown> | undefined, tlvsArg: string | undefined): IOtbrDatasetRaw | undefined {
    if (!jsonArg && !tlvsArg) {
      return undefined;
    }
    const parsedTlvs = tlvsArg ? parseDatasetTlvs(tlvsArg) : undefined;
    return {
      json: jsonArg,
      tlvs: tlvsArg,
      channel: numberValue(jsonArg?.channel ?? jsonArg?.Channel) ?? parsedTlvs?.channel,
      networkName: stringValue(jsonArg?.networkName ?? jsonArg?.NetworkName) ?? parsedTlvs?.networkName,
      panId: stringValue(jsonArg?.panId ?? jsonArg?.PanId) ?? numberValue(jsonArg?.panId ?? jsonArg?.PanId),
      extendedPanId: stringValue(jsonArg?.extPanId ?? jsonArg?.ExtPanId),
    };
  }

  private entities(borderAgentIdArg: string | undefined, extendedAddressArg: string | undefined, datasetArg: IOtbrDatasetRaw | undefined, coprocessorVersionArg: string | undefined): ISimpleLocalSnapshot['entities'] {
    const entities: ISimpleLocalSnapshot['entities'] = [
      { id: 'active_dataset', name: 'Active Dataset', platform: 'binary_sensor', state: Boolean(datasetArg?.tlvs || datasetArg?.json), available: true, attributes: { tlvs: datasetArg?.tlvs } },
    ];
    if (borderAgentIdArg) {
      entities.push({ id: 'border_agent_id', name: 'Border Agent ID', platform: 'sensor', state: borderAgentIdArg, available: true });
    }
    if (extendedAddressArg) {
      entities.push({ id: 'extended_address', name: 'Extended Address', platform: 'sensor', state: extendedAddressArg, available: true });
    }
    if (datasetArg?.channel !== undefined) {
      entities.push({ id: 'channel', name: 'Channel', platform: 'sensor', state: datasetArg.channel, available: true });
    }
    if (datasetArg?.networkName) {
      entities.push({ id: 'network_name', name: 'Network Name', platform: 'sensor', state: datasetArg.networkName, available: true });
    }
    if (coprocessorVersionArg) {
      entities.push({ id: 'coprocessor_version', name: 'Coprocessor Version', platform: 'sensor', state: coprocessorVersionArg, available: true });
    }
    return entities;
  }

  private async requestJson<TValue>(pathArg: string): Promise<TValue> {
    const text = await this.requestText(pathArg, { headers: { accept: 'application/json' }, allowNoContent: true });
    if (text === undefined || text === '') {
      return undefined as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`Open Thread Border Router endpoint ${pathArg} did not return JSON: ${errorMessage(errorArg)}`);
    }
  }

  private async requestJsonString(pathArg: string): Promise<string> {
    const value = await this.requestJson<unknown>(pathArg);
    if (typeof value !== 'string') {
      throw new Error(`Open Thread Border Router endpoint ${pathArg} did not return a string.`);
    }
    return value;
  }

  private async requestText(pathArg: string, optionsArg: { method?: string; body?: BodyInit; headers?: Record<string, string>; allowNoContent?: boolean } = {}): Promise<string | undefined> {
    const response = await this.fetch(pathArg, optionsArg);
    if (response.status === 204 && optionsArg.allowNoContent) {
      await response.arrayBuffer().catch(() => undefined);
      return undefined;
    }
    return response.text();
  }

  private async fetch(pathArg: string, optionsArg: { method?: string; body?: BodyInit; headers?: Record<string, string>; allowStatuses?: number[] } = {}): Promise<Response> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Open Thread Border Router REST request requires config.url or config.host.');
    }
    const url = new URL(pathArg.startsWith('/') ? pathArg : `/${pathArg}`, `${endpoint.baseUrl}/`).toString();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.configArg.timeoutMs || otbrDefaultTimeoutMs);
    try {
      const response = await globalThis.fetch(url, { method: optionsArg.method || 'GET', headers: optionsArg.headers, body: optionsArg.body, signal: controller.signal });
      if (!response.ok && !optionsArg.allowStatuses?.includes(response.status)) {
        const text = await response.text().catch(() => '');
        throw new Error(`Open Thread Border Router endpoint ${url} failed with HTTP ${response.status}${text ? `: ${text.slice(0, 160)}` : ''}`);
      }
      return response;
    } finally {
      clearTimeout(timer);
    }
  }

  private endpoint(): IOtbrEndpoint | undefined {
    const configured = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!configured) {
      return undefined;
    }
    const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(configured) ? configured : `${this.configArg.useTls ? 'https' : 'http'}://${configured}`;
    try {
      const url = new URL(withProtocol);
      if (this.configArg.port) {
        url.port = String(this.configArg.port);
      }
      url.pathname = url.pathname.replace(/\/$/, '');
      url.search = '';
      url.hash = '';
      return { baseUrl: url.toString().replace(/\/$/, ''), host: url.hostname, port: url.port ? Number(url.port) : undefined, protocol: url.protocol === 'https:' ? 'https' : 'http' };
    } catch {
      const protocol = this.configArg.useTls ? 'https' : 'http';
      const port = this.configArg.port;
      return { baseUrl: `${protocol}://${configured}${port ? `:${port}` : ''}`, host: configured, port, protocol };
    }
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || Boolean(this.configArg.entities?.length || this.configArg.snapshotProvider);
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return OtbrMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorArg });
  }
}

const parseDatasetTlvs = (tlvsArg: string): { channel?: number; networkName?: string } => {
  const bytes = Buffer.from(tlvsArg.trim(), 'hex');
  const result: { channel?: number; networkName?: string } = {};
  let offset = 0;
  while (offset + 2 <= bytes.length) {
    const tag = bytes[offset++];
    const length = bytes[offset++];
    if (offset + length > bytes.length) {
      break;
    }
    const value = bytes.subarray(offset, offset + length);
    offset += length;
    if (tag === 0) {
      result.channel = Number(BigInt(`0x${value.toString('hex') || '0'}`));
    } else if (tag === 3) {
      result.networkName = value.toString('utf8');
    }
  }
  return result;
};

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

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

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 clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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