import type { StorageAdapter } from "grammy";

export interface CouchDBStorageOptions {
  /** Если true, при удалении сессии будет ставиться _deleted: true для поддержки репликаций по условиям полей */
  softDelete?: boolean;
  type?: string;
}

export interface CouchDBSessionDocument<T> {
  _id: string;
  _rev?: string;
  type: string;
  session?: T;
  created_at: string;
  updated_at: string;
  _deleted?: boolean;
}

export class CouchDBStorageAdapter<T> implements StorageAdapter<T> {
  constructor(
    private db: PouchDB.Database<CouchDBSessionDocument<T>>,
    private options: CouchDBStorageOptions = {
      softDelete: false,
      type: "tg_bot_session",
    },
  ) {}

  /**
   * Чтение сессии по ключу.
   * Возвращает данные сессии или undefined, если сессия не найдена.
   */
  async read(key: string) {
    try {
      const id = this.getId(key);
      const doc = await this.db.get(id);
      return doc.session;
    } catch (error: any) {
      if (error.status !== 404) {
        throw error;
      }
    }
  }

  /**
   * Запись или обновление сессии по ключу.
   * При конфликте повторяет попытку записи.
   */
  async write(key: string, value: T) {
    const _id = this.getId(key);
    await this.retryOnConflict(async () => {
      let doc: CouchDBSessionDocument<T>;
      const updated_at = new Date().toISOString();
      try {
        doc = await this.db.get(_id);
        doc.session = value;
        doc.updated_at = updated_at;
      } catch (error: any) {
        if (error.status === 404) {
          // Документ не найден, создаём новый
          doc = {
            _id,
            type: "tg_bot_session",
            session: value,
            created_at: updated_at,
            updated_at,
          };
        } else {
          throw error;
        }
      }

      await this.db.put(doc);
    });
  }

  /**
   * Удаление сессии по ключу без предварительного получения документа.
   * Если включен softDelete, пытаемся сразу поставить _deleted: true.
   * Если нет softDelete, просто пытаемся удалить без rev, что приведёт к ошибке 409,
   * тогда при конфликте получим актуальный _rev и удалим.
   */
  async delete(key: string) {
    const id = this.getId(key);
    await this.retryOnConflict(async () => {
      let doc: CouchDBSessionDocument<T>;
      try {
        doc = await this.db.get(id);
      } catch (error: any) {
        if (error.status === 404) {
          return; // Документ уже отсутствует
        } else {
          throw error;
        }
      }

      if (this.options.softDelete) {
        doc._deleted = true; // Помечаем документ как удалённый
        await this.db.put(doc);
      } else {
        await this.db.remove(id, doc._rev!);
      }
    });
  }

  /**
   * Вспомогательная функция для повторения операции при конфликте.
   * Если при put/remove случится конфликт, повторим попытку до retries раз.
   * fn принимает параметр attempt (номер попытки, начиная с 1).
   */
  private async retryOnConflict(
    fn: (attempt: number) => Promise<void>,
    retries = 3,
  ): Promise<void> {
    for (let i = 0; i < retries; i++) {
      try {
        await fn(i + 1);
        return;
      } catch (error: any) {
        if (error.status === 409 && i < retries - 1) {
          // Попытка ещё раз при конфликте
        } else {
          throw error;
        }
      }
    }
  }

  private getId(key: string) {
    return this.options.type ? `${this.options.type}:${key}` : key;
  }
}
