/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: MPL-2.0
 */

import { inject as service } from '@ember/service';
import Adapter from '@ember-data/adapter';
import AdapterError, {
  AbortError,
  TimeoutError,
  ServerError,
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  ConflictError,
  InvalidError,
} from '@ember-data/adapter/error';

// TODO These are now exactly the same, apart from the fact that one uses
// `serialized, unserialized` and the other just `query`
// they could actually be one function now, but would be nice to think about
// the naming of things (serialized vs query etc)
const read = function (adapter, modelName, type, query = {}) {
  return adapter.rpc(
    function (adapter, ...rest) {
      return adapter[`requestFor${type}`](...rest);
    },
    function (serializer, ...rest) {
      return serializer[`respondFor${type}`](...rest);
    },
    query,
    modelName
  );
};
const write = function (adapter, modelName, type, snapshot) {
  return adapter.rpc(
    function (adapter, ...rest) {
      return adapter[`requestFor${type}`](...rest);
    },
    function (serializer, ...rest) {
      return serializer[`respondFor${type}`](...rest);
    },
    snapshot,
    modelName
  );
};
export default class HttpAdapter extends Adapter {
  @service('client/http') client;

  rpc(req, resp, obj, modelName) {
    const client = this.client;
    const store = this.store;
    const adapter = this;

    let unserialized, serialized;
    const serializer = store.serializerFor(modelName);
    const modelClass = store.modelFor(modelName);
    // workable way to decide whether this is a snapshot
    // essentially 'is attributable'.
    // Snapshot is private so we can't do instanceof here
    // and using obj.constructor.name gets changed/minified
    // during compilation so you can't rely on it
    // checking for `attributes` being a function is more
    // reliable as that is the thing we need to call
    if (typeof obj.attributes === 'function') {
      unserialized = obj.attributes();
      serialized = serializer.serialize(obj, {});
    } else {
      unserialized = obj;
      serialized = unserialized;
    }

    return client
      .request(function (request) {
        return req(adapter, request, serialized, unserialized, modelClass);
      })
      .catch(function (e) {
        return adapter.error(e);
      })
      .then(function (respond) {
        // TODO: When HTTPAdapter:responder changes, this will also need to change
        return resp(serializer, respond, serialized, unserialized, modelClass);
      });
    // TODO: Potentially add specific serializer errors here
    // .catch(function(e) {
    //   return Promise.reject(e);
    // });
  }

  error(err) {
    if (err instanceof TypeError) {
      throw err;
    }
    const errors = [
      {
        status: `${err.statusCode}`,
        title: 'The backend responded with an error',
        detail: err.message,
      },
    ];
    let error;
    const detailedMessage = '';
    try {
      switch (err.statusCode) {
        case 0:
          error = new AbortError();
          error.errors[0].status = '0';
          break;
        case 401:
          error = new UnauthorizedError(errors, detailedMessage);
          break;
        case 403:
          error = new ForbiddenError(errors, detailedMessage);
          break;
        case 404:
          error = new NotFoundError(errors, detailedMessage);
          break;
        case 408:
          error = new TimeoutError();
          break;
        case 409:
          error = new ConflictError(errors, detailedMessage);
          break;
        case 422:
          error = new InvalidError(errors); //payload.errors
          break;
        default:
          if (err.statusCode >= 500) {
            error = new ServerError(errors, detailedMessage);
          } else {
            error = new AdapterError(errors, detailedMessage);
          }
      }
    } catch (e) {
      error = e;
    }
    // TODO: This comes originates from ember-data
    // This can be confusing if you need to use this with Promise.reject
    // Consider changing this to return the error and then
    // throw from the call site instead
    throw error;
  }

  query(store, type, query) {
    return read(this, type.modelName, 'Query', query);
  }

  queryRecord(store, type, query) {
    return read(this, type.modelName, 'QueryRecord', query);
  }

  findAll(store, type) {
    return read(this, type.modelName, 'FindAll');
  }

  createRecord(store, type, snapshot) {
    return write(this, type.modelName, 'CreateRecord', snapshot);
  }

  updateRecord(store, type, snapshot) {
    return write(this, type.modelName, 'UpdateRecord', snapshot);
  }

  deleteRecord(store, type, snapshot) {
    return write(this, type.modelName, 'DeleteRecord', snapshot);
  }
}
