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

import Serializer from './http';
import { set } from '@ember/object';

import {
  HEADERS_SYMBOL as HTTP_HEADERS_SYMBOL,
  HEADERS_INDEX as HTTP_HEADERS_INDEX,
  HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER,
  HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE,
  HEADERS_PARTITION as HTTP_HEADERS_PARTITION,
} from 'consul-ui/utils/http/consul';
import { CACHE_CONTROL as HTTP_HEADERS_CACHE_CONTROL } from 'consul-ui/utils/http/headers';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
import { NSPACE_KEY } from 'consul-ui/models/nspace';
import { PARTITION_KEY } from 'consul-ui/models/partition';
import createFingerprinter from 'consul-ui/utils/create-fingerprinter';

const map = function (obj, cb) {
  if (!Array.isArray(obj)) {
    return [obj].map(cb)[0];
  }
  return obj.map(cb);
};

const attachHeaders = function (headers, body, query = {}) {
  // lowercase everything incase we get browser inconsistencies
  const lower = {};
  Object.keys(headers).forEach(function (key) {
    lower[key.toLowerCase()] = headers[key];
  });
  //
  body[HTTP_HEADERS_SYMBOL] = lower;
  return body;
};
export default class ApplicationSerializer extends Serializer {
  attachHeaders = attachHeaders;
  fingerprint = createFingerprinter(DATACENTER_KEY, NSPACE_KEY, PARTITION_KEY);

  respondForQuery(respond, query) {
    return respond((headers, body) =>
      attachHeaders(
        headers,
        map(
          body,
          this.fingerprint(
            this.primaryKey,
            this.slugKey,
            query.dc,
            headers[HTTP_HEADERS_NAMESPACE],
            headers[HTTP_HEADERS_PARTITION]
          )
        ),
        query
      )
    );
  }

  respondForQueryRecord(respond, query) {
    return respond((headers, body) =>
      attachHeaders(
        headers,
        this.fingerprint(
          this.primaryKey,
          this.slugKey,
          query.dc,
          headers[HTTP_HEADERS_NAMESPACE],
          headers[HTTP_HEADERS_PARTITION]
        )(body),
        query
      )
    );
  }

  respondForCreateRecord(respond, serialized, data) {
    const slugKey = this.slugKey;
    const primaryKey = this.primaryKey;

    return respond((headers, body) => {
      // If creates are true use the info we already have
      if (body === true) {
        body = data;
      }
      // Creates need a primaryKey adding
      return this.fingerprint(
        primaryKey,
        slugKey,
        data[DATACENTER_KEY],
        headers[HTTP_HEADERS_NAMESPACE],
        data.Partition
      )(body);
    });
  }

  respondForUpdateRecord(respond, serialized, data) {
    const slugKey = this.slugKey;
    const primaryKey = this.primaryKey;

    return respond((headers, body) => {
      // If updates are true use the info we already have
      // TODO: We may aswell avoid re-fingerprinting here if we are just going
      // to reuse data then its already fingerprinted and as the response is
      // true we don't have anything changed so the old fingerprint stays the
      // same as long as nothing in the fingerprint has been edited (the
      // namespace?)
      if (body === true) {
        body = data;
      }
      return this.fingerprint(
        primaryKey,
        slugKey,
        data[DATACENTER_KEY],
        headers[HTTP_HEADERS_NAMESPACE],
        headers[HTTP_HEADERS_PARTITION]
      )(body);
    });
  }

  respondForDeleteRecord(respond, serialized, data) {
    const slugKey = this.slugKey;
    const primaryKey = this.primaryKey;

    return respond((headers, body) => {
      // Deletes only need the primaryKey/uid returning and they need the slug
      // key AND potential namespace in order to create the correct
      // uid/fingerprint
      return {
        [primaryKey]: this.fingerprint(
          primaryKey,
          slugKey,
          data[DATACENTER_KEY],
          headers[HTTP_HEADERS_NAMESPACE],
          headers[HTTP_HEADERS_PARTITION]
        )({
          [slugKey]: data[slugKey],
          [NSPACE_KEY]: data[NSPACE_KEY],
          [PARTITION_KEY]: data[PARTITION_KEY],
        })[primaryKey],
      };
    });
  }

