'use strict';

const async = require('async');
const nconf = require('nconf');
const crypto = require('crypto');
const path = require('path');
const winston = require('winston');
const mime = require('mime');
const validator = require('validator');

const db = require('../database');
const image = require('../image');
const topics = require('../topics');
const file = require('../file');

module.exports = function (Posts) {
	Posts.uploads = {};

	const md5 = filename => crypto.createHash('md5').update(filename).digest('hex');
	const pathPrefix = path.join(nconf.get('upload_path'), 'files');
	const searchRegex = /\/assets\/uploads\/files\/([^\s")]+\.?[\w]*)/g;

	Posts.uploads.sync = async function (pid) {
		// Scans a post's content and updates sorted set of uploads

		const [content, currentUploads, isMainPost] = await Promise.all([
			Posts.getPostField(pid, 'content'),
			Posts.uploads.list(pid),
			Posts.isMain(pid),
		]);

		// Extract upload file paths from post content
		let match = searchRegex.exec(content);
		const uploads = [];
		while (match) {
			uploads.push(match[1].replace('-resized', ''));
			match = searchRegex.exec(content);
		}

		// Main posts can contain topic thumbs, which are also tracked by pid
		if (isMainPost) {
			const tid = await Posts.getPostField(pid, 'tid');
			let thumbs = await topics.thumbs.get(tid);
			thumbs = thumbs.map(thumb => thumb.url.replace(path.join(nconf.get('relative_path'), nconf.get('upload_url'), 'files/'), '')).filter(path => !validator.isURL(path, {
				require_protocol: true,
			}));
			uploads.push(...thumbs);
		}

		// Create add/remove sets
		const add = uploads.filter(path => !currentUploads.includes(path));
		const remove = currentUploads.filter(path => !uploads.includes(path));
		await Promise.all([
			Posts.uploads.associate(pid, add),
			Posts.uploads.dissociate(pid, remove),
		]);
	};

	Posts.uploads.list = async function (pid) {
		return await db.getSortedSetMembers(`post:${pid}:uploads`);
	};

	Posts.uploads.listWithSizes = async function (pid) {
		const paths = await Posts.uploads.list(pid);
		const sizes = await db.getObjects(paths.map(path => `upload:${md5(path)}`)) || [];

		return sizes.map((sizeObj, idx) => ({
			...sizeObj,
			name: paths[idx],
		}));
	};

	Posts.uploads.isOrphan = async function (filePath) {
		const length = await db.sortedSetCard(`upload:${md5(filePath)}:pids`);
		return length === 0;
	};

	Posts.uploads.getUsage = async function (filePaths) {
		// Given an array of file names, determines which pids they are used in
		if (!Array.isArray(filePaths)) {
			filePaths = [filePaths];
		}

		const keys = filePaths.map(fileObj => `upload:${md5(fileObj.name.replace('-resized', ''))}:pids`);
		return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1)));
	};

	Posts.uploads.associate = async function (pid, filePaths) {
		// Adds an upload to a post's sorted set of uploads
		filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths;
		if (!filePaths.length) {
			return;
		}
		// Only process files that exist
		filePaths = await async.filter(filePaths, async filePath => await file.exists(path.join(pathPrefix, filePath)));

		const now = Date.now();
		const scores = filePaths.map(() => now);
		const bulkAdd = filePaths.map(path => [`upload:${md5(path)}:pids`, now, pid]);
		await Promise.all([
			db.sortedSetAdd(`post:${pid}:uploads`, scores, filePaths),
			db.sortedSetAddBulk(bulkAdd),
			Posts.uploads.saveSize(filePaths),
		]);
	};

	Posts.uploads.dissociate = async function (pid, filePaths) {
		// Removes an upload from a post's sorted set of uploads
		filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths;
		if (!filePaths.length) {
			return;
		}

		const bulkRemove = filePaths.map(path => [`upload:${md5(path)}:pids`, pid]);
		await Promise.all([
			db.sortedSetRemove(`post:${pid}:uploads`, filePaths),
			db.sortedSetRemoveBulk(bulkRemove),
		]);
	};

	Posts.uploads.dissociateAll = async (pid) => {
		const current = await Posts.uploads.list(pid);
		await Promise.all(current.map(async path => await Posts.uploads.dissociate(pid, path)));
	};

	Posts.uploads.saveSize = async (filePaths) => {
		filePaths = filePaths.filter((fileName) => {
			const type = mime.getType(fileName);
			return type && type.match(/image./);
		});
		await Promise.all(filePaths.map(async (fileName) => {
			try {
				const size = await image.size(path.join(pathPrefix, fileName));
				winston.verbose(`[posts/uploads/${fileName}] Saving size`);
				await db.setObject(`upload:${md5(fileName)}`, {
					width: size.width,
					height: size.height,
				});
			} catch (err) {
				winston.error(`[posts/uploads] Error while saving post upload sizes (${fileName}): ${err.message}`);
			}
		}));
	};
};
