const getTypeOf = (thing) => typeof thing;
const EMPTY_STRING = '';
const STRING = getTypeOf(EMPTY_STRING);
const T = 't';
const V = 'v';
const strStartsWith = (str, prefix) => str.startsWith(prefix);

const promise = Promise;
const getIfNotFunction =
  (predicate = isNullish) =>
  (value, then, otherwise) =>
    predicate(value) ? otherwise?.() : then(value);
const isInstanceOf = (thing, cls) => thing instanceof cls;
const isNullish = (thing) => thing == null;
const isUndefined = (thing) => thing === void 0;
const ifNotUndefined = getIfNotFunction(isUndefined);
const isString = (thing) => getTypeOf(thing) == STRING;
const slice = (arrayOrString, start, end) => arrayOrString.slice(start, end);
const size = (arrayOrString) => arrayOrString.length;
const promiseAll = async (promises) => promise.all(promises);

const arrayEvery = (array, cb) => array.every(cb);
const arrayMap = (array, cb) => array.map(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayPush = (array, ...values) => array.push(...values);
const arrayUnshift = (array, ...values) => array.unshift(...values);

const object = Object;
const objEntries = object.entries;
const objNew = (entries = []) => object.fromEntries(entries);
const objHas = (obj, id) => id in obj;
const objToArray = (obj, cb) =>
  arrayMap(objEntries(obj), ([id, value]) => cb(value, id));
const objEnsure = (obj, id, getDefaultValue) => {
  if (!objHas(obj, id)) {
    obj[id] = getDefaultValue();
  }
  return obj[id];
};

const jsonString = JSON.stringify;
const jsonParse = JSON.parse;
const jsonStringWithMap = (obj) =>
  jsonString(obj, (_key, value) =>
    isInstanceOf(value, Map) ? object.fromEntries([...value]) : value,
  );

const collForEach = (coll, cb) => coll?.forEach(cb);

const mapForEach = (map, cb) =>
  collForEach(map, (value, key) => cb(key, value));

const SET_CHANGES = 's';
const STORE_PATH = '/store';
const PUT = 'PUT';
const construct = (prefix, type, payload) =>
  prefix + type + (isString(payload) ? payload : jsonStringWithMap(payload));
const deconstruct = (prefix, message, stringified) => {
  const prefixSize = size(prefix);
  return strStartsWith(message, prefix)
    ? [
        message[prefixSize],
        (stringified ? jsonParse : String)(slice(message, prefixSize + 1)),
      ]
    : void 0;
};

const HAS_STORE = 'hasStore';
const RESPONSE_HEADERS = objNew(
  arrayMap(['Origin', 'Methods', 'Headers'], (suffix) => [
    'Access-Control-Allow-' + suffix,
    '*',
  ]),
);
const hasStoreInStorage = async (storage, storagePrefix = EMPTY_STRING) =>
  !!(await storage.get(storagePrefix + HAS_STORE));
const loadStoreFromStorage = async (storage, storagePrefix = EMPTY_STRING) => {
  const tables = {};
  const values = {};
  mapForEach(await storage.list(), (key, cellOrValue) =>
    ifNotUndefined(deconstruct(storagePrefix, key), ([type, ids]) => {
      if (type == T) {
        const [tableId, rowId, cellId] = jsonParse('[' + ids + ']');
        objEnsure(objEnsure(tables, tableId, objNew), rowId, objNew)[cellId] =
          cellOrValue;
      } else if (type == V) {
        values[ids] = cellOrValue;
      }
    }),
  );
  return [tables, values];
};
const broadcastChanges = async (server, changes, without) =>
  server.party.broadcast(
    construct(
      server.config.messagePrefix ?? EMPTY_STRING,
      SET_CHANGES,
      changes,
    ),
    without,
  );
const saveStore = async (that, changes, initialSave, requestOrConnection) => {
  const storage = that.party.storage;
  const storagePrefix = that.config.storagePrefix ?? EMPTY_STRING;
  const keysToSet = {
    [storagePrefix + HAS_STORE]: 1,
  };
  const keysToDel = [];
  const keyPrefixesToDel = [];
  await promiseAll(
    objToArray(changes[0], async (table, tableId) =>
      isUndefined(table)
        ? !initialSave &&
          (await that.canDelTable(tableId, requestOrConnection)) &&
          arrayUnshift(
            keyPrefixesToDel,
            constructStorageKey(storagePrefix, T, tableId),
          )
        : (await that.canSetTable(tableId, initialSave, requestOrConnection)) &&
          (await promiseAll(
            objToArray(table, async (row, rowId) =>
              isUndefined(row)
                ? !initialSave &&
                  (await that.canDelRow(tableId, rowId, requestOrConnection)) &&
                  arrayPush(
                    keyPrefixesToDel,
                    constructStorageKey(storagePrefix, T, tableId, rowId),
                  )
                : (await that.canSetRow(
                    tableId,
                    rowId,
                    initialSave,
                    requestOrConnection,
                  )) &&
                  (await promiseAll(
                    objToArray(row, async (cell, cellId) => {
                      const ids = [tableId, rowId, cellId];
                      const key = constructStorageKey(storagePrefix, T, ...ids);
                      if (isUndefined(cell)) {
                        if (
                          !initialSave &&
                          (await that.canDelCell(...ids, requestOrConnection))
                        ) {
                          arrayPush(keysToDel, key);
                        }
                      } else if (
                        await that.canSetCell(
                          ...ids,
                          cell,
                          initialSave,
                          requestOrConnection,
                          await storage.get(key),
                        )
                      ) {
                        keysToSet[key] = cell;
                      }
                    }),
                  )),
            ),
          )),
    ),
  );
  await promiseAll(
    objToArray(changes[1], async (value, valueId) => {
      const key = storagePrefix + V + valueId;
      if (isUndefined(value)) {
        if (
          !initialSave &&
          (await that.canDelValue(valueId, requestOrConnection))
        ) {
          arrayPush(keysToDel, key);
        }
      } else if (
        await that.canSetValue(
          valueId,
          value,
          initialSave,
          requestOrConnection,
          await storage.get(key),
        )
      ) {
        keysToSet[key] = value;
      }
    }),
  );
  if (!arrayIsEmpty(keyPrefixesToDel)) {
    mapForEach(await storage.list(), (key) =>
      arrayEvery(
        keyPrefixesToDel,
        (keyPrefixToDelete) =>
          !strStartsWith(key, keyPrefixToDelete) ||
          (arrayPush(keysToDel, key) && 0),
      ),
    );
  }
  await storage.delete(keysToDel);
  await storage.put(keysToSet);
};
const constructStorageKey = (storagePrefix, type, ...ids) =>
  construct(storagePrefix, type, slice(jsonStringWithMap(ids), 1, -1));
const createResponse = async (that, status, body = null) =>
  new Response(body, {
    status,
    headers: that.config.responseHeaders,
  });
class TinyBasePartyKitServer {
  constructor(party) {
    this.party = party;
    this.config.storePath ??= STORE_PATH;
    this.config.messagePrefix ??= EMPTY_STRING;
    this.config.storagePrefix ??= EMPTY_STRING;
    this.config.responseHeaders ??= RESPONSE_HEADERS;
  }
  config = {};
  async onRequest(request) {
    const {
      party: {storage},
      config: {storePath = STORE_PATH, storagePrefix},
    } = this;
    if (new URL(request.url).pathname.endsWith(storePath)) {
      const hasExistingStore = await hasStoreInStorage(storage, storagePrefix);
      const text = await request.text();
      if (request.method == PUT) {
        if (hasExistingStore) {
          return createResponse(this, 205);
        }
        await saveStore(this, jsonParse(text), true, request);
        return createResponse(this, 201);
      }
      return createResponse(
        this,
        200,
        hasExistingStore
          ? jsonStringWithMap(
              await loadStoreFromStorage(storage, storagePrefix),
            )
          : EMPTY_STRING,
      );
    }
    return createResponse(this, 404);
  }
  async onMessage(message, connection) {
    const {
      config: {messagePrefix = EMPTY_STRING, storagePrefix},
    } = this;
    await ifNotUndefined(
      deconstruct(messagePrefix, message, 1),
      async ([type, payload]) => {
        if (
          type == SET_CHANGES &&
          (await hasStoreInStorage(this.party.storage, storagePrefix))
        ) {
          await saveStore(this, payload, false, connection);
          broadcastChanges(this, payload, [connection.id]);
        }
      },
    );
  }
  async canSetTable(_tableId, _initialSave, _requestOrConnection) {
    return true;
  }
  async canDelTable(_tableId, _connection) {
    return true;
  }
  async canSetRow(_tableId, _rowId, _initialSave, _requestOrConnection) {
    return true;
  }
  async canDelRow(_tableId, _rowId, _connection) {
    return true;
  }
  async canSetCell(
    _tableId,
    _rowId,
    _cellId,
    _cell,
    _initialSave,
    _requestOrConnection,
    _oldCell,
  ) {
    return true;
  }
  async canDelCell(_tableId, _rowId, _cellId, _connection) {
    return true;
  }
  async canSetValue(
    _valueId,
    _value,
    _initialSave,
    _requestOrConnection,
    _oldValue,
  ) {
    return true;
  }
  async canDelValue(_valueId, _connection) {
    return true;
  }
}

export {
  TinyBasePartyKitServer,
  broadcastChanges,
  hasStoreInStorage,
  loadStoreFromStorage,
};
