/**
 * @author mia-pi-git
 */

import { FS, Net, Utils } from '../../lib';

export const SEASONS_PER_YEAR = 4;
export const FORMATS_PER_SEASON = 4;
export const BADGE_THRESHOLDS: Record<string, number> = {
	gold: 3,
	silver: 30,
	bronze: 100,
};
export const FIXED_FORMATS = ['randombattle', 'ou'];
export const FORMAT_POOL = ['ubers', 'uu', 'ru', 'nu', 'pu', 'lc', 'doublesou', 'monotype'];
export const PUBLIC_PHASE_LENGTH = 3;

interface SeasonData {
	current: { period: number, year: number, formatsGeneratedAt: number, season: number };
	badgeholders: { [period: string]: { [format: string]: { [badgeType: string]: string[] } } };
	formatSchedule: Record<string, string[]>;
}

export let data: SeasonData;

try {
	data = JSON.parse(FS('config/chat-plugins/seasons.json').readSync());
} catch {
	data = {
		// force a reroll
		current: { season: null!, year: null!, formatsGeneratedAt: null!, period: null! },
		formatSchedule: {},
		badgeholders: {},
	};
}

export function getBadges(user: User, curFormat: string) {
	let userBadges: { type: string, format: string }[] = [];
	const season = data.current.season; // don't factor in old badges
	for (const format in data.badgeholders[season]) {
		const badges = data.badgeholders[season][format];
		for (const type in badges) {
			if (badges[type].includes(user.id)) {
				// ex badge-bronze-gen9ou-250-1-2024
				userBadges.push({ type, format });
			}
		}
	}
	// find which ones we should prioritize showing - badge of current tier/season, then top badges of other formats for this season
	let curFormatBadge;
	for (const [i, badge] of userBadges.entries()) {
		if (badge.format === curFormat) {
			userBadges.splice(i, 1);
			curFormatBadge = badge;
		}
	}
	// now - sort by highest levels
	userBadges = Utils.sortBy(userBadges, x => Object.keys(BADGE_THRESHOLDS).indexOf(x.type))
		.slice(0, 2);
	if (curFormatBadge) userBadges.unshift(curFormatBadge);
	// format and return
	return userBadges;
}

function getUserHTML(user: User, format: string) {
	const buf = `<username>${user.name}</username>`;
	const badgeType = getBadges(user, format).find(x => x.format === format)?.type;
	if (badgeType) {
		let formatType = format.split(/gen\d+/)[1];
		if (!['ou', 'randombattle'].includes(formatType)) formatType = 'rotating';
		return `<img src="https://${Config.routes.client}/sprites/misc/${formatType}_${badgeType}.png" />` + buf;
	}
	return buf;
}

export function setFormatSchedule() {
	// guard heavily against this being overwritten
	if (data.current.formatsGeneratedAt === getYear()) return;
	data.current.formatsGeneratedAt = getYear();
	const formats = generateFormatSchedule();
	for (const [i, formatList] of formats.entries()) {
		data.formatSchedule[i + 1] = FIXED_FORMATS.concat(formatList.slice());
	}
	saveData();
}

class ScheduleGenerator {
	formats: string[][];
	items = new Map<string, number>();
	constructor() {
		this.formats = new Array(SEASONS_PER_YEAR).fill(null).map(() => [] as string[]);
		for (const format of FORMAT_POOL) this.items.set(format, 0);
	}
	generate() {
		for (let i = 0; i < this.formats.length; i++) {
			this.step([i, 0]);
		}
		for (let i = 1; i < SEASONS_PER_YEAR; i++) {
			this.step([0, i]);
		}
		return this.formats;
	}
	swap(x: number, y: number) {
		const item = this.formats[x][y];
		for (let i = 0; i < SEASONS_PER_YEAR; i++) {
			if (this.formats[i].includes(item)) continue;
			for (const [j, cur] of this.formats[i].entries()) {
				if (cur === item) continue;
				if (this.formats[x].includes(cur)) continue;
				this.formats[i][j] = item;
				return cur;
			}
		}
		throw new Error("Couldn't find swap target for " + item + ": " + JSON.stringify(this.formats));
	}
	select(x: number, y: number): string {
		const items = Array.from(this.items).filter(entry => entry[1] < 2);
		const item = Utils.randomElement(items);
		if (item[1] >= 2) {
			this.items.delete(item[0]);
			return this.select(x, y);
		}
		this.items.set(item[0], item[1] + 1);
		if (item[0] && this.formats[x].includes(item[0])) {
			this.formats[x][y] = item[0];
			return this.swap(x, y);
		}
		return item[0];
	}
	step(start: [number, number]) {
		let [x, y] = start;
		while (x < this.formats.length && y < FORMATS_PER_SEASON) {
			const item = this.select(x, y);
			this.formats[x][y] = item;
			x++;
			y++;
		}
	}
}

