import { Inject, Injectable, OnInit, Type } from '@angular/core';
import { DOCUMENT } from '@angular/common';

import * as jspb from 'google-protobuf';
import * as nats from 'nats.ws';
import { Base64 } from 'js-base64';

import { HEADER_CLIENT } from 'app/modules/protobuf/c2se/core/network_pb';
import { Client } from 'app/modules/protobuf/models/core/network_pb';

import { ModuleConfig } from '../config/module-config';
import { _MODULE_CONFIG } from '../config/token';
import { WebSessionStorageService } from 'app/core/web-storage/services/web-session-storage.service';

type DeserializeConstructor<T> = {
  new (): T;
  deserializeBinary(this: DeserializeConstructor<T>, bytes: Uint8Array): T;
};

const NAME_GET_ERROR = 'getError';
const NAME_GET_RESULT = 'getResult';

@Injectable({
  providedIn: 'root',
})
export class NatsService {
  private __conn?: nats.NatsConnection;

  /**
   * Constructor
   */
  constructor(
    @Inject(_MODULE_CONFIG) private __config: ModuleConfig,
    @Inject(DOCUMENT) private __document: Document,
    private __webSessionStorageService: WebSessionStorageService
  ) {}

  ngOnInit(): void {}

  // -----------------------------------------------------------------------------------------------------
  // @ Accessors
  // -----------------------------------------------------------------------------------------------------
  get connection(): nats.NatsConnection | undefined {
    return this.__conn;
  }

  // -----------------------------------------------------------------------------------------------------
  // @ Public methods
  // -----------------------------------------------------------------------------------------------------

  connect(): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      if (!!this.__conn) {
        return resolve();
      }
      nats
        .connect(this.__config.connectionOptions)
        .then((conn) => {
          console.log('NATS connected', conn.info?.client_ip);
          this.__conn = conn;

          this.__webSessionStorageService
            .watch('accessToken')
            .subscribe((accessToken) => {
              console.log('accessToken in NatsService', accessToken);
            });

          resolve();
        })
        .catch((e) => {
          reject(e);
        });
    });
  }

  request<R>(
    subject: string,
    req: Uint8Array,
    deserialize: (data: Uint8Array) => any,
    opts?: nats.RequestOptions | undefined
  ): Promise<R> {
    return new Promise<R>((resolve, reject) => {
      let c = new Client();
      c.setClientIp(this.__conn?.info?.client_ip + '');
      c.setAccessToken(this.__webSessionStorageService.get('accessToken'));
      c.setSiteUrl(this.__document.location.hostname);

      let _opts: nats.RequestOptions = !!opts ? opts : { timeout: 3000 };
      if (!_opts.headers) {
        _opts.headers = nats.headers();
      }

      _opts.headers.append(
        HEADER_CLIENT,
        Base64.fromUint8Array(c.serializeBinary())
      );

      this.__conn?.request(subject, req, _opts).then((msg) => {
        let res = deserialize(msg.data);

        let get_error = (res as any)[NAME_GET_ERROR];
        if (!!get_error) {
          let error = get_error.call(res);
          if (!!error) {
            return reject(error);
          }
        }

        let get_result = (res as any)[NAME_GET_RESULT];
        if (!!get_result) {
          let result = get_result.call(res);
          if (!result) {
            return reject('result is not exist');
          }
          return resolve(result);
        }

        return reject('protocol error');
      });
    });
  }
}
