'use strict';

module.exports = function (module) {
	const helpers = require('./helpers');

	module.setObject = async function (key, data) {
		if (!key || !data) {
			return;
		}

		if (data.hasOwnProperty('')) {
			delete data[''];
		}

		await module.transaction(async (client) => {
			const dataString = JSON.stringify(data);
			async function setOne(key) {
				await helpers.ensureLegacyObjectType(client, key, 'hash');
				await client.query({
					name: 'setObject',
					text: `
	INSERT INTO "legacy_hash" ("_key", "data")
	VALUES ($1::TEXT, $2::TEXT::JSONB)
	ON CONFLICT ("_key")
	DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`,
					values: [key, dataString],
				});
			}
			if (Array.isArray(key)) {
				await Promise.all(key.map(k => setOne(k)));
			} else {
				await setOne(key);
			}
		});
	};

	module.setObjectBulk = async function (keys, data) {
		if (!keys.length || !data.length) {
			return;
		}
		// TODO: single query?
		await Promise.all(keys.map((k, i) => module.setObject(k, data[i])));
	};

	module.setObjectField = async function (key, field, value) {
		if (!field) {
			return;
		}

		await module.transaction(async (client) => {
			const valueString = JSON.stringify(value);
			async function setOne(key) {
				await helpers.ensureLegacyObjectType(client, key, 'hash');
				await client.query({
					name: 'setObjectField',
					text: `
	INSERT INTO "legacy_hash" ("_key", "data")
	VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::TEXT::JSONB))
	ON CONFLICT ("_key")
	DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], $3::TEXT::JSONB)`,
					values: [key, field, valueString],
				});
			}

			if (Array.isArray(key)) {
				await Promise.all(key.map(k => setOne(k)));
			} else {
				await setOne(key);
			}
		});
	};

	module.getObject = async function (key, fields = []) {
		if (!key) {
			return null;
		}
		if (fields.length) {
			return await module.getObjectFields(key, fields);
		}
		const res = await module.pool.query({
			name: 'getObject',
			text: `
SELECT h."data"
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
			values: [key],
		});

		return res.rows.length ? res.rows[0].data : null;
	};

	module.getObjects = async function (keys, fields = []) {
		if (!Array.isArray(keys) || !keys.length) {
			return [];
		}
		if (fields.length) {
			return await module.getObjectsFields(keys, fields);
		}
		const res = await module.pool.query({
			name: 'getObjects',
			text: `
SELECT h."data"
  FROM UNNEST($1::TEXT[]) WITH ORDINALITY k("_key", i)
  LEFT OUTER JOIN "legacy_object_live" o
               ON o."_key" = k."_key"
  LEFT OUTER JOIN "legacy_hash" h
               ON o."_key" = h."_key"
              AND o."type" = h."type"
 ORDER BY k.i ASC`,
			values: [keys],
		});

		return res.rows.map(row => row.data);
	};

	module.getObjectField = async function (key, field) {
		if (!key) {
			return null;
		}

		const res = await module.pool.query({
			name: 'getObjectField',
			text: `
SELECT h."data"->>$2::TEXT f
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
			values: [key, field],
		});

		return res.rows.length ? res.rows[0].f : null;
	};

	module.getObjectFields = async function (key, fields) {
		if (!key) {
			return null;
		}
		if (!Array.isArray(fields) || !fields.length) {
			return await module.getObject(key);
		}
		const res = await module.pool.query({
			name: 'getObjectFields',
			text: `
SELECT (SELECT jsonb_object_agg(f, d."value")
          FROM UNNEST($2::TEXT[]) f
          LEFT OUTER JOIN jsonb_each(h."data") d
                       ON d."key" = f) d
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT`,
			values: [key, fields],
		});

		if (res.rows.length) {
			return res.rows[0].d;
		}

		const obj = {};
		fields.forEach((f) => {
			obj[f] = null;
		});

		return obj;
	};

	module.getObjectsFields = async function (keys, fields) {
		if (!Array.isArray(keys) || !keys.length) {
			return [];
		}

		if (!Array.isArray(fields) || !fields.length) {
			return await module.getObjects(keys);
		}
		const res = await module.pool.query({
			name: 'getObjectsFields',
			text: `
SELECT (SELECT jsonb_object_agg(f, d."value")
          FROM UNNEST($2::TEXT[]) f
          LEFT OUTER JOIN jsonb_each(h."data") d
                       ON d."key" = f) d
  FROM UNNEST($1::text[]) WITH ORDINALITY k("_key", i)
  LEFT OUTER JOIN "legacy_object_live" o
               ON o."_key" = k."_key"
  LEFT OUTER JOIN "legacy_hash" h
               ON o."_key" = h."_key"
              AND o."type" = h."type"
 ORDER BY k.i ASC`,
			values: [keys, fields],
		});

		return res.rows.map(row => row.d);
	};

	module.getObjectKeys = async function (key) {
		if (!key) {
			return;
		}

		const res = await module.pool.query({
			name: 'getObjectKeys',
			text: `
SELECT ARRAY(SELECT jsonb_object_keys(h."data")) k
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
			values: [key],
		});

		return res.rows.length ? res.rows[0].k : [];
	};

	module.getObjectValues = async function (key) {
		const data = await module.getObject(key);
		return data ? Object.values(data) : [];
	};

	module.isObjectField = async function (key, field) {
		if (!key) {
			return;
		}

		const res = await module.pool.query({
			name: 'isObjectField',
			text: `
SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
			values: [key, field],
		});

		return res.rows.length ? res.rows[0].b : false;
	};

	module.isObjectFields = async function (key, fields) {
		if (!key) {
			return;
		}

		const data = await module.getObjectFields(key, fields);
		if (!data) {
			return fields.map(() => false);
		}
		return fields.map(field => data.hasOwnProperty(field) && data[field] !== null);
	};

	module.deleteObjectField = async function (key, field) {
		await module.deleteObjectFields(key, [field]);
	};

	module.deleteObjectFields = async function (key, fields) {
		if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) {
			return;
		}
		async function delKey(key, fields) {
			await module.pool.query({
				name: 'deleteObjectFields',
				text: `
	UPDATE "legacy_hash"
	   SET "data" = COALESCE((SELECT jsonb_object_agg("key", "value")
								FROM jsonb_each("data")
							   WHERE "key" <> ALL ($2::TEXT[])), '{}')
	 WHERE "_key" = $1::TEXT`,
				values: [key, fields],
			});
		}
		if (Array.isArray(key)) {
			await Promise.all(key.map(k => delKey(k, fields)));
		} else {
			await delKey(key, fields);
		}
	};

	module.incrObjectField = async function (key, field) {
		return await module.incrObjectFieldBy(key, field, 1);
	};

	module.decrObjectField = async function (key, field) {
		return await module.incrObjectFieldBy(key, field, -1);
	};

	module.incrObjectFieldBy = async function (key, field, value) {
		value = parseInt(value, 10);

		if (!key || isNaN(value)) {
			return null;
		}

		return await module.transaction(async (client) => {
			if (Array.isArray(key)) {
				await helpers.ensureLegacyObjectsType(client, key, 'hash');
			} else {
				await helpers.ensureLegacyObjectType(client, key, 'hash');
			}

			const res = await client.query(Array.isArray(key) ? {
				name: 'incrObjectFieldByMulti',
				text: `
INSERT INTO "legacy_hash" ("_key", "data")
SELECT UNNEST($1::TEXT[]), jsonb_build_object($2::TEXT, $3::NUMERIC)
ON CONFLICT ("_key")
DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC))
RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
				values: [key, field, value],
			} : {
				name: 'incrObjectFieldBy',
				text: `
INSERT INTO "legacy_hash" ("_key", "data")
VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::NUMERIC))
ON CONFLICT ("_key")
DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC))
RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
				values: [key, field, value],
			});
			return Array.isArray(key) ? res.rows.map(r => parseFloat(r.v)) : parseFloat(res.rows[0].v);
		});
	};
};