export function generateFormatSchedule() {
	return new ScheduleGenerator().generate();
}

export async function getLadderTop(format: string) {
	try {
		const results = await Net(`https://${Config.routes.root}/ladder/?format=${toID(format)}&json`).get();
		const reply = JSON.parse(results);
		return reply.toplist;
	} catch {
		return null;
	}
}

export async function updateBadgeholders() {
	rollSeason();
	const period = `${data.current.season}`;
	if (!data.badgeholders[period]) {
		data.badgeholders[period] = {};
	}
	for (const formatName of data.formatSchedule[findPeriod()]) {
		const formatid = `gen${Dex.gen}${formatName}`;
		const response = await getLadderTop(formatid);
		if (!response) continue; // ??
		const newHolders: Record<string, string[]> = {};
		for (const [i, row] of response.entries()) {
			let badgeType = null;
			for (const type in BADGE_THRESHOLDS) {
				if ((i + 1) <= BADGE_THRESHOLDS[type]) {
					badgeType = type;
					break;
				}
			}
			if (!badgeType) break;
			if (!newHolders[badgeType]) newHolders[badgeType] = [];
			newHolders[badgeType].push(row.userid);
		}
		data.badgeholders[period][formatid] = newHolders;
	}
	saveData();
}

function getYear() {
	return new Date().getFullYear();
}

function findPeriod(modifier = 0) {
	return Math.floor((new Date().getMonth() + modifier) / (SEASONS_PER_YEAR - 1)) + 1;
}

/** Are we in the last three days of the month (the public phase, where badged battles are public and the room is active?) */
function checkPublicPhase() {
	const daysInCurrentMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
	// last 3 days of the month, and next month is a new season
	return new Date().getDate() >= (daysInCurrentMonth - PUBLIC_PHASE_LENGTH) && findPeriod() !== findPeriod(1);
}

export function saveData() {
	FS('config/chat-plugins/seasons.json').writeUpdate(() => JSON.stringify(data));
}

export function rollSeason() {
	const year = getYear();
	if (data.current.year !== year) {
		data.current.year = year;
		setFormatSchedule();
	}
	if (findPeriod() !== data.current.period) {
		data.current.season++;
		data.badgeholders[data.current.season] = {};
		for (const k of data.formatSchedule[findPeriod()]) {
			data.badgeholders[data.current.season][`gen${Dex.gen}${k}`] = {};
		}
		data.current.period = findPeriod();
		saveData();
	}
}

export let updateTimeout: NodeJS.Timeout | true | null = null;

export function rollTimer() {
	if (updateTimeout === true) return;
	if (updateTimeout) {
		clearTimeout(updateTimeout);
	}
	updateTimeout = true;
	void updateBadgeholders();
	const time = Date.now();
	const next = new Date();
	next.setHours(next.getHours() + 1, 0, 0, 0);
	updateTimeout = setTimeout(() => rollTimer(), next.getTime() - time);

	const discussionRoom = Rooms.search('seasondiscussion');
	if (discussionRoom) {
		if (checkPublicPhase() && discussionRoom.settings.isPrivate) {
			discussionRoom.setPrivate(false);
			discussionRoom.settings.modchat = 'autoconfirmed';
			discussionRoom.add(
				`|html|<div class="broadcast-blue"><strong>The public phase of the month has now started!</strong>` +
				`<br /> Badged battles are now forced public, and this room is open for use.</div>`
			).update();
		} else if (!checkPublicPhase() && !discussionRoom.settings.isPrivate) {
			discussionRoom.setPrivate('unlisted');
			discussionRoom.add(
				`|html|<div class="broadcast-blue">The public phase of the month has ended.</div>`
			).update();
		}
	}
}

export function destroy() {
	if (updateTimeout && typeof updateTimeout !== 'boolean') {
		clearTimeout(updateTimeout);
	}
}

rollTimer();

export const commands: Chat.ChatCommands = {
	seasonschedule: 'seasons',
	seasons() {
		return this.parse(`/join view-seasonschedule`);
	},
};

