
const uuidv4 = (): string => {
   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
      const r = crypto.getRandomValues(new Uint8Array(1))[0] & 15;
      const v = c === 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
   });
}

const xorEncrypt = (text: string, key: string) => {
   const textBytes = new TextEncoder().encode(text);
   const keyBytes = new TextEncoder().encode(key);

   const result = new Uint8Array(textBytes.length);
   for (let i = 0; i < textBytes.length; i++) {
      result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
   }


   return btoa(String.fromCharCode(...Array.from(result)));
}

const xorDecrypt = (encodedText: string, key: string) => {
   const binaryString = atob(encodedText);
   const textBytes = new Uint8Array(binaryString.length);
   for (let i = 0; i < binaryString.length; i++) {
      textBytes[i] = binaryString.charCodeAt(i);
   }

   const keyBytes = new TextEncoder().encode(key);
   const result = new Uint8Array(textBytes.length);

   for (let i = 0; i < textBytes.length; i++) {
      result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
   }

   return new TextDecoder().decode(result);
}


export class rpcClient {
   private static domains: string[] = [];
   private static endpoint: string = '';
   private static readonly ENDPOINT_INDEX_KEY = 'rpcClient_endpointIndex';
   private static currentDomainIndex: number = 0;
   private static initialized: boolean = false;
   private static probeCompleted: boolean = false;
   private static readonly PING_TIMEOUT = 2000;
   private static headers: Record<string, string> = {};
   private static tokenGetter: (() => string | null) | null = null;
   private static tokenGetterAsync: (() => Promise<string | null>) | null = null;
   private static unauthorizedHandler: (() => void) | null = null;
   private static networkErrorHandler: (() => void) | null = null;
   private static toastHandler: ((message: string) => void) | null = null;

   public static getRedirectURL(to: string): string {

      let path = '/redirect?to=' + encodeURIComponent(to);

      let url = this.getCurrentEndpointDomain() + path;

      return url;

   }

   private static defaultExtensions: DefaultExtensionsType = [];

   public static setDefaultExtensions(extensions: DefaultExtensionsType): void {
      this.defaultExtensions = extensions;
   }

   private static abortControllerTimeout: number = 30000;

   public static setAbortControllerTimeout(timeout: number): void {
      this.abortControllerTimeout = timeout;
   }

   private static useEncrypt: boolean = false;

   public static setUseEncrypt(useEncrypt: boolean): void {
      this.useEncrypt = useEncrypt;
   }

   public static setNetworkErrorHandler(handler: () => void): void {
      this.networkErrorHandler = handler;
   }

   public static async init(): Promise<void> {
      if (this.initialized) return;
      try {
         const savedIndex = localStorage.getItem(this.ENDPOINT_INDEX_KEY);
         if (savedIndex !== null) {
            const index = parseInt(savedIndex, 10);
            if (!isNaN(index) && index >= 0 && index < this.domains.length) {
               this.currentDomainIndex = index;
            }
         }
      } catch (error) {
         console.log('⚠️ Ошибка при загрузке индекса endpoint:', error);
      }
      this.initialized = true;
   }

   private static async saveEndpointIndex(): Promise<void> {
      try {
         localStorage.setItem(this.ENDPOINT_INDEX_KEY, this.currentDomainIndex.toString());
      } catch (error) {
         console.log('⚠️ Ошибка при сохранении индекса endpoint:', error);
      }
   }

   private static switchToNextDomain(): void {
      if (this.domains.length <= 1) return;
      this.currentDomainIndex = (this.currentDomainIndex + 1) % this.domains.length;
      this.saveEndpointIndex();
   }

   private static isConfigReady(): boolean {
      return this.domains.length > 0 && this.endpoint.length > 0;
   }

   private static async waitForConfig(): Promise<void> {
      if (!this.isConfigReady()) {
         // Ждем 1 секунду и проверяем еще раз
         await new Promise<void>((resolve) => setTimeout(resolve, 1000));

         if (!this.isConfigReady()) {
            throw new Error('Domains and endpoint are not set');
         }
      }

      await this.init();
      await this.probeDomains();
   }

