import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js';

class TfTabNewsFeedElement extends LitElement {
	static get properties() {
		return {
			whoami: {type: String},
			users: {type: Object},
			hash: {type: String},
			following: {type: Array},
			messages: {type: Array},
			drafts: {type: Object},
			expanded: {type: Object},
			channels_unread: {type: Object},
			channels_latest: {type: Object},
			loading: {type: Number},
			time_range: {type: Array},
			time_loading: {type: Array},
			private_messages: {type: Array},
			grouped_private_messages: {type: Object},
			recent_reactions: {type: Array},
			direction: {type: Boolean},
		};
	}

	static styles = styles;

	constructor() {
		super();
		let self = this;
		this.whoami = null;
		this.users = {};
		this.hash = '#';
		this.following = [];
		this.drafts = {};
		this.expanded = {};
		this.channels_unread = {};
		this.channels_latest = {};
		this.start_time = new Date().valueOf();
		this.time_range = [0, 0];
		this.time_loading = undefined;
		this.recent_reactions = [];
		this.loading = 0;
	}

	channel() {
		return this.hash.startsWith('##')
			? this.hash.substring(2)
			: this.hash.substring(1);
	}

	async _fetch_related_messages(messages, max_rowid) {
		let refs = await tfrpc.rpc.query(
			`
				WITH
					news AS (
						SELECT value AS id FROM json_each(?)
					)
				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
				UNION
				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
			`,
			[JSON.stringify(messages.map((x) => x.id)), max_rowid],
			{cacheable: true}
		);
		let related_messages = await tfrpc.rpc.query(
			`
				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
				FROM messages
				JOIN json_each(?2) refs ON messages.id = refs.value
				JOIN json_each(?1) AS following ON messages.author = following.value
				where messages.rowid <= ?3
			`,
			[
				JSON.stringify(this.following),
				JSON.stringify(refs.map((x) => x.ref)),
				max_rowid,
			],
			{cacheable: true}
		);
		let combined = [].concat(messages, related_messages);
		let refs2 = await tfrpc.rpc.query(
			`
				WITH
					news AS (
						SELECT value AS id FROM json_each(?)
					)
				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
				UNION
				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
			`,
			[JSON.stringify(combined.map((x) => x.id)), max_rowid],
			{cacheable: true}
		);
		let result = [].concat(
			combined,
			await tfrpc.rpc.query(
				`
				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
				FROM json_each(?2) refs
				JOIN messages ON messages.id = refs.value
				JOIN json_each(?1) following ON messages.author = following.value
				WHERE messages.content ->> 'type' != 'post' AND messages.rowid <= ?3
			`,
				[
					JSON.stringify(this.following),
					JSON.stringify(refs2.map((x) => x.ref)),
					max_rowid,
				],
				{cacheable: true}
			)
		);
		return result;
	}

