import { ChangeEvent, ChangeStream, Collection, MongoClient, ObjectId } from "mongodb";
import { Contest, GameResult, Player, User, Session, Config, GameResultVersion, latestGameResultVersion, GameCorrection } from "./types/types";
import { Observable, Subject } from "rxjs";

interface Migration {
	perform(store: Store): Promise<void>;
}

const migrations: Migration[] = [];
export class Store {
	public contestCollection: Collection<Contest<ObjectId>>;
	public gamesCollection: Collection<GameResult<ObjectId>>;
	public gameCorrectionsCollection: Collection<GameCorrection<ObjectId>>;
	public sessionsCollection: Collection<Session<ObjectId>>;
	public playersCollection: Collection<Player<ObjectId>>;
	public configCollection: Collection<Config<ObjectId>>;
	public userCollection: Collection<User<ObjectId>>;

	private readonly contestChangesSubject = new Subject<ChangeEvent<Contest<ObjectId>>>();
	private readonly configChangesSubject = new Subject<ChangeEvent<Config<ObjectId>>>();
	private readonly gameChangesSubject = new Subject<ChangeEvent<GameResult<ObjectId>>>();
	private readonly playerChangesSubject = new Subject<ChangeEvent<Player<ObjectId>>>();
	private contestStream: ChangeStream<Contest<ObjectId>>;
	private configStream: ChangeStream<Config<ObjectId>>;
	private gameStream: ChangeStream<GameResult<ObjectId>>;
	private playerStream: ChangeStream<GameResult<ObjectId>>;

	public get ContestChanges(): Observable<ChangeEvent<Contest<ObjectId>>> {
		return this.contestChangesSubject;
	}

	public get ConfigChanges(): Observable<ChangeEvent<Config<ObjectId>>> {
		return this.configChangesSubject;
	}

	public get GameChanges(): Observable<ChangeEvent<GameResult<ObjectId>>> {
		return this.gameChangesSubject;
	}

	public get PlayerChanges(): Observable<ChangeEvent<Player<ObjectId>>> {
		return this.playerChangesSubject;
	}

	public async init(username: string, password: string): Promise<void> {
		const url = `mongodb://${username}:${password}@${process.env.NODE_ENV === "production" ? 'majsoul_mongo' : 'localhost'}:27017/?authMechanism=SCRAM-SHA-256&authSource=admin`;
		const client = new MongoClient(url);

		await client.connect();

		console.log("Connected successfully to server");

		const majsoulDb = client.db('majsoul');

		this.contestCollection = await majsoulDb.collection("contests");
		this.gamesCollection = await majsoulDb.collection("games");
		this.gameCorrectionsCollection = await majsoulDb.collection("gameCorrections");
		this.sessionsCollection = await majsoulDb.collection("sessions");
		this.sessionsCollection.createIndex({ scheduledTime: -1 });
		this.playersCollection = await majsoulDb.collection("players");
		this.configCollection = await majsoulDb.collection("config");

		this.contestStream = this.contestCollection.watch().on("change", change => this.contestChangesSubject.next(change));
		this.configStream = this.configCollection.watch().on("change", change => this.configChangesSubject.next(change));
		this.gameStream = this.gamesCollection.watch().on("change", change => this.gameChangesSubject.next(change));
		this.playerStream = this.playersCollection.watch().on("change", change => this.playerChangesSubject.next(change));

		if ((await this.configCollection.countDocuments()) < 1) {
			this.configCollection.insertOne({});
		}

		const oauthDb = client.db('oauth');
		this.userCollection = await oauthDb.collection("users", {});
	}

	public async isGameRecorded(majsoulId: string): Promise<boolean> {
		return await this.gamesCollection.countDocuments(
			{
				majsoulId,
				version: {
					$gte: latestGameResultVersion,
				},
				$or: [
					{
						notFoundOnMajsoul: { $exists: true },
					},
					{
						contestMajsoulId: { $exists: true },
					}
				]
			},
			{ limit: 1 }
		) === 1;
	}

	public async recordGame(contestId: ObjectId, gameResult: GameResult): Promise<void> {
		console.log(`Recording game id ${gameResult.majsoulId}`);
		delete gameResult._id;
		const gameRecord: Omit<GameResult<ObjectId>, "_id"> = {
			...gameResult,
			contestId,
			notFoundOnMajsoul: false,
			players: (await Promise.all(gameResult.players
				.map(player =>
					player == null
						? Promise.resolve(null)
						: this.playersCollection.findOneAndUpdate(
							{ majsoulId: player.majsoulId },
							{ $set: { majsoulId: player.majsoulId, nickname: player.nickname } },
							{ upsert: true, returnOriginal: false, projection: { _id: true } }
						)
				)
			)).map(p => p?.value),
		};

		await this.gamesCollection.findOneAndUpdate(
			{
				majsoulId: gameResult.majsoulId
			},
			{
				$set: {
					...gameRecord,
				}
			},
			{
				upsert: true
			}
		);
	}

	public async migrate(): Promise<void> {
		for (const migration of migrations) {
			await migration.perform(this);
		}
	}
}