/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: BUSL-1.1
 */

import Service, { inject as service } from '@ember/service';
import { assert } from '@ember/debug';
import { typeOf } from '@ember/utils';
import { get, set } from '@ember/object';
import { isChangeset } from 'validated-changeset';
import HTTPError from 'consul-ui/utils/http/error';
import { ACCESS_READ } from 'consul-ui/abilities/base';

export const softDelete = (repo, item) => {
  // Some deletes need to be more of a soft delete.
  // Therefore the partition still exists once we've requested a delete/removal.
  // This makes 'removing' more of a custom action rather than a standard
  // ember-data delete.
  // Here we use the same request for a delete but we bypass ember-data's
  // destroyRecord/unloadRecord and serialization so we don't get
  // ember data error messages when the UI tries to update a 'DeletedAt' property
  // on an object that ember-data is trying to delete
  const res = repo.store.adapterFor(repo.getModelName()).rpc(
    (adapter, request, serialized, unserialized) => {
      return adapter.requestForDeleteRecord(request, serialized, unserialized);
    },
    (serializer, respond, serialized, unserialized) => {
      return item;
    },
    item,
    repo.getModelName()
  );
  return res;
};
export default class RepositoryService extends Service {
  @service('store') store;
  @service('env') env;
  @service('repository/permission') permissions;

  getModelName() {
    assert('RepositoryService.getModelName should be overridden', false);
  }

  getPrimaryKey() {
    assert('RepositoryService.getPrimaryKey should be overridden', false);
  }

  getSlugKey() {
    assert('RepositoryService.getSlugKey should be overridden', false);
  }

  /**
   * Creates a set of permissions based on an id/slug, loads in the access
   * permissions for them and checks/validates
   */
  async authorizeBySlug(cb, access, params) {
    params.resources = await this.permissions.findBySlug(params, this.getModelName());
    return this.validatePermissions(cb, access, params);
  }

  /**
   * Loads in the access permissions and checks/validates them for a set of
   * permissions
   */
  async authorizeByPermissions(cb, access, params) {
    params.resources = await this.permissions.authorize(params);
    return this.validatePermissions(cb, access, params);
  }

  /**
   * Checks already loaded permissions for certain access before calling cb to
   * return the thing you wanted to check the permissions on
   */
  async validatePermissions(cb, access, params) {
    // inspect the permissions for this segment/slug remotely, if we have zero
    // permissions fire a fake 403 so we don't even request the model/resource
    if (params.resources.length > 0) {
      const resource = params.resources.find((item) => item.Access === access);
      if (resource && resource.Allow === false) {
        // TODO: Here we temporarily make a hybrid HTTPError/ember-data HTTP error
        // we should eventually use HTTPError's everywhere
        const e = new HTTPError(403);
        e.errors = [{ status: '403' }];
        throw e;
      }
    }
    const item = await cb(params.resources);
    // add the `Resource` information to the record/model so we can inspect
    // them in other places like templates etc
    // TODO: We mostly use this to authorize single items but we do
    // occasionally get an array back here e.g. service-instances, so we
    // should make this fact more obvious
    if (get(item, 'Resources')) {
      set(item, 'Resources', params.resources);
    }
    return item;
  }

  shouldReconcile(item, params) {
    const dc = get(item, 'Datacenter');
    if (dc !== params.dc) {
      return false;
    }
    if (this.env.var('CONSUL_NSPACES_ENABLED')) {
      const nspace = get(item, 'Namespace');
      if (typeof nspace !== 'undefined' && nspace !== params.ns) {
        return false;
      }
    }
    if (this.env.var('CONSUL_PARTITIONS_ENABLED')) {
      const partition = get(item, 'Partition');
      if (typeof partition !== 'undefined' && partition !== params.partition) {
        return false;
      }
    }

    // We were seeing issues where services were all being unloaded after visiting
    // a peers imported services page. So if you viewes services -> peered imported services -> services
    // only the peered services would remain as the others were all unloaded. Not certain if
    // we should be doing any manual reconciling as it is. Not enough historical context
    // to determine that at this time.
    //
    // https://hashicorp.atlassian.net/browse/NET-6900
    if (this.env.var('CONSUL_PEERINGS_ENABLED') && this.getModelName() === 'service') {
      return false;
    }
    return true;
  }

  reconcile(meta = {}, params = {}, configuration = {}) {
    // unload anything older than our current sync date/time
    if (typeof meta.date !== 'undefined') {
      this.store.peekAll(this.getModelName()).forEach((item) => {
        const date = get(item, 'SyncTime');
        if (
          !item.isDeleted &&
          typeof date !== 'undefined' &&
          date != meta.date &&
          this.shouldReconcile(item, params)
        ) {
          this.store.unloadRecord(item);
        }
      });
    }
  }

  peekOne(id) {
    return this.store.peekRecord(this.getModelName(), id);
  }

  peekAll() {
    return this.store.peekAll(this.getModelName());
  }

  cached(params) {
    const entries = Object.entries(params);
    return this.store.peekAll(this.getModelName()).filter((item) => {
      return entries.every(([key, value]) => item[key] === value);
    });
  }

  // @deprecated
  async findAllByDatacenter(params, configuration = {}) {
    return this.findAll(...arguments);
  }

  async findAll(params = {}, configuration = {}) {
    if (typeof configuration.cursor !== 'undefined') {
      params.index = configuration.cursor;
      params.uri = configuration.uri;
    }
    return this.query(params);
  }

  async query(params = {}, configuration = {}) {
    let error, meta, res;
    try {
      res = await this.store.query(this.getModelName(), params);
      meta = res.meta;
    } catch (e) {
      switch (get(e, 'errors.firstObject.status')) {
        case '404':
        case '403':
          meta = {
            date: Number.POSITIVE_INFINITY,
          };
          error = e;
          break;
        default:
          throw e;
      }
    }
    if (typeof meta !== 'undefined') {
      this.reconcile(meta, params, configuration);
    }
    if (typeof error !== 'undefined') {
      throw error;
    }
    return res;
  }

  async findBySlug(params, configuration = {}) {
    if (params.id === '') {
      return this.create({
        Datacenter: params.dc,
        Namespace: params.ns,
        Partition: params.partition,
      });
    }
    if (typeof configuration.cursor !== 'undefined') {
      params.index = configuration.cursor;
      params.uri = configuration.uri;
    }
    return this.authorizeBySlug(
      () => this.store.queryRecord(this.getModelName(), params),
      ACCESS_READ,
      params
    );
  }

  create(obj) {
    // TODO: This should probably return a Promise
    return this.store.createRecord(this.getModelName(), obj);
  }

  persist(item) {
    // workaround for saving changesets that contain fragments
    // firstly commit the changes down onto the object if
    // its a changeset, then save as a normal object
    if (isChangeset(item)) {
      item.execute();
      item = item.data;
    }
    set(item, 'SyncTime', undefined);
    return item.save();
  }

  remove(obj) {
    let item = obj;
    if (typeof obj.destroyRecord === 'undefined') {
      item = obj.get('data');
    }
    // TODO: Change this to use vanilla JS
    // I think this was originally looking for a plain object
    // as opposed to an ember one
    if (typeOf(item) === 'object') {
      item = this.store.peekRecord(this.getModelName(), item[this.getPrimaryKey()]);
    }
    return item.destroyRecord().then((item) => {
      return this.store.unloadRecord(item);
    });
  }

  invalidate() {
    // TODO: This should probably return a Promise
    this.store.unloadAll(this.getModelName());
  }
}