   private static async pingDomain(domain: string, index: number): Promise<boolean> {
      const url = this.joinUrl(domain, 'ping');
      console.log(`[rpcClient] probe: checking [${index}] ${domain} → ${url}`);

      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.PING_TIMEOUT);

      try {
         const response = await fetch(url, { method: 'GET', signal: controller.signal });
         const text = await response.text();
         const ok = response.ok && text.trim() === 'pong';

         if (ok) {
            console.log(`[rpcClient] probe: [${index}] ${domain} → pong`);
         } else {
            console.log(`[rpcClient] probe: [${index}] ${domain} → fail (status=${response.status}, body="${text.trim()}")`);
         }

         return ok;
      } catch (error) {
         console.log(`[rpcClient] probe: [${index}] ${domain} → fail (${error})`);
         return false;
      } finally {
         clearTimeout(timeoutId);
      }
   }

   private static async probeDomains(): Promise<void> {
      if (this.probeCompleted) {
         return;
      }

      if (this.domains.length <= 1) {
         console.log(`[rpcClient] probe: skipped (single domain) → [0] ${this.domains[0] ?? 'none'}`);
         this.probeCompleted = true;
         return;
      }

      const startIndex = this.currentDomainIndex;
      console.log(`[rpcClient] probe: start from index ${startIndex}, total ${this.domains.length} domains`);

      let selectedIndex: number | null = null;

      for (let i = 0; i < this.domains.length; i++) {
         const idx = (startIndex + i) % this.domains.length;
         if (await this.pingDomain(this.domains[idx], idx)) {
            selectedIndex = idx;
            if (idx !== this.currentDomainIndex) {
               this.currentDomainIndex = idx;
               await this.saveEndpointIndex();
            }
            break;
         }
      }

      if (selectedIndex !== null) {
         console.log(`[rpcClient] probe: selected domain [${selectedIndex}] ${this.domains[selectedIndex]}`);
      } else {
         console.log(`[rpcClient] probe: no domain responded, keeping index [${this.currentDomainIndex}] ${this.domains[this.currentDomainIndex]}`);
      }

      this.probeCompleted = true;
   }

   private static joinUrl(domain: string, endpoint: string): string {
      const base = domain.replace(/\/+$/, '');
      const path = endpoint.replace(/^\/+/, '');
      if (!path) return base;
      return `${base}/${path}`;
   }

   private static getCurrentEndpoint(): string {
      if (!this.isConfigReady()) throw new Error('Domains and endpoint are not set');
      return this.joinUrl(this.domains[this.currentDomainIndex], this.endpoint);
   }

   public static getCurrentEndpointDomain(): string {
      return this.domains[this.currentDomainIndex];
   }

   private static isNetworkError(error: any): boolean {
      return (
         error.name === 'AbortError' ||
         error.name === 'TimeoutError' ||
         error.name === 'TypeError' ||
         error.message?.includes('Network request failed') ||
         error.message?.includes('Failed to fetch') ||
         error.message?.includes('NetworkError')
      );
   }

   public static setToken(getter: () => string | null): void {
      this.tokenGetter = getter;
   }

   public static setTokenAsync(getter: () => Promise<string | null>): void {
      this.tokenGetterAsync = getter;
   }

   public static setUnauthorizedHandler(handler: () => void): void {
      this.unauthorizedHandler = handler;
   }

   public static setToast(handler: (message: string) => void): void {
      this.toastHandler = handler;
   }

   public static setDomains(domains: string[]): void {
      this.domains = domains;
      this.currentDomainIndex = 0;
      this.initialized = false;
      this.probeCompleted = false;
   }

   public static setEndpoint(endpoint: string): void {
      this.endpoint = endpoint;
      this.initialized = false;
      this.probeCompleted = false;
   }

   public static getToken(): string | null {
      if (this.tokenGetter) return this.tokenGetter();
      return null;
   }

   public static async getTokenAsync(): Promise<string | null> {
      // Приоритет у синхронной функции
      if (this.tokenGetter) {
         return this.tokenGetter();
      }
      // Если синхронной нет, используем асинхронную
      if (this.tokenGetterAsync) {
         return await this.tokenGetterAsync();
      }
      return null;
   }

   public static setHeaders(headers: Record<string, string>): void {
      this.headers = { ...this.headers, ...headers };
   }

   private static handleError(error: any): void {
      if (error?.type === 'UNAUTHORIZED' || error?.type === 'UNAUTHENTICATED') {
         if (this.unauthorizedHandler) {
            this.unauthorizedHandler();
         }
      } else if (error?.message) {
         if (this.toastHandler) {
            this.toastHandler(error.message);
         }
      }
   }

   public static async callFormData<R>(method: string, formData: FormData): Promise<R> {
      await this.waitForConfig();
      const endpoint = this.getCurrentEndpoint();

      formData.append('method', method);

      let _token = await this.getTokenAsync();
      if (_token) {
         formData.append('token', _token);
      }

      formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);

      const context = this.useEncrypt ? uuidv4() : undefined;

      if (this.useEncrypt && context) {
         formData.append('context', context);
      }


      const headers = { ...this.headers };
      delete headers['Content-Type'];

      const controller = new AbortController();
      const timeoutId = setTimeout(() => {
         controller.abort();
      }, this.abortControllerTimeout);

      return fetch(endpoint, { method: 'POST', headers, body: formData, signal: controller.signal })
         .then(res => res.json())
         .then((json: RpcResponse<R>) => {
            clearTimeout(timeoutId);

            if (this.useEncrypt && context) {

               if (json.result) {
                  json.result = JSON.parse(xorDecrypt(json.result as string, context));
               }
               if (json.extensions) {
                  json.extensions = JSON.parse(xorDecrypt(json.extensions as string, context));
               }
            }


            if (json?.error) {
               this.handleError(json.error);
               throw json.error;
            }
            this.saveEndpointIndex();
            return json.result as R;
         })
         .catch(err => {
            clearTimeout(timeoutId);
            if (this.isNetworkError(err)) {

               if (this.networkErrorHandler) {
                  this.networkErrorHandler();
               }

               this.switchToNextDomain();
            }
            throw err;
         });
   }

   public static async call<P, R, E>(method: string, params?: P, extensions?: Array<string>): Promise<RpcResponse<R, E>> {
      await this.waitForConfig();
      const endpoint = this.getCurrentEndpoint();


      const context = this.useEncrypt ? uuidv4() : undefined;


      let _extensions = extensions != undefined ? extensions : this.defaultExtensions;


      const token = await this.getTokenAsync();
      const body = {
         method, extensions: _extensions, context: context, token: token,
         params: params ? context ? xorEncrypt(JSON.stringify(params ?? {}), context) : params : undefined,
         id: Date.now().toString()
      };

      console.log('🚀🚀🚀 method: ', method, ' RPC Endpoint: ', endpoint);
      console.log('🚀🚀🚀 method: ', method, ' RPC Body: ', JSON.stringify(body));


      const controller = new AbortController();
      const timeoutId = setTimeout(() => {
         controller.abort();
      }, this.abortControllerTimeout);

      return fetch(endpoint, {
         method: 'POST', headers: {
            'Content-Type': 'application/json',
            'timezone': Intl.DateTimeFormat().resolvedOptions().timeZone,
            ...this.headers
         }, body: JSON.stringify(body),
         signal: controller.signal
      })
         .then(res => res.json())
         .then((json: RpcResponse<R>) => {
            clearTimeout(timeoutId);

            if (this.useEncrypt && context) {

               if (json.result) {
                  json.result = JSON.parse(xorDecrypt(json.result as string, context));
               }
               if (json.extensions) {
                  json.extensions = JSON.parse(xorDecrypt(json.extensions as string, context));
               }
            }

            console.log('🍪🍪🍪 method: ', method, ' RPC Response: ', JSON.stringify(json));
            if (json?.error) {
               this.handleError(json.error);
               throw json.error;
            }
            this.saveEndpointIndex();

            if (json?.extensions) {
               ExtensionsStore.addToStore(json.extensions);
            }

            return json as RpcResponse<R, E>;
         })
         .catch(err => {
            clearTimeout(timeoutId);
            if (this.isNetworkError(err)) {
               if (this.networkErrorHandler) {
                  this.networkErrorHandler();
               }
               this.switchToNextDomain();
            }
            console.log('🛑🛑🛑 RPC Error: ', JSON.stringify(body));
            throw err;
         });
   }
}
