import PouchDB from 'pouchdb';
import pluginFind from 'pouchdb-find';
import pluginAuth from 'pouchdb-authentication';
import pluginRelational from 'relational-pouch';
import pluginUpsert from 'pouchdb-upsert';
import { isDevMode, COUCHDB_HOST } from '@/config/env';
import { fetchCouchDatabase } from './autoshopee/db';
import { loggerModel } from '@/plugins/logger';
import { CacheCouchDbConnect } from '@/plugins/store/cache';
import { userSetting } from '@/plugins/setting';
import { setKeyCookie } from '@/browser/cookies';

const USE_SESSION = false;
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 1000;
const MIN_LIMIT = 1;
const DEFAULT_SKIP = 0;
const COUNT_LIMIT = 10000;
const AUTH_SESSION_COOKIE = 'AuthSession';
const SYNC_SINCE = 'now';

const VALIDATION_OPTIONS = {
  convert: true,
  abortEarly: false,
  allowUnknown: true,
};

const SYNC_OPTIONS = {
  live: true,
  retry: true,
};

const CHANGES_OPTIONS = {
  since: SYNC_SINCE,
  live: true,
  include_docs: true,
};

const mapCachedSyncRemote = new Map();

PouchDB.plugin(pluginFind).plugin(pluginRelational).plugin(pluginAuth).plugin(pluginUpsert);

export class ModelApiLoader extends PouchDB {
  static COUCHDB_HOST = COUCHDB_HOST;

  /**
   * @param {string} name
   * @param {JoiValidatorSchema} schema
   */
  constructor(name, schema) {
    super(name);
    this.remoteName = name;
    this.localName = name;
    this.schema = schema;
  }

  /**
   * @param {object} payload
   * @returns {Promise<object>}
   */
  async validate(payload) {
    if (!this.schema) {
      return payload;
    }

    return this.schema.validateAsync(payload, VALIDATION_OPTIONS);
  }

  /**
   * @returns {Promise<object[]>}
   */
  async findAll() {
    return (await this.allDocs({
      include_docs: true,
      attachments: true,
    })).rows.map((item) => item.doc);
  }

  /**
   * @param {object} where
   * @param {number} limit
   * @param {number} skip
   * @param {string[]} sort
   * @param {string[]} projection
   * @returns {Promise<object[]>}
   */
  async findMany(where = {}, limit = DEFAULT_LIMIT, skip = DEFAULT_SKIP, sort = null, projection = null) {
    const safeLimit = Number.isFinite(limit)
      ? Math.min(Math.max(limit, MIN_LIMIT), MAX_LIMIT)
      : DEFAULT_LIMIT;

    const safeSkip = Number.isFinite(skip) && skip >= 0
      ? skip
      : DEFAULT_SKIP;

    const payload = {
      selector: where,
      limit: safeLimit,
      skip: safeSkip,
    };

    if (projection?.length) {
      payload.fields = projection;
    }

    if (sort?.length) {
      payload.sort = sort;
    }

    const result = await this.find(payload);

    if (isDevMode && result.warning) {
      loggerModel.warn({
        msg: 'POUCHDB_FIND_WARNING',
        warning: result.warning,
        where,
        name: this.name,
      });
    }

    return result.docs;
  }

  /**
   * @returns {Promise<number>}
   */
  async countAll() {
    const result = await this.allDocs({
      include_docs: false,
    });

    return result.rows.length;
  }

  /**
   * @param {object} where
   * @returns {Promise<number>}
   */
  async count(where = {}) {
    if (!where || !Object.keys(where).length) {
      return this.countAll();
    }

    const result = await this.find({
      selector: where,
      fields: ['_id'],
      limit: COUNT_LIMIT,
    });

    return result.docs.length;
  }

  /**
   * @param {object} where
   * @returns {Promise<object | null>}
   */
  async findOne(where = {}) {
    const data = await this.findMany(where, MIN_LIMIT);
    return data?.[0] ?? null;
  }

  /**
   * @param {object} payload
   * @param {string | number} _id
   * @returns {Promise<object>}
   */
  async insert(payload, _id) {
    try {
      const docId = _id ? _id.toString() : payload._id;

      if (!docId) {
        payload = await this.validate(payload);
        payload.createdAt = new Date();
        payload.updatedAt = new Date();

        const { id, rev } = await this.post(payload);
        return { id, rev, ...payload };
      }

      const validatedPayload = await this.validate(payload);

      const result = await this.upsert(docId, (existingDoc) => {
        const mergedDoc = {
          ...existingDoc,
          ...validatedPayload,
          _id: docId,
        };

        if (existingDoc._id) {
          mergedDoc.createdAt = existingDoc.createdAt || new Date();
        } else {
          mergedDoc.createdAt = new Date();
        }

        mergedDoc.updatedAt = new Date();

        return mergedDoc;
      });

      return result;
    } catch (error) {
      loggerModel.error({
        msg: 'POUCHDB_INSERT_DATA_ERROR',
        name: this.name,
        error,
        payload,
        insertId: _id,
      });
      throw error;
    }
  }

  /**
   * @param {string} id
   * @returns {Promise<object | null>}
   */
  async getId(id) {
    try {
      return await this.get(id);
    } catch {
      return null;
    }
  }