	async fetch_messages(start_time, end_time) {
		this.dispatchEvent(
			new CustomEvent('loadmessages', {
				bubbles: true,
				composed: true,
			})
		);
		this.time_loading = [start_time, end_time];
		let max_rowid = (
			await tfrpc.rpc.query(`SELECT MAX(rowid) AS max_rowid FROM messages`)
		)[0].max_rowid;
		let result;
		const k_max_results = 64;
		if (this.hash == '#@') {
			result = await tfrpc.rpc.query(
				`
					WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM messages_refs
						JOIN messages ON messages.id = messages_refs.message
						JOIN json_each(?2) AS following ON messages.author = following.value
						WHERE
							messages_refs.ref = ?1 AND
							messages.author != ?1 AND
							(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4 AND
							messages.rowid <= ?6
						ORDER BY timestamp DESC limit ?5)
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM mentions
						JOIN messages_refs ON mentions.id = messages_refs.ref
						JOIN messages ON messages_refs.message = messages.id
						WHERE mentions.rowid <= ?6
					UNION
					SELECT TRUE AS is_primary, * FROM mentions
				`,
				[
					this.whoami,
					JSON.stringify(this.following),
					start_time,
					end_time,
					k_max_results,
					max_rowid,
				],
				{cacheable: true}
			);
		} else if (this.hash.startsWith('#@')) {
			result = await tfrpc.rpc.query(
				`
					WITH
						selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
							FROM messages
							WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3 AND messages.rowid <= ?5
							ORDER BY sequence DESC LIMIT ?4
						)
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM selected
						JOIN messages_refs ON selected.id = messages_refs.ref
						JOIN messages ON messages_refs.message = messages.id
						WHERE messages.rowid <= ?5
					UNION
					SELECT TRUE AS is_primary, * FROM selected
				`,
				[
					this.hash.substring(1),
					start_time,
					end_time,
					k_max_results,
					max_rowid,
				],
				{cacheable: true}
			);
		} else if (this.hash.startsWith('#%')) {
			result = await tfrpc.rpc.query(
				`
					SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
					FROM messages
					WHERE messages.id = ?1
					UNION
					SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
					FROM messages JOIN messages_refs
					ON messages.id = messages_refs.message
					WHERE messages_refs.ref = ?1 AND messages.rowid <= ?2
				`,
				[this.hash.substring(1), max_rowid],
				{cacheable: true}
			);
		} else if (this.hash.startsWith('##')) {
			let initial_messages = await tfrpc.rpc.query(
				`
					WITH
						all_news AS (
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
								FROM messages
								JOIN json_each(?) AS following ON messages.author = following.value
								WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote' AND messages.rowid <= ?6
							UNION
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
								FROM messages_refs
								JOIN messages ON messages.id = messages_refs.message
								JOIN json_each(?1) AS following ON messages.author = following.value
								WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote' AND messages.rowid <= ?6
							)
					SELECT TRUE AS is_primary, all_news.* FROM all_news
						WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
						ORDER BY all_news.timestamp DESC LIMIT ?5
				`,
				[
					JSON.stringify(this.following),
					start_time,
					end_time,
					this.hash.substring(2),
					k_max_results,
					max_rowid,
				],
				{cacheable: true}
			);
			result = await this._fetch_related_messages(initial_messages, max_rowid);
		} else if (this.hash.startsWith('#🔐')) {
			let ids =
				this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
			result = await tfrpc.rpc.query(
				`
					SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
					FROM messages
					JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
					WHERE
						(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
						json(messages.content) LIKE '"%'
					ORDER BY messages.rowid DESC LIMIT ?4
				`,
				[
					JSON.stringify(
						this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
							(x) => x.id
						) ?? []
					),
					start_time,
					end_time,
					k_max_results,
				],
				{cacheable: true}
			);
			let decrypted = (await this.decrypt(result)).filter((x) => x.decrypted);
			result = await this._fetch_related_messages(decrypted, max_rowid);
		} else if (this.hash == '#👍') {
			result = await tfrpc.rpc.query(
				`
					WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM messages
						JOIN json_each(?1) AS following ON messages.author = following.value
						WHERE
							messages.content ->> 'type' = 'vote' AND
							(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3 AND
							messages.rowid <= ?5
						ORDER BY timestamp DESC limit ?4)
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM votes
						JOIN messages ON messages.id = votes.content ->> '$.vote.link'
						WHERE votes.rowid <= ?5
					UNION
					SELECT TRUE AS is_primary, * FROM votes
				`,
				[
					JSON.stringify(this.following),
					start_time,
					end_time,
					k_max_results,
					max_rowid,
				],
				{cacheable: true}
			);
		} else if (this.hash == '#🚩') {
			result = await tfrpc.rpc.query(
				`
					WITH flags AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM messages
						WHERE
							messages.content ->> 'type' = 'flag' AND
							(?1 IS NULL OR messages.timestamp >= ?1) AND messages.timestamp < ?2 AND
							messages.rowid <= ?4
						ORDER BY timestamp DESC limit ?3)
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
						FROM flags
						JOIN messages ON messages.id = flags.content ->> '$.flag.link' AND
						flags.rowid <= ?4
					UNION
					SELECT TRUE AS is_primary, * FROM flags
				`,
				[start_time, end_time, k_max_results, max_rowid],
				{cacheable: true}
			);
		} else if (this.hash.startsWith('#🗓️')) {
			let start_date = new Date(this.hash.substring('#🗓️'.length));
			let end_date = new Date(start_date.valueOf() + 60 * 60 * 24 * 1000);
			let initial_messages = await tfrpc.rpc.query(
				`
					WITH
						channels AS (SELECT '#' || value AS value FROM json_each(?5))
					SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
							FROM messages
							JOIN json_each(?1) AS following ON messages.author = following.value
							WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
								messages.content ->> 'type' != 'vote' AND
								(messages.content ->> 'root' IS NULL OR (
									NOT EXISTS (SELECT * FROM messages root JOIN channels ON ('#' || (root.content ->> 'channel')) = channels.value WHERE root.id = messages.content ->> 'root') AND
									NOT EXISTS (SELECT * FROM messages root JOIN messages_refs ON root.id = messages.content ->> 'root' JOIN channels ON messages_refs.message = root.id AND messages_refs.ref = channels.value)
								)) AND
								(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND
								NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value) AND
								messages.rowid <= ?6
					ORDER BY timestamp DESC LIMIT ?4
				`,
				[
					JSON.stringify(this.following),
					start_date.valueOf(),
					end_date.valueOf(),
					k_max_results,
					JSON.stringify(Object.keys(this.channels_latest)),
					max_rowid,
				],
				{cacheable: true}
			);
			result = await this._fetch_related_messages(initial_messages, max_rowid);
		} else {
			let initial_messages = await tfrpc.rpc.query(
				`
					WITH
						channels AS (SELECT '#' || value AS value FROM json_each(?5))
					SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
							FROM messages
							JOIN json_each(?1) AS following ON messages.author = following.value
							WHERE
								messages.content ->> 'type' != 'vote' AND
								(messages.content ->> 'root' IS NULL OR (
									NOT EXISTS (SELECT * FROM messages root JOIN channels ON ('#' || (root.content ->> 'channel')) = channels.value WHERE root.id = messages.content ->> 'root') AND
									NOT EXISTS (SELECT * FROM messages root JOIN messages_refs ON root.id = messages.content ->> 'root' JOIN channels ON messages_refs.message = root.id AND messages_refs.ref = channels.value)
								)) AND
								(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND
								NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value) AND
								messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
								messages.rowid <= ?6
					ORDER BY timestamp DESC LIMIT ?4
				`,
				[
					JSON.stringify(this.following),
					start_time,
					end_time,
					k_max_results,
					JSON.stringify(Object.keys(this.channels_latest)),
					max_rowid,
				],
				{cacheable: true}
			);
			result = await this._fetch_related_messages(initial_messages, max_rowid);
		}
		this.time_loading = undefined;
		return result;
	}

	update_time_range_from_messages(messages) {
		let only_primary = messages.filter((x) => x.is_primary);
		this.time_range = [
			only_primary.reduce(
				(accumulator, current) => Math.min(accumulator, current.timestamp),
				this.time_range[0]
			),
			only_primary.reduce(
				(accumulator, current) => Math.max(accumulator, current.timestamp),
				this.time_range[1]
			),
		];
	}

	unread_allowed() {
		return (
			this.hash == '#@' ||
			(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
		);
	}

	async load_more() {
		this.loading++;
		this.loading_canceled = false;
		try {
			let last_start_time = this.time_range[0];
			let more = await this.fetch_messages(null, last_start_time);
			this.update_time_range_from_messages(
				more.filter((x) => x.timestamp < last_start_time)
			);
			this.messages = await this.decrypt([...more, ...this.messages]);
		} finally {
			this.loading--;
		}
	}

	cancel_load() {
		this.loading_canceled = true;
	}

	async decrypt(messages) {
		let result = [];
		for (let message of messages) {
			let content;
			try {
				content = JSON.parse(message?.content);
			} catch {}
			if (typeof content === 'string') {
				let decrypted;
				try {
					decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
				} catch {}
				if (decrypted) {
					try {
						message.decrypted = JSON.parse(decrypted);
					} catch {
						message.decrypted = decrypted;
					}
				}
			}
			result.push(message);
		}
		return result;
	}

	merge_messages(old_messages, new_messages) {
		let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
		return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
	}

	async load_latest() {
		this.loading++;
		let now = new Date().valueOf();
		const k_one_hour = 60 * 60 * 1000;
		const k_one_day = 24 * k_one_hour;
		let end_time = Math.round((now + k_one_day) / k_one_hour) * k_one_hour;
		let messages = [];
		try {
			messages = await this.fetch_messages(this.time_range[0], end_time);
			messages = await this.decrypt(messages);
			this.update_time_range_from_messages(
				messages.filter(
					(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
				)
			);
		} finally {
			this.loading--;
		}
		this.messages = this.merge_messages(
			this.messages,
			Object.values(
				Object.fromEntries(
					[...this.messages, ...messages]
						.sort((x, y) => x.timestamp - y.timestamp)
						.slice(-1024)
						.map((x) => [x.id, x])
				)
			)
		);
	}

	make_messages_key() {
		return JSON.stringify([
			this.hash,
			Object.keys(this.channels_latest ?? {}).filter((x) => x != '🔐'),
		]);
	}

	async load_messages() {
		let start_time = new Date();
		let self = this;
		this.loading++;
		let messages = [];
		let original_key = this.make_messages_key();
		try {
			if (this._messages_key !== original_key) {
				this.messages = [];
				this._messages_key = original_key;
			}
			this._messages_following = JSON.stringify(this.following);
			this._private_messages = JSON.stringify([
				this.private_messages,
				this.grouped_private_messages,
			]);
			this._channels_latest = JSON.stringify(
				Object.keys(this.channels_latest ?? {})
			);
			let now = new Date().valueOf();
			let start_time = now - 24 * 60 * 60 * 1000;
			this.start_time = start_time;
			this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
			messages = await this.fetch_messages(null, this.time_range[1]);
			this.update_time_range_from_messages(
				messages.filter((x) => x.timestamp < this.time_range[1])
			);
			messages = await this.decrypt(messages);
		} finally {
			this.loading--;
		}
		let current_key = this.make_messages_key();
		if (current_key === original_key) {
			this.messages = this.merge_messages(this.messages, messages);
		}
		this.time_loading = undefined;
	}

	mark_all_read() {
		let newest = this.messages.reduce(
			(accumulator, current) => Math.max(accumulator, current.rowid),
			this.channels_latest[this.channel()] ?? -1
		);
		if (newest >= 0) {
			this.dispatchEvent(
				new CustomEvent('channelsetunread', {
					bubbles: true,
					composed: true,
					detail: {
						channel: this.channel(),
						unread: newest + 1,
					},
				})
			);
		}
	}

	close_private_chat() {
		this.mark_all_read();
		this.dispatchEvent(
			new CustomEvent('closeprivatechat', {
				bubbles: true,
				composed: true,
				detail: {
					key: JSON.stringify(
						this.hash == '#🔐'
							? []
							: this.hash.substring('#🔐'.length).split(',')
					),
				},
			})
		);
		tfrpc.rpc.setHash('#');
	}

	render_close_chat_button() {
		if (this.hash.startsWith('#🔐')) {
			return html`
				<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}>
					Close Chat
				</button>
			`;
		}
	}

	render() {
		if (
			!this.messages ||
			this._messages_key !== this.make_messages_key() ||
			this._messages_following !== JSON.stringify(this.following) ||
			(this.hash.startsWith('#🔐') &&
				this._private_messages !==
					JSON.stringify([
						this.private_messages,
						this.grouped_private_messages,
					]))
		) {
			console.log(
				`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
			);
			this.load_messages();
		}
		let more;
		if (!this.hash.startsWith('#%')) {
			more = html`
				<p>
					${this.unread_allowed()
						? html`
								<button
									class="w3-button w3-theme-d1"
									@click=${this.mark_all_read}
								>
									Mark All Read
								</button>
							`
						: undefined}
					<button
						?disabled=${this.loading}
						class="w3-button w3-theme-d1"
						@click=${this.load_more}
					>
						Load More
					</button>
					<button
						class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
						@click=${this.cancel_load}
					>
						Cancel
					</button>
					<span
						>Showing
						${new Date(
							this.time_loading
								? Math.min(this.time_loading[0], this.time_range[0])
								: this.time_range[0]
						).toLocaleDateString()}
						-
						${new Date(
							this.time_loading
								? Math.max(this.time_loading[1], this.time_range[1])
								: this.time_range[1]
						).toLocaleDateString()}.</span
					>
				</p>
			`;
		}
		let unread = this.unread_allowed()
			? html`<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
					Mark All Read
				</button>`
			: undefined;
		return cache(html`
			<style>
				${generate_theme()}
			</style>
			<div class="w3-margin-bottom">
				${this.direction ? more : unread}
				${this.direction ? undefined : this.render_close_chat_button()}
			</div>
			<tf-news
				id="news"
				whoami=${this.whoami}
				.users=${this.users}
				.messages=${this.messages}
				.following=${this.following}
				.drafts=${this.drafts}
				.expanded=${this.expanded}
				hash=${this.hash}
				channel=${this.channel()}
				channel_unread=${this.channels_unread?.[this.channel()]}
				.recent_reactions=${this.recent_reactions}
				@mark_all_read=${this.mark_all_read}
				?direction=${this.direction}
			></tf-news>
			${!this.loading && this.hash.startsWith('#%') && !this.messages?.length
				? html`<div class="w3-container">
						Message <b>${this.hash.substring(1)}</b> not found.
					</div>`
				: undefined}
			<div class="w3-margin-top">
				${this.direction ? this.render_close_chat_button() : undefined}
				${this.direction ? unread : more}
			</div>
		`);
	}
}

customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
