/**
 * Wrapper to facilitate posting / interacting with Smogon.
 * By Mia.
 * @author mia-pi-git
 */
import { Net, FS, Utils } from '../../lib';

export interface Nomination {
	by: ID;
	ips: string[];
	info: string;
	date: number;
	standing: string;
	alts: string[];
	primaryID: ID;
	claimed?: ID;
	post?: string;
}

interface IPData {
	country: string;
	isp: string;
	city: string;
	regionName: string;
	lat: number;
	lon: number;
}

export function getIPData(ip: string) {
	try {
		return Net("https://miapi.dev/api/ip/" + ip).get().then(JSON.parse) as Promise<IPData>;
	} catch {
		return null;
	}
}

export const Smogon = new class {
	async post(threadNum: string, postText: string) {
		if (!Config.smogon) return null;
		try {
			const raw = await Net(`https://www.smogon.com/forums/api/posts`).get({
				method: 'POST',
				body: new URLSearchParams({
					thread_id: threadNum,
					message: postText,
				}).toString(),
				headers: {
					'XF-Api-Key': Config.smogon,
					'Content-Type': 'application/x-www-form-urlencoded',
				},
			});
			// todo return URL of post
			const data = JSON.parse(raw);
			if (data.errors?.length) {
				const errData = data.errors.pop();
				throw new Error(errData.message);
			}
			return data;
		} catch (e: any) {
			if (e.message.includes('Not Found')) {
				// special case to be loud
				throw new Error("WHO DELETED THE PERMA THREAD");
			}
			return { error: e.message };
		}
	}
};