export const pages: Chat.PageTable = {
	seasonschedule() {
		this.checkCan('globalban');
		let buf = `<div class="pad"><h2>Season schedule for ${getYear()}</h2><br />`;
		buf += `<div class="ladder pad"><table><tr><th>Season #</th><th>Formats</th></tr>`;
		for (const period in data.formatSchedule) {
			const match = findPeriod() === Number(period);
			const formatString = data.formatSchedule[period]
				.sort()
				.map(x => Dex.formats.get(x).name.replace(`[Gen ${Dex.gen}]`, ''))
				.join(', ');
			buf += `<tr><td>${match ? `<strong>${period}</strong>` : period}</td>`;
			buf += `<td>${match ? `<strong>${formatString}</strong>` : formatString}</td></tr>`;
		}
		buf += `</tr></table></div>`;
		return buf;
	},
	seasonladder(query, user) {
		const format = toID(query.shift());
		const season = toID(query.shift()) || `${data.current.season}`;
		if (!data.badgeholders[season]) {
			throw new Chat.ErrorMessage(`Season ${season} not found.`);
		}
		this.title = `[Seasons]`;
		let buf = '<div class="pad">';
		if (!Object.keys(data.badgeholders[season]).includes(format)) {
			// fall back to the master list so that people can still access this easily from the ladder page of other formats
			this.title += ` All`;
			buf += `<h2>Season Records</h2>`;
			const seasonsDesc = Utils.sortBy(
				Object.keys(data.badgeholders),
				s => s.split('-').map(x => -Number(x))
			);
			for (const s of seasonsDesc) {
				buf += `<h3>Season ${s}</h3><hr />`;
				for (const f in data.badgeholders[s]) {
					buf += `<a class="button" name="send" target="replace" href="/view-seasonladder-${f}-${s}">${Dex.formats.get(f).name}</a>`;
				}
				buf += `<br />`;
			}
			return buf;
		}
		this.title += ` ${format} [Season ${season}]`;
		const uppercase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
		let formatName = Dex.formats.get(format).name;
		// futureproofing for gen10/etc
		const room = Rooms.search(Utils.splitFirst(format, /\d+/)[1] || '');
		if (room) {
			formatName = `<a href="/${room.roomid}">${formatName}</a>`;
		}
		buf += `<h2>Season results for ${formatName} [${season}]</h2>`;
		buf += `<small><a target="replace" href="/view-seasonladder">View past seasons</a></small>`;
		let i = 0;
		for (const badgeType in data.badgeholders[season][format]) {
			buf += `<div class="ladder pad"><table>`;
			let formatType = format.split(/gen\d+/)[1];
			if (!['ou', 'randombattle'].includes(formatType)) formatType = 'rotating';
			buf += `<tr><h2><img src="https://${Config.routes.client}/sprites/misc/${formatType}_${badgeType}.png" /> ${uppercase(badgeType)}</h2></tr>`;
			for (const userid of data.badgeholders[season][format][badgeType]) {
				i++;
				buf += `<tr><td>${i}</td><td><a href="https://${Config.routes.root}/users/${userid}">${userid}</a></td></tr>`;
			}
			buf += `</table></div>`;
		}
		return buf;
	},
};

export const handlers: Chat.Handlers = {
	onBattleStart(user, room) {
		if (!room.battle) return; // should never happen, just sating TS
		// now first verify they have a badge
		const badges = getBadges(user, room.battle.format);
		if (!badges.length) return;
		const slot = room.battle.playerTable[user.id]?.slot;
		if (!slot) return; // not in battle fsr? wack
		for (const badge of badges) {
			room.add(`|badge|${slot}|${badge.type}|${badge.format}|${BADGE_THRESHOLDS[badge.type]}-${data.current.season}`);
		}

		if (
			checkPublicPhase() && !room.battle.forcedSettings.privacy &&
			badges.filter(x => x.format === room.battle!.format).length && room.battle.rated
		) {
			room.battle.forcedSettings.privacy = 'medal';
			room.add(
				`|html|<div class="broadcast-red"><strong>This battle is required to be public due to one or more player having a season medal.</strong><br />` +
				`During the public phase, you can discuss the state of the ladder <a href="/seasondiscussion">in a special chatroom.</a></div>`
			);
			room.setPrivate(false);
			const seasonRoom = Rooms.search('seasondiscussion');
			if (seasonRoom) {
				const p1html = getUserHTML(user, room.battle.format);
				const otherPlayer = user.id === room.battle.p1.id ? room.battle.p2 : room.battle.p1;
				const otherUser = otherPlayer.getUser();
				const p2html = otherUser ? getUserHTML(otherUser, room.battle.format) : `<username>${otherPlayer.name}</username>`;
				const formatName = Dex.formats.get(room.battle.format).name;
				seasonRoom.add(
					`|raw|<a href="/${room.roomid}" class="ilink">${formatName} battle started between ` +
					`${p1html} and ${p2html}. (rating: ${Math.floor(room.battle.rated)})</a>`
				).update();
			}
		}

		room.add(
			`|uhtml|medal-msg|<div class="broadcast-blue">Curious what those medals under the avatar are? PS now has Ladder Seasons!` +
			` For more information, check out the <a href="https://www.smogon.com/forums/threads/3740067/">thread on Smogon.</a></div>`
		);
		room.update();
	},
};
