import { ChangeEventCR, ChangeEventUpdate, ObjectId } from 'mongodb';
import * as store from "./store";
import { combineLatest, defer, EMPTY, from, merge, Observable, timer } from "rxjs";
import { delay, distinctUntilChanged, filter, first, map, mapTo, mergeAll, share, switchAll, takeUntil } from 'rxjs/operators';
import { Majsoul, Store } from ".";
import { nameofContest } from "./connector";

export class ContestTracker {
	private contestDeleted$: Observable<any>;
	private contestUpdates$: Observable<ChangeEventUpdate<store.Contest<ObjectId>>>;
	private gamesCreated$: Observable<ChangeEventCR<store.GameResult<ObjectId>>>;
	constructor(
		public readonly id: ObjectId,
		private readonly mongoStore: Store.Store,
		private readonly api: Majsoul.Api
	) { }

	public get ContestDeleted$(): Observable<any> {
		return this.contestDeleted$ ??= merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id }))).pipe(
				filter(contest => contest == null)
			),
			this.mongoStore.ContestChanges.pipe(
				filter(changeEvent => changeEvent.operationType === "delete"
					&& changeEvent.documentKey._id.equals(this.id))
			)
		).pipe(first(), share());
	}

	private get ContestUpdates$(): Observable<ChangeEventUpdate<store.Contest<ObjectId>>> {
		return this.contestUpdates$ ??= this.mongoStore.ContestChanges.pipe(
			filter(changeEvent => changeEvent.operationType === "update"
				&& changeEvent.documentKey._id.equals(this.id)
			),
			share()
		) as Observable<ChangeEventUpdate<store.Contest<ObjectId>>>;
	}

	private get GamesCreated$(): Observable<ChangeEventCR<store.GameResult<ObjectId>>> {
		return this.gamesCreated$ ??= this.mongoStore.GameChanges.pipe(
			filter(changeEvent => changeEvent.operationType === "insert"
				&& changeEvent.fullDocument.contestId.equals(this.id)
			),
			share()
		) as Observable<ChangeEventCR<store.GameResult<ObjectId>>>;
	}

	public get NotFoundOnMajsoul$(): Observable<boolean> {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.notFoundOnMajsoul ?? false)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("notFoundOnMajsoul")) >= 0),
				mapTo(false)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.notFoundOnMajsoul !== undefined),
				map(event => event.updateDescription.updatedFields.notFoundOnMajsoul)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get MajsoulId$(): Observable<number> {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.majsoulId)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("majsoulId")) >= 0),
				mapTo(null as number)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.majsoulId !== undefined),
				map(event => event.updateDescription.updatedFields.majsoulId)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get MajsoulFriendlyId$(): Observable<number> {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.notFoundOnMajsoul ? null as number : contest.majsoulFriendlyId)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("majsoulFriendlyId")) >= 0
					|| event.updateDescription.updatedFields?.notFoundOnMajsoul === true),
				mapTo(null as number)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.majsoulFriendlyId !== undefined),
				map(event => event.updateDescription.updatedFields.majsoulFriendlyId)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get UpdateRequest$() {
		return this.MajsoulFriendlyId$.pipe(
			map(majsoulFriendlyId => majsoulFriendlyId == null
				? EMPTY
				: timer(0, 86400000).pipe(
					mapTo(majsoulFriendlyId),
					takeUntil(this.ContestDeleted$)
				)
			),
			switchAll()
		);
	}

	public get Track$(): Observable<boolean> {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.track ?? false)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("track")) >= 0),
				mapTo(false)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.track !== undefined),
				map(event => event.updateDescription.updatedFields.track ?? false)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get AdminPlayerFetchRequested$(): Observable<boolean> {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.adminPlayerFetchRequested ?? false)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("adminPlayerFetchRequested")) >= 0),
				mapTo(false)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.adminPlayerFetchRequested !== undefined),
				map(event => event.updateDescription.updatedFields.adminPlayerFetchRequested ?? false)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get LiveGames$() {
		return combineLatest([this.MajsoulId$, this.NotFoundOnMajsoul$, this.Track$]).pipe(
			map(([majsoulId, notFoundOnMajsoul, track]) => (majsoulId == null || notFoundOnMajsoul || !track)
				? EMPTY
				: this.api.subscribeToContestChatSystemMessages(majsoulId).pipe(
					filter(notification => notification.game_end && notification.game_end.constructor.name === "CustomizedContestGameEnd"),
					map(notification => notification.uuid as string),
					takeUntil(this.ContestDeleted$)
				)
			),
			switchAll(),
			delay(2000),
		);
	}

	public get RecordedGames$() {
		return merge(
			defer(() => from(this.mongoStore.gamesCollection.find({ contestId: this.id }).toArray()))
				.pipe(mergeAll()),
			this.GamesCreated$.pipe(
				map(event => event.fullDocument)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get Teams$() {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.teams)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("teams")) >= 0),
				mapTo(null as store.ContestTeam<ObjectId>[])
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.teams !== undefined),
				map(event => event.updateDescription.updatedFields.teams)
			)
		).pipe(
			takeUntil(this.ContestDeleted$)
		);
	}

	public get SpreadsheetId$() {
		return merge(
			defer(() => from(this.mongoStore.contestCollection.findOne({ _id: this.id })))
				.pipe(map(contest => contest.spreadsheetId)),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.removedFields.indexOf(nameofContest("spreadsheetId")) >= 0
					|| event.updateDescription.updatedFields?.notFoundOnMajsoul === true),
				mapTo(null as string)
			),
			this.ContestUpdates$.pipe(
				filter(event => event.updateDescription.updatedFields?.spreadsheetId !== undefined),
				map(event => event.updateDescription.updatedFields.spreadsheetId)
			)
		).pipe(
			distinctUntilChanged(),
			takeUntil(this.ContestDeleted$)
		);
	}
}