import { effect, inject, Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { AuthService } from '@core/auth';
import { memorize } from '@utils/common';
import { interval } from 'rxjs';

interface DbEntry<T> {
  key: string;
  token: object | string;
  data: T;
  ttl: number;
}

const H12INMS = 1000 * 60 * 60 * 12;

@Injectable({ providedIn: 'root' })
export class CacheService {
  private auth = inject(AuthService);

  private db!: IDBDatabase;

  get sub() {
    return Number(this.auth.getClaimByKey('sub'));
  }

  _cleanupInterval = toSignal(interval(1000 * 60));

  _cleanupIntervalEffect = effect(() => {
    this._cleanupInterval();
    this.cleanup();
  });

  get storeName() {
    return 'cache';
  }

  private getKey(token: object | string): string {
    if (typeof token === 'string') {
      return this.hash(token);
    }

    return this.hash(JSON.stringify(token));
  }

  private hash(data: string): string {
    let hash = 0;
    for (let i = 0; i < data.length; i++) {
      hash = data.charCodeAt(i) + ((hash << 5) - hash);
    }
    return (this.sub + hash).toString(16);
  }

  @memorize()
  private async openDB(): Promise<IDBDatabase> {
    if (this.db) {
      return this.db; // Datenbank bereits geöffnet, bestehende Verbindung zurückgeben
    }

    return new Promise<IDBDatabase>((resolve, reject) => {
      const request = indexedDB.open('isa-cache', 1);

      request.onerror = (event) => {
        reject(event);
      };

      request.onupgradeneeded = (event) => {
        this.db = (event.target as IDBOpenDBRequest).result;

        if (!this.db.objectStoreNames.contains(this.storeName)) {
          this.db.createObjectStore(this.storeName, { keyPath: 'key' });
        }
      };

      request.onsuccess = (event) => {
        this.db = (event.target as IDBOpenDBRequest).result;
        resolve(this.db);
      };
    });
  }

  private async getObjectStore(mode: IDBTransactionMode = 'readonly'): Promise<IDBObjectStore> {
    const db = await this.openDB(); // Datenbankverbindung öffnen oder wiederverwenden
    const transaction = db.transaction(this.storeName, mode);
    return transaction.objectStore(this.storeName);
  }

  async set<T>(token: object | string, data: T, options: { ttl?: number } = {}): Promise<string> {
    const store = await this.getObjectStore('readwrite');
    return new Promise<string>((resolve, reject) => {
      const key = this.getKey(token);
      const entry: DbEntry<T> = {
        key,
        data,
        token,
        ttl: Date.now() + (options.ttl || H12INMS),
      };

      const request = store.put(entry);
      request.onsuccess = (event) => {
        resolve(key);
      };
      request.onerror = (event) => {
        reject(event);
      };
    });
  }

  private async cached(token: object | string): Promise<DbEntry<any> | undefined> {
    const store = await this.getObjectStore();
    return new Promise<DbEntry<any> | undefined>((resolve, reject) => {
      const request = store.get(this.getKey(token));
      request.onsuccess = (event) => {
        resolve((event.target as IDBRequest).result);
      };
      request.onerror = (event) => {
        reject(event);
      };
    });
  }

  async get<T = any>(token: object | string): Promise<T | undefined> {
    const cached = await this.cached(token);

    if (!cached) {
      return undefined;
    }

    if (cached.ttl < Date.now()) {
      this.delete(token);
      return undefined;
    }

    return cached.data;
  }

  async delete(token: object | string): Promise<void> {
    const store = await this.getObjectStore('readwrite');
    return new Promise<void>((resolve, reject) => {
      const request = store.delete(this.getKey(token));
      request.onsuccess = () => {
        resolve();
      };
      request.onerror = (event) => {
        reject(event);
      };
    });
  }

  async cleanup() {
    const store = await this.getObjectStore('readwrite');

    store.openCursor().onsuccess = (event) => {
      const cursor = (event.target as IDBRequest).result;
      if (cursor) {
        if (cursor.value.ttl < Date.now()) {
          store.delete(cursor.key);
        }
        cursor.continue();
      }
    };

    return new Promise<void>((resolve, reject) => {
      store.transaction.oncomplete = () => {
        resolve();
      };
      store.transaction.onerror = (event) => {
        reject(event);
      };
    });
  }

  async clear() {
    const store = await this.getObjectStore('readwrite');
    return new Promise<void>((resolve, reject) => {
      const request = store.clear();
      request.onsuccess = () => {
        resolve();
      };
      request.onerror = (event) => {
        reject(event);
      };
    });
  }
}