export const Nominations = new class {
	noms: Nomination[] = [];
	icons: Record<string, string> = {};
	constructor() {
		this.load();
	}
	load() {
		try {
			let data = JSON.parse(FS('config/chat-plugins/permas.json').readSync());
			if (Array.isArray(data)) {
				data = { noms: data, icons: {} };
				FS('config/chat-plugins/permas.json').writeSync(JSON.stringify(data));
			}
			this.noms = data.noms;
			this.icons = data.icons;
		} catch {}
	}
	fetchModlog(id: string) {
		return Rooms.Modlog.search('global', {
			user: [{ search: id, isExact: true }],
			note: [],
			ip: [],
			action: [],
			actionTaker: [],
		}, undefined, true);
	}
	save() {
		FS('config/chat-plugins/permas.json').writeUpdate(() => JSON.stringify({ noms: this.noms, icons: this.icons }));
	}
	notifyStaff() {
		const usRoom = Rooms.get('upperstaff');
		if (!usRoom) return;
		usRoom.send(`|uhtml|permanoms|${this.getDisplayButton()}`);
		Chat.refreshPageFor('permalocks', usRoom);
	}
	async add(target: string, connection: Connection) {
		const user = connection.user;
		const [primary, rawAlts, rawIps, type, details] = Utils.splitFirst(target, '|', 4).map(f => f.trim());
		const primaryID = toID(primary);
		const alts = rawAlts.split(',').map(toID).filter(Boolean);
		const ips = rawIps.split(',').map(f => f.trim()).filter(Boolean);
		for (const ip of ips) {
			if (!IPTools.ipRegex.test(ip)) this.error(`Invalid IP: ${ip}`, connection);
		}
		const standings = this.getStandings();
		if (!standings[type]) {
			this.error(`Invalid standing: ${type}.`, connection);
		}
		if (!details) {
			this.error("Details must be provided. Explain why this user should be permalocked.", connection);
		}
		if (!primaryID) {
			this.error("A primary username must be provided. Use one of their alts if necessary.", connection);
		}
		for (const nom of this.noms) {
			if (nom.primaryID === primaryID) {
				this.error(`'${primaryID}' was already nominated for permalock by ${nom.by}.`, connection);
			}
		}
		const ipTable = new Set<string>(ips);
		const altTable = new Set<string>([...alts]);
		for (const alt of [primaryID, ...alts]) {
			const modlog = await this.fetchModlog(alt);
			if (!modlog?.results.length) continue;
			for (const entry of modlog.results) {
				if (entry.ip) ipTable.add(entry.ip);
				if (entry.autoconfirmedID) altTable.add(entry.autoconfirmedID);
				if (entry.alts) {
					for (const id of entry.alts) altTable.add(id);
				}
			}
		}
		altTable.delete(primaryID);
		this.noms.push({
			by: user.id,
			alts: [...altTable],
			ips: Utils.sortBy([...ipTable], z => -(IPTools.ipToNumber(z) || Infinity)),
			info: details,
			primaryID,
			standing: type,
			date: Date.now(),
		});
		Utils.sortBy(this.noms, nom => -nom.date);
		this.save();
		this.notifyStaff();
		Rooms.get('staff')?.addByUser(user, `${user.name} submitted a perma nomination for ${primaryID}`);
	}
	find(id: string) {
		return this.noms.find(f => f.primaryID === id);
	}
	error(message: string, conn: Connection): never {
		conn.popup(message);
		throw new Chat.Interruption();
	}
	close(target: string, context: Chat.CommandContext) {
		const entry = this.find(target);
		if (!entry) {
			this.error(`There is no nomination pending for '${toID(target)}'.`, context.connection);
		}
		this.noms.splice(this.noms.findIndex(f => f.primaryID === entry.primaryID), 1);
		this.save();
		this.notifyStaff();
		// todo fix when on good comp
		return context.closePage(`permalocks-view-${entry.primaryID}`);
	}
	display(nom: Nomination, canEdit?: boolean) {
		let buf = `<div class="infobox">`;
		let title = nom.primaryID as string;
		if (canEdit) {
			title = `<a href="/view-permalocks-view-${nom.primaryID}" target="_replace">${nom.primaryID}</a>`;
		}
		buf += `<strong>${title}</strong> (submitted by ${nom.by})<br />`;
		buf += `Submitted ${Chat.toTimestamp(new Date(nom.date), { human: true })}<br />`;
		buf += `${Chat.count(nom.alts, 'alts')}, ${Chat.count(nom.ips, 'IPs')}`;
		buf += `</div>`;
		return buf;
	}
	displayModlog(results: import('../modlog').ModlogEntry[] | null) {
		if (!results) return '';
		let curDate = '';
		return results.map(result => {
			const date = new Date(result.time || Date.now());
			const entryRoom = result.visualRoomID || result.roomID || 'global';
			let [dateString, timestamp] = Chat.toTimestamp(date, { human: true }).split(' ');
			let line = `<small>[${timestamp}] (${entryRoom})</small> ${result.action}`;
			if (result.userid) {
				line += `: [${result.userid}]`;
				if (result.autoconfirmedID) line += ` ac: [${result.autoconfirmedID}]`;
				if (result.alts.length) line += ` alts: [${result.alts.join('], [')}]`;
				if (result.ip) line += ` [<a href="https://whatismyipaddress.com/ip/${result.ip}" target="_blank">${result.ip}</a>]`;
			}

			if (result.loggedBy) line += `: by ${result.loggedBy}`;
			if (result.note) line += Utils.html`: ${result.note}`;

			if (dateString !== curDate) {
				curDate = dateString;
				dateString = `</p><p>[${dateString}]<br />`;
			} else {
				dateString = ``;
			}
			const thisRoomID = entryRoom?.split(' ')[0];
			if (thisRoomID.startsWith('battle-')) {
				timestamp = `<a href="/${thisRoomID}">${timestamp}</a>`;
			} else {
				const [day, time] = Chat.toTimestamp(date).split(' ');
				timestamp = `<a href="/view-chatlog-${thisRoomID}--${day}--time-${toID(time)}">${timestamp}</a>`;
			}
			return `${dateString}${line}`;
		}).join(`<br />`);
	}
	async displayActionPage(nom: Nomination) {
		let buf = `<div class="pad">`;
		const standings = this.getStandings();
		buf += `<button class="button" name="send" value="/perma viewnom ${nom.primaryID}" style="float:right">`;
		buf += `<i class="fa fa-refresh"></i> Refresh</button>`;
		buf += `<h3>Nomination: ${nom.primaryID}</h3><hr />`;
		buf += `<strong>By:</strong> ${nom.by} (on ${Chat.toTimestamp(new Date(nom.date))})<br />`;
		buf += `<strong>Recommended punishment:</strong> ${standings[nom.standing]}<br />`;
		buf += `<details class="readmore"><summary><strong>Modlog</strong></summary>`;
		buf += `<div class="infobox limited">`;
		const modlog = await this.fetchModlog(nom.primaryID);
		if (!modlog) {
			buf += `None found.`;
		} else {
			buf += this.displayModlog(modlog.results);
		}
		buf += `</div></details>`;
		if (nom.alts.length) {
			buf += `<details class="readmore"><summary><strong>Listed alts</strong></summary>`;
			for (const [i, alt] of nom.alts.entries()) {
				buf += `- ${alt}: `;
				buf += `<form data-submitsend="/perma standing ${alt},{standing},{reason}">`;
				buf += this.standingDropdown("standing");
				buf += ` <button class="button notifying" type="submit">Change standing</button>`;
				buf += ` <input name="reason" placeholder="Reason" />`;
				buf += `</form>`;
				if (nom.alts[i + 1]) buf += `<br />`;
			}
			buf += `</details>`;
		}
		if (nom.ips.length) {
			buf += `<details class="readmore"><summary><strong>Listed IPs</strong></summary>`;
			for (const [i, ip] of nom.ips.entries()) {
				const ipData = await getIPData(ip);
				buf += `- <a href="https://whatismyipaddress.com/ip/${ip}">${ip}</a>`;
				if (ipData) {
					buf += `(ISP: ${ipData.isp}, loc: ${ipData.city}, ${ipData.regionName} in ${ipData.country})`;
				}
				buf += `: `;
				buf += `<form data-submitsend="/perma ipstanding ${ip},{standing},{reason}">`;
				buf += this.standingDropdown("standing");
				buf += ` <button class="button notifying" type="submit">Change standing for all users on IP</button>`;
				buf += ` <input name="reason" placeholder="Reason" />`;
				buf += `</form>`;
				if (nom.ips[i + 1]) buf += `<br />`;
			}
			buf += `</details>`;
		}
		const [matches] = await LoginServer.request('ipmatches', {
			id: nom.primaryID,
		});
		if (matches?.results?.length) {
			buf += `<details class="readmore"><summary><strong>Registration IP matches</strong></summary>`;
			for (const [i, { userid, banstate }] of matches.results.entries()) {
				buf += `- ${userid}: `;
				buf += `<form data-submitsend="/perma standing ${userid},{standing}">`;
				buf += this.standingDropdown("standing", `${banstate}`);
				buf += ` <button class="button notifying" type="submit">Change standing</button></form>`;
				if (matches.results[i + 1]) buf += `<br />`;
			}
			buf += `</details>`;
		}
		buf += `<p><strong>Staff notes:</strong></p>`;
		buf += `<p><div class="infobox">${Chat.formatText(nom.info).replace(/\n/ig, '<br />')}</div></p>`;
		buf += `<details class="readmore"><summary><strong>Act on primary:</strong></summary>`;
		buf += `<form data-submitsend="/perma actmain ${nom.primaryID},{standing},{note}">`;
		buf += `Standing: ${this.standingDropdown('standing')}`;
		buf += `<br />Notes:<br />`;
		buf += `<textarea name="note" style="width: 100%" cols="50" rows="10"></textarea><br />`;
		buf += `<button class="button notifying" type="submit">Change standing and make post</button>`;
		buf += `</form></details><br />`;
		buf += `<button class="button notifying" name="send" value="/perma resolve ${nom.primaryID}">Mark resolved</button>`;
		return buf;
	}
	standingDropdown(elemName: string, curStanding: string | null = null) {
		let buf = `<select name="${elemName}">`;
		const standings = this.getStandings();
		for (const k in standings) {
			buf += `<option ${curStanding === k ? "disabled" : ""} value="${k}">${standings[k]}</option>`;
		}
		buf += `</select>`;
		return buf;
	}
	getStandings() {
		if (Config.standings) return Config.standings;
		Config.standings = {
			'-20': "Confirmed",
			'-10': "Autoconfirmed",
			'0': "New",
			"20": "Permalock",
			"30": "Permaban",
			"100": "Disabled",
		};
		return Config.standings;
	}
	displayAll(canEdit: boolean) {
		let buf = `<div class="pad">`;
		buf += `<button class="button" name="send" value="/perma noms" style="float:right"><i class="fa fa-refresh"></i> Refresh</button>`;
		buf += `<h3>Pending perma nominations</h3><hr />`;
		if (!this.noms.length) {
			buf += `None found.`;
			return buf;
		}
		for (const nom of this.noms) {
			buf += this.display(nom, canEdit);
			buf += `<br />`;
		}
		return buf;
	}
	displayNomPage() {
		let buf = `<div class="pad"><h3>Make a nomination for a permanent punishment.</h3><hr />`;
		// const [primary, rawAlts, rawIps, details] = Utils.splitFirst(target, '|', 3).map(f => f.trim());
		buf += `<form data-submitsend="/perma submit {primary}|{alts}|{ips}|{type}|{details}">`;
		buf += `<div class="infobox">`;
		buf += `<strong>Primary userid:</strong> <input name="primary" /><br />`;
		buf += `<strong>Alts:</strong><br /><textarea name="alts"></textarea><br /><small>(Separated by commas)</small><br />`;
		buf += `<strong>Static IPs:</strong><br /><textarea name="ips"></textarea><br /><small>(Separated by commas)</small></div><br />`;
		buf += `<strong>Punishment:</strong> `;
		buf += `<select name="type"><option value="20">Permalock</option><option value="30">Permaban</option></select>`;
		buf += `<div class="infobox">`;
		buf += `<strong>Please explain why this user deserves a permanent punishment</strong><br />`;
		buf += `<small>Note: Modlogs are automatically included in review and do not need to be added here.</small><br />`;
		buf += `<textarea style="width: 100%" name="details" cols="50" rows="10"></textarea></div>`;
		buf += `<button class="button notifying" type="submit">Submit nomination</button>`;
		return buf;
	}
	getDisplayButton() {
		const unclaimed = this.noms.filter(f => !f.claimed);
		let buf = `<div class="infobox">`;
		if (!this.noms.length) {
			buf += `No permalock nominations active.`;
		} else {
			let className = 'button';
			if (unclaimed.length) className += ' notifying';
			buf += `<button class="${className}" name="send" value="/j view-permalocks-list">`;
			buf += `${Chat.count(this.noms.length, 'nominations')}`;
			if (unclaimed.length !== this.noms.length) {
				buf += ` (${unclaimed.length} unclaimed)`;
			}
			buf += `</button>`;
		}
		buf += `</div>`;
		return buf;
	}
};