  // this could get confusing if you tried to override say
  // `normalizeQueryResponse`
  // TODO: consider creating a method for each one of the
  // `normalize...Response` family
  normalizeResponse(store, modelClass, payload, id, requestType) {
    const normalizedPayload = this.normalizePayload(payload, id, requestType);
    // put the meta onto the response, here this is ok as JSON-API allows this
    // and our specific data is now in response[primaryModelClass.modelName]
    // so we aren't in danger of overwriting anything (which was the reason
    // for the Symbol-like property earlier) use a method modelled on
    // ember-data methods so we have the opportunity to do this on a per-model
    // level
    const meta = this.normalizeMeta(store, modelClass, normalizedPayload, id, requestType);
    // get distinct consul versions from list and add it as meta
    if (modelClass.modelName === 'node' && requestType === 'query') {
      meta.versions = this.getDistinctConsulVersions(normalizedPayload);
    }
    if (requestType !== 'query') {
      normalizedPayload.meta = meta;
    }
    const res = super.normalizeResponse(
      store,
      modelClass,
      {
        meta: meta,
        [modelClass.modelName]: normalizedPayload,
      },
      id,
      requestType
    );
    // If the result of the super normalizeResponse is undefined its because
    // the JSONSerializer (which REST inherits from) doesn't recognise the
    // requestType, in this case its likely to be an 'action' request rather
    // than a specific 'load me some data' one. Therefore its ok to bypass the
    // store here for the moment we currently use this for self, but it also
    // would affect any custom methods that use a serializer in our custom
    // service/store
    if (typeof res === 'undefined') {
      return payload;
    }
    return res;
  }

  timestamp() {
    return new Date().getTime();
  }

  normalizeMeta(store, modelClass, payload, id, requestType) {
    // Pick the meta/headers back off the payload and cleanup
    const headers = payload[HTTP_HEADERS_SYMBOL] || {};
    delete payload[HTTP_HEADERS_SYMBOL];
    const meta = {
      cacheControl: headers[HTTP_HEADERS_CACHE_CONTROL.toLowerCase()],
      cursor: headers[HTTP_HEADERS_INDEX.toLowerCase()],
      dc: headers[HTTP_HEADERS_DATACENTER.toLowerCase()],
      nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()],
      partition: headers[HTTP_HEADERS_PARTITION.toLowerCase()],
    };
    if (typeof headers['x-range'] !== 'undefined') {
      meta.range = headers['x-range'];
    }
    if (typeof headers['refresh'] !== 'undefined') {
      meta.interval = headers['refresh'] * 1000;
    }
    if (requestType === 'query') {
      meta.date = this.timestamp();
      payload.forEach(function (item) {
        set(item, 'SyncTime', meta.date);
      });
    }
    return meta;
  }

  normalizePayload(payload, id, requestType) {
    return payload;
  }

  // getDistinctConsulVersions will be called only for nodes and query request type
  // the list of versions is to be added as meta to resp, without changing original response structure
  // hence this function is added in application.js
  getDistinctConsulVersions(payload) {
    // create a Set and add version with only major.minor : ex-1.24.6 as 1.24
    let versionSet = new Set();
    payload.forEach(function (item) {
      if (item.Meta && item.Meta['consul-version']) {
        const split = item.Meta['consul-version'].split('.');
        versionSet.add(split[0] + '.' + split[1]);
      }
    });

    const versionArray = Array.from(versionSet);

    // Sort the array in descending order using a custom comparison function
    versionArray.sort((a, b) => {
      // Split the versions into arrays of numbers
      const versionA = a.split('.').map((part) => {
        const number = Number(part);
        return isNaN(number) ? 0 : number;
      });
      const versionB = b.split('.').map((part) => {
        const number = Number(part);
        return isNaN(number) ? 0 : number;
      });

      const minLength = Math.min(versionA.length, versionB.length);

      // start with comparing major version num, if equal then compare minor
      for (let i = 0; i < minLength; i++) {
        if (versionA[i] !== versionB[i]) {
          return versionB[i] - versionA[i];
        }
      }
      return versionB.length - versionA.length;
    });

    return versionArray; //sorted array
  }
}