  /**
   * @param {string} id
   * @param {object} payload
   * @param isValidate
   * @returns {Promise<object | null>}
   */
  async updateId(id, payload, isValidate = true) {
    try {
      const data = await this.getId(id);
      if (!data) {
        return null;
      }

      let setUpload = { ...data, ...payload };

      if (isValidate) {
        setUpload = await this.validate(setUpload);
      }

      setUpload.updatedAt = new Date();

      return this.put({
        _id: id,
        _rev: data._rev,
        ...setUpload,
      });
    } catch (error) {
      loggerModel.error({
        msg: 'POUCHDB_UPDATE_DATA_ERROR',
        name: this.name,
        error,
        updateId: id,
        payload,
      });
      throw error;
    }
  }

  /**
   * @param {string} id
   * @param {object} payload
   * @returns {Promise<object | null>}
   */
  async setId(id, payload) {
    const data = await this.getId(id);
    if (!data) {
      return null;
    }
    return this.put({ _id: id, ...payload });
  }

  /**
   * @param {string} id
   * @returns {Promise<object | null>}
   */
  async removeId(id) {
    const data = await this.getId(id);
    if (!data) {
      return null;
    }
    return this.remove(data._id, data._rev);
  }

  async removeAll() {
    const allDocs = await this.findAll();
    const deleteDocs = allDocs.map((doc) => ({
      _id: doc._id,
      _rev: doc._rev,
      _deleted: true,
    }));
    return this.bulkDocs(deleteDocs);
  }

  async removeQuery(query = {}) {
    const allDocs = await this.findMany(query);
    const deleteDocs = allDocs.map((doc) => ({
      _id: doc._id,
      _rev: doc._rev,
      _deleted: true,
    }));
    return this.bulkDocs(deleteDocs);
  }

  /**
   * @param {string[]} session
   * @param {string} url
   * @returns {void}
   */
  async sessionRemote(session, url) {
    const sessionUrl = url || COUCHDB_HOST;
    return Promise.all(
      session.map((cookieValue) => {
        const [name, value] = cookieValue.split(';')[0].split('=');
        return setKeyCookie(name, value, sessionUrl);
      }),
    );
  }

  /**
   * @param {string} url
   * @returns {Promise<void>}
   */
  async sessionLogout(url) {
    const sessionUrl = url || COUCHDB_HOST;
    return setKeyCookie(AUTH_SESSION_COOKIE, '', sessionUrl);
  }

  /**
   * @returns {Promise<{remoteUrl: string,}>}
   */
  async getRemoteUrl() {
    const returnRemote = {
      remoteUrl: '',
    };

    const ownerId = await userSetting.userId.getSync();

    if (!(ownerId)) {
      return returnRemote;
    }

    const cacheKey = [ownerId, this.name].join('-');
    const CacheConnect = new CacheCouchDbConnect(cacheKey);

    const getCache = await CacheConnect.get();

    if (getCache) {
      if (USE_SESSION && getCache.session) {
        await this.sessionRemote(getCache.session, getCache.uri);
        returnRemote.remoteUrl = getCache.uri;
      } else {
        returnRemote.remoteUrl = getCache.url;
      }

      return returnRemote;
    }

    const fetchAuth = await fetchCouchDatabase(this.name);

    if (!(fetchAuth?.db)) {
      return returnRemote;
    }

    await CacheConnect.set(fetchAuth);

    if (USE_SESSION && fetchAuth.session) {
      await this.sessionRemote(fetchAuth.session, fetchAuth.uri);
      returnRemote.remoteUrl = fetchAuth.uri;
    } else {
      returnRemote.remoteUrl = fetchAuth.url;
    }

    return returnRemote;
  }

  /**
   * @returns {Promise<object | null>}
   */
  async pushRemote() {
    const { remoteUrl } = await this.getRemoteUrl();

    return this.replicate.to(remoteUrl);
  }

  /**
   * @returns {Promise<object | null>}
   */
  async pullRemote() {
    const { remoteUrl } = await this.getRemoteUrl();

    return this.replicate.from(remoteUrl);
  }

  /**
   * @returns {Promise<boolean | null>}
   */
  async syncRemote() {
    const cacheKey = this.name;

    if (mapCachedSyncRemote.has(cacheKey)) {
      return mapCachedSyncRemote.get(cacheKey);
    }

    const { remoteUrl } = await this.getRemoteUrl();

    PouchDB.sync(this.name, remoteUrl, SYNC_OPTIONS)
      .on('change', () => loggerModel.log('📦 Sync change:', this.name))
      .on('paused', () => loggerModel.log('⏸ Sync paused', this.name))
      .on('active', () => loggerModel.log('▶️ Sync resumed', this.name))
      .on('error', (error) => loggerModel.error('❌ Sync error:', this.name, error));

    mapCachedSyncRemote.set(cacheKey, {
      remoteUrl,
    });

    return {
      remoteUrl,
    };
  }

  /**
   * @param {Function} callback
   * @param {object} options
   * @returns {object}
   */
  changesListener(callback, options = {}) {
    return this.changes({
      ...CHANGES_OPTIONS,
      ...options,
    }).on('change', callback);
  }
}