export const commands: Chat.ChatCommands = {
	perma: {
		''(target, room, user) {
			this.checkCan('lock');
			if (!user.can('rangeban')) {
				return this.parse(`/j view-permalocks-submit`);
			} else {
				return this.parse(`/j view-permalocks-list`);
			}
		},
		viewnom(target) {
			this.checkCan('rangeban');
			return this.parse(`/j view-permalocks-view-${toID(target)}`);
		},
		submit(target, room, user) {
			this.checkCan('lock');
			return Nominations.add(target, this.connection);
		},
		list() {
			this.checkCan('lock');
			return this.parse(`/j view-permalocks-list`);
		},
		nom() {
			this.checkCan('lock');
			return this.parse(`/j view-permalocks-submit`);
		},
		async actmain(target, room, user) {
			this.checkCan('rangeban');
			const [primaryName, standingName, postReason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
			const primary = toID(primaryName);
			if (!primary) return this.popupReply(`Invalid primary username.`);
			const nom = Nominations.find(primary);
			if (!nom) return this.popupReply(`No permalock nomination found for ${primary}.`);
			const standing = parseInt(standingName);
			const standings = Nominations.getStandings();
			if (!standings[standing]) return this.popupReply(`Invalid standing.`);
			if (!toID(postReason)) return this.popupReply(`A reason must be given.`);
			// todo thread num
			const threadNum = Config.permathread;
			if (!threadNum) {
				throw new Chat.ErrorMessage("The link to the perma has not been set - the post could not be made.");
			}
			let postBuf = `[b][url="https://${Config.routes.root}/users/${primary}"]${primary}[/url][/b]`;
			const icon = Nominations.icons[user.id] ? `:${Nominations.icons[user.id]}: - ` : ``;
			postBuf += ` was added to ${standings[standing]} by ${user.name} (${icon}${postReason}).\n`;
			postBuf += `Nominated by ${nom.by}.\n[spoiler=Nomination notes]${nom.info}[/spoiler]\n`;
			postBuf += `${nom.alts.length ? `[spoiler=Alts]${nom.alts.join(', ')}[/spoiler]` : ""}\n`;
			if (nom.ips.length) {
				postBuf += `[spoiler=IPs]`;
				for (const ip of nom.ips) {
					const ipData = await getIPData(ip);
					postBuf += `- [url=https://whatismyipaddress.com/ip/${ip}]${ip}[/url]`;
					if (ipData) {
						postBuf += ` (ISP: ${ipData.isp}, loc: ${ipData.city}, ${ipData.regionName} in ${ipData.country})`;
					}
					postBuf += '\n';
				}
				postBuf += `[/spoiler]`;
			}

			const modlog = await Nominations.fetchModlog(nom.primaryID);
			if (modlog?.results.length) {
				let rawHTML = Nominations.displayModlog(modlog.results);
				rawHTML = rawHTML.replace(/<br \/>/g, '\n');
				rawHTML = Utils.stripHTML(rawHTML);
				rawHTML = rawHTML.replace(/&#x2f;/g, '/');
				postBuf += `\n[spoiler=Modlog]${rawHTML}[/spoiler]`;
			}

			const res = await Smogon.post(
				threadNum,
				postBuf,
			);
			if (!res || res.error) {
				return this.popupReply(`Error making post: ${res?.error}`);
			}
			const url = `https://smogon.com/forums/threads/${threadNum}/post-${res.post.post_id}`;
			const result = await LoginServer.request('setstanding', {
				user: primary,
				standing,
				reason: url,
				actor: user.id,
			});
			if (result[1]) {
				return this.popupReply(`Error changing standing: ${result[1].message}`);
			}
			nom.post = url;
			this.popupReply(`|html|Standing successfully changed. Smogon post can be found <a href="${url}">at this link</a>.`);
		},
		async standing(target) {
			this.checkCan('rangeban');
			const [name, rawStanding, reason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
			const id = toID(name);
			if (!id || id.length > 18) {
				return this.popupReply('Invalid username: ' + name);
			}
			const standingNum = parseInt(rawStanding);
			if (!standingNum) {
				return this.popupReply(`Invalid standing: ` + rawStanding);
			}
			if (!reason.length) {
				return this.popupReply(`A reason must be given.`);
			}
			const res = await LoginServer.request('setstanding', {
				user: id,
				standing: standingNum,
				reason,
				actor: this.user.id,
			});
			if (res[1]) {
				return this.popupReply(`Error in standing change: ` + res[1].message);
			}
			this.popupReply(`Standing successfully changed to ${standingNum} for ${id}.`);
			// no need to modlog, is in usermodlog already
		},
		async ipstanding(target) {
			this.checkCan('rangeban');
			const [ip, standingName, reason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
			if (!IPTools.ipToNumber(ip)) {
				return this.popupReply(`Invalid IP: ${ip}`);
			}
			const standingNum = parseInt(standingName);
			if (!Config.standings[`${standingNum}`]) {
				return this.popupReply(`Invalid standing: ${standingName}.`);
			}
			if (!reason.length) {
				return this.popupReply('Specify a reason.');
			}
			const res = await LoginServer.request('ipstanding', {
				reason,
				standing: standingNum,
				ip,
				actor: this.user.id,
			});
			if (res[1]) {
				return this.popupReply(`Error changing standing: ${res[1].message}`);
			}
			this.popupReply(`All standings on the IP ${ip} changed successfully to ${standingNum}.`);
			this.globalModlog(`IPSTANDING`, null, `${standingNum}${reason ? ` (${reason})` : ""}`, ip);
		},
		resolve(target) {
			this.checkCan('rangeban');
			Nominations.close(target, this);
		},
		seticon(target, room, user) {
			this.checkCan('rangeban');
			let [monName, targetId] = target.split(',');
			if (!targetId) targetId = user.id;
			const mon = Dex.species.get(monName);
			if (!mon.exists) {
				throw new Chat.ErrorMessage(`Species ${monName} does not exist.`);
			}
			Nominations.icons[targetId] = mon.name.toLowerCase();
			Nominations.save();
			this.sendReply(
				`|html|Updated ${targetId === user.id ? 'your' : `${targetId}'s`} permalock post icon to ` +
				`<psicon pokemon='${mon.name.toLowerCase()}' />`
			);
		},
		deleteicon(target, room, user) {
			this.checkCan('rangeban');
			const targetID = toID(target);
			if (!Nominations.icons[targetID]) {
				throw new Chat.ErrorMessage(`${targetID} does not have an icon set.`);
			}
			delete Nominations.icons[targetID];
			Nominations.save();
			this.sendReply(`Removed ${targetID}'s permalock post icon.`);
		},
		help: [
			'/perma nom OR /perma - Open the page to make a nomination for a permanent punishment. Requires: % @ ~',
			'/perma list - View open nominations. Requires: % @ ~',
			'/perma viewnom [userid] - View a nomination for the given [userid]. Requires: ~',
		],
	},
};

export const pages: Chat.PageTable = {
	permalocks: {
		list(query, user, conn) {
			this.checkCan('lock');
			this.title = '[Permalock Nominations]';
			return Nominations.displayAll(user.can('rangeban'));
		},
		view(query, user) {
			this.checkCan('rangeban');
			const id = toID(query.shift());
			if (!id) throw new Chat.ErrorMessage(`Invalid userid.`);
			const nom = Nominations.find(id);
			if (!nom) throw new Chat.ErrorMessage(`No nomination found for '${id}'.`);
			this.title = `[Perma Nom] ${nom.primaryID}`;
			return Nominations.displayActionPage(nom);
		},
		submit() {
			this.checkCan('lock');
			this.title = '[Perma Nom] Create';
			return Nominations.displayNomPage();
		},
	},
};

process.nextTick(() => {
	Chat.multiLinePattern.register('/perma(noms?)? ');
});
