import * as plugins from '../../plugins.js';
import type { IShellyConfig, IShellyDeviceConfig, IShellyDeviceInfo, IShellySnapshot, IShellyStatus } from './shelly.types.js';

interface IShellyDigestChallenge {
  realm: string;
  nonce: string;
  qop?: string;
  opaque?: string;
}

export class ShellyClient {
  private requestId = 1;
  private nonceCount = 0;
  private digestChallenge?: IShellyDigestChallenge;

  constructor(private readonly config: IShellyConfig) {}

  public async getSnapshot(): Promise<IShellySnapshot> {
    const deviceInfo = await this.getDeviceInfo();
    const status = await this.getStatus();
    const deviceConfig = await this.getDeviceConfig();
    return { deviceInfo, status, deviceConfig };
  }

  public async getDeviceInfo(): Promise<IShellyDeviceInfo> {
    if (this.config.deviceInfo) {
      return this.config.deviceInfo;
    }
    return this.requestJson<IShellyDeviceInfo>('/shelly');
  }

  public async getStatus(): Promise<IShellyStatus> {
    if (this.config.status) {
      return this.config.status;
    }
    return this.callRpc<IShellyStatus>('Shelly.GetStatus');
  }

  public async getDeviceConfig(): Promise<IShellyDeviceConfig | undefined> {
    if (this.config.deviceConfig) {
      return this.config.deviceConfig;
    }
    if (!this.config.host) {
      return undefined;
    }
    return this.callRpc<IShellyDeviceConfig>('Shelly.GetConfig');
  }

  public async setSwitchOutput(switchIdArg: number, onArg: boolean): Promise<void> {
    await this.callRpc('Switch.Set', {
      id: switchIdArg,
      on: onArg,
    });
  }

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

  private async callRpc<TResult>(methodArg: string, paramsArg?: Record<string, unknown>): Promise<TResult> {
    const response = await this.requestJson<unknown>('/rpc', {
      id: this.requestId++,
      method: methodArg,
      params: paramsArg,
    });
    if (this.isRecord(response) && 'error' in response) {
      const error = response.error;
      throw new Error(`Shelly RPC ${methodArg} failed: ${JSON.stringify(error)}`);
    }
    if (this.isRecord(response) && 'result' in response) {
      return response.result as TResult;
    }
    if (this.isRecord(response) && 'params' in response) {
      return response.params as TResult;
    }
    return response as TResult;
  }

  private async requestJson<TResult>(pathArg: string, bodyArg?: Record<string, unknown>): Promise<TResult> {
    if (!this.config.host) {
      throw new Error('Shelly host is required when fixture data is not provided.');
    }

    const method = bodyArg ? 'POST' : 'GET';
    const firstResponse = await this.fetchPath(pathArg, method, bodyArg);
    if (firstResponse.status !== 401) {
      return this.parseJsonResponse<TResult>(firstResponse, pathArg);
    }

    if (!this.config.password) {
      throw new Error('Shelly device requires authentication, but no password was provided.');
    }

    this.digestChallenge = this.parseDigestChallenge(firstResponse.headers.get('www-authenticate'));
    this.nonceCount = 0;

    const secondResponse = await this.fetchPath(pathArg, method, bodyArg);
    return this.parseJsonResponse<TResult>(secondResponse, pathArg);
  }

  private async fetchPath(pathArg: string, methodArg: string, bodyArg?: Record<string, unknown>): Promise<Response> {
    const headers: Record<string, string> = {};
    if (bodyArg) {
      headers['content-type'] = 'application/json';
    }
    if (this.digestChallenge) {
      headers.authorization = this.createDigestAuthorization(methodArg, pathArg);
    }

    return globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
      method: methodArg,
      headers,
      body: bodyArg ? JSON.stringify(bodyArg) : undefined,
    });
  }

  private async parseJsonResponse<TResult>(responseArg: Response, pathArg: string): Promise<TResult> {
    if (!responseArg.ok) {
      const text = await responseArg.text();
      throw new Error(`Shelly request ${pathArg} failed with HTTP ${responseArg.status}: ${text}`);
    }
    return responseArg.json() as Promise<TResult>;
  }

  private parseDigestChallenge(headerArg: string | null): IShellyDigestChallenge {
    if (!headerArg) {
      throw new Error('Shelly authentication challenge did not include a WWW-Authenticate header.');
    }
    const values: Record<string, string> = {};
    const regex = /(\w+)=(?:"([^"]*)"|([^,\s]+))/g;
    let match: RegExpExecArray | null;
    while ((match = regex.exec(headerArg))) {
      values[match[1]] = match[2] ?? match[3] ?? '';
    }
    if (!values.realm || !values.nonce) {
      throw new Error(`Unsupported Shelly authentication challenge: ${headerArg}`);
    }
    return {
      realm: values.realm,
      nonce: values.nonce,
      qop: values.qop,
      opaque: values.opaque,
    };
  }

  private createDigestAuthorization(methodArg: string, pathArg: string): string {
    if (!this.digestChallenge || !this.config.password) {
      throw new Error('Shelly digest authentication cannot be created before a challenge is received.');
    }

    const username = this.config.username || 'admin';
    const cnonce = plugins.crypto.randomBytes(8).toString('hex');
    const nc = (++this.nonceCount).toString(16).padStart(8, '0');
    const ha1 = this.sha256(`${username}:${this.digestChallenge.realm}:${this.config.password}`);
    const ha2 = this.sha256(`${methodArg}:${pathArg}`);
    const response = this.sha256(`${ha1}:${this.digestChallenge.nonce}:${nc}:${cnonce}:auth:${ha2}`);
    const parts = [
      `username="${username}"`,
      `realm="${this.digestChallenge.realm}"`,
      `nonce="${this.digestChallenge.nonce}"`,
      `uri="${pathArg}"`,
      'algorithm=SHA-256',
      `response="${response}"`,
      'qop=auth',
      `nc=${nc}`,
      `cnonce="${cnonce}"`,
    ];
    if (this.digestChallenge.opaque) {
      parts.push(`opaque="${this.digestChallenge.opaque}"`);
    }
    return `Digest ${parts.join(', ')}`;
  }

  private sha256(valueArg: string): string {
    return plugins.crypto.createHash('sha256').update(valueArg).digest('hex');
  }

  private baseUrl(): string {
    const protocol = this.config.protocol || 'http';
    const port = this.config.port && this.config.port !== 80 ? `:${this.config.port}` : '';
    return `${protocol}://${this.config.host}${port}`;
  }

  private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }
}
