import { IsNull, LessThan, MoreThan } from "typeorm";
import { redisClient } from "../../db/redis.js";
import { genId } from "../../misc/gen-id.js";
import { ReversiGames, Users } from "../../models/index.js";
import { deserializeLogs, restoreGame, serializeLogs, standardMap, ReversiEngine } from "./engine.js";
import { publishReversiGameStream, publishReversiStream } from "../stream.js";
const INVITATION_TIMEOUT_MS = 1000 * 20;
const TIME_LIMIT_MIN = 5;
const TIME_LIMIT_MAX = 300;
const updateKeys = [
    "map",
    "bw",
    "isLlotheo",
    "canPutEverywhere",
    "loopedBoard",
    "timeLimitForEachTurn"
];
async function getGame(id) {
    return await ReversiGames.findOne({
        where: {
            id
        },
        relations: {
            user1: true,
            user2: true
        }
    });
}
async function publishGame(id, type, body) {
    publishReversiGameStream(id, type, body);
}
async function endGame(game, winnerId, reason) {
    await ReversiGames.update(game.id, {
        isEnded: true,
        endedAt: new Date(),
        winnerId,
        surrenderedUserId: reason === "surrender" ? winnerId === game.user1Id ? game.user2Id : game.user1Id : null,
        timeoutUserId: reason === "timeout" ? winnerId === game.user1Id ? game.user2Id : game.user1Id : null
    });
    const fresh = await getGame(game.id);
    if (fresh) {
        await publishGame(game.id, "ended", {
            winnerId,
            game: await ReversiGames.packDetail(fresh)
        });
    }
}
export function isValidUpdateKey(key) {
    return typeof key === "string" && updateKeys.includes(key);
}
export function isValidUpdateValue(key, value) {
    switch(key){
        case "map":
            return Array.isArray(value) && value.length > 0 && value.every((row)=>typeof row === "string" && row.length <= 64);
        case "bw":
            return value === "random" || value === "1" || value === "2";
        case "isLlotheo":
        case "canPutEverywhere":
        case "loopedBoard":
            return typeof value === "boolean";
        case "timeLimitForEachTurn":
            return typeof value === "number" && value >= TIME_LIMIT_MIN && value <= TIME_LIMIT_MAX;
    }
}
export async function matchSpecificUser(me, target, multiple = false) {
    if (!multiple) {
        const since = genId(new Date(Date.now() - 1000 * 60 * 3));
        const games = await ReversiGames.find({
            where: [
                {
                    id: MoreThan(since),
                    user1Id: me.id,
                    user2Id: target.id,
                    isStarted: false
                },
                {
                    id: MoreThan(since),
                    user1Id: target.id,
                    user2Id: me.id,
                    isStarted: false
                }
            ],
            relations: {
                user1: true,
                user2: true
            },
            order: {
                id: "DESC"
            }
        });
        if (games.length > 0) return games[0];
    }
    const invitations = await redisClient.zrangebyscore(`reversi:matchSpecific:${me.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
    if (invitations.includes(target.id)) {
        await redisClient.zrem(`reversi:matchSpecific:${me.id}`, target.id);
        return await matched(target.id, me.id, {
            noIrregularRules: false
        });
    }
    const pipeline = redisClient.pipeline();
    pipeline.zadd(`reversi:matchSpecific:${target.id}`, Date.now(), me.id);
    pipeline.expire(`reversi:matchSpecific:${target.id}`, 120);
    await pipeline.exec();
    publishReversiStream(target.id, "invited", {
        user: await Users.pack(me.id, target, {
            detail: false
        })
    });
    return null;
}
export async function matchAnyUser(me, options, multiple = false) {
    if (!multiple) {
        const since = genId(new Date(Date.now() - 1000 * 60 * 3));
        const games = await ReversiGames.find({
            where: [
                {
                    id: MoreThan(since),
                    user1Id: me.id,
                    isStarted: false
                },
                {
                    id: MoreThan(since),
                    user2Id: me.id,
                    isStarted: false
                }
            ],
            relations: {
                user1: true,
                user2: true
            },
            order: {
                id: "DESC"
            }
        });
        if (games.length > 0) return games[0];
    }
    const invitations = await redisClient.zrangebyscore(`reversi:matchSpecific:${me.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
    if (invitations.length > 0) {
        const inviterId = invitations[Math.floor(Math.random() * invitations.length)];
        await redisClient.zrem(`reversi:matchSpecific:${me.id}`, inviterId);
        return await matched(inviterId, me.id, {
            noIrregularRules: false
        });
    }
    const matchings = await redisClient.zrevrange("reversi:matchAny", 0, 2);
    const items = matchings.filter((id)=>!id.startsWith(me.id));
    if (items.length > 0) {
        const [matchedUserId, option] = items[0].split(":");
        await redisClient.zrem("reversi:matchAny", me.id, matchedUserId, `${me.id}:noIrregularRules`, `${matchedUserId}:noIrregularRules`);
        return await matched(matchedUserId, me.id, {
            noIrregularRules: options.noIrregularRules || option === "noIrregularRules"
        });
    }
    const pipeline = redisClient.pipeline();
    pipeline.zadd("reversi:matchAny", Date.now(), options.noIrregularRules ? `${me.id}:noIrregularRules` : me.id);
    pipeline.expire("reversi:matchAny", 15);
    await pipeline.exec();
    return null;
}
async function matched(parentId, childId, options) {
    const game = await ReversiGames.insert({
        id: genId(),
        startedAt: null,
        endedAt: null,
        user1Id: parentId,
        user2Id: childId,
        user1Ready: false,
        user2Ready: false,
        black: null,
        isStarted: false,
        isEnded: false,
        winnerId: null,
        surrenderedUserId: null,
        timeoutUserId: null,
        timeLimitForEachTurn: 90,
        logs: [],
        map: standardMap,
        bw: "random",
        noIrregularRules: options.noIrregularRules,
        isLlotheo: false,
        canPutEverywhere: false,
        loopedBoard: false,
        form1: null,
        form2: null,
        crc32: null
    }).then((x)=>getGame(x.identifiers[0].id));
    if (!game) throw new Error("failed to create reversi game");
    publishReversiStream(parentId, "matched", {
        game: await ReversiGames.packDetail(game)
    });
    return game;
}
export async function cancelMatch(user, userId) {
    if (userId) await redisClient.zrem(`reversi:matchSpecific:${userId}`, user.id);
    await redisClient.zrem("reversi:matchAny", user.id, `${user.id}:noIrregularRules`);
}
export async function getInvitations(user) {
    return await redisClient.zrangebyscore(`reversi:matchSpecific:${user.id}`, Date.now() - INVITATION_TIMEOUT_MS, "+inf");
}
export async function gameReady(gameId, user, ready) {
    const game = await getGame(gameId);
    if (!game || game.isStarted) return;
    if (game.user1Id !== user.id && game.user2Id !== user.id) return;
    const patch = game.user1Id === user.id ? {
        user1Ready: ready
    } : {
        user2Ready: ready
    };
    await ReversiGames.update(game.id, patch);
    const fresh = await getGame(game.id);
    if (!fresh) return;
    await publishGame(game.id, "changeReadyStates", {
        user1: fresh.user1Ready,
        user2: fresh.user2Ready
    });
    if (fresh.user1Ready && fresh.user2Ready) setTimeout(()=>startGame(fresh.id), 3000);
}
export async function updateSettings(gameId, user, key, value) {
    const game = await getGame(gameId);
    if (!game || game.isStarted) return;
    if (game.user1Id !== user.id && game.user2Id !== user.id) return;
    if (game.user1Id === user.id && game.user1Ready) return;
    if (game.user2Id === user.id && game.user2Ready) return;
    if (game.noIrregularRules && (key === "isLlotheo" || key === "canPutEverywhere" || key === "loopedBoard")) return;
    await ReversiGames.update(game.id, {
        [key]: value
    });
    await publishGame(game.id, "updateSettings", {
        userId: user.id,
        key,
        value
    });
}
async function startGame(gameId) {
    const game = await getGame(gameId);
    if (!game || game.isStarted || game.isEnded || !game.user1Ready || !game.user2Ready) return;
    const black = game.bw === "random" ? Math.random() > 0.5 ? 1 : 2 : parseInt(game.bw, 10);
    const engine = new ReversiEngine(game.map, game);
    await ReversiGames.update(game.id, {
        startedAt: new Date(),
        isStarted: true,
        black,
        crc32: engine.calcCrc32()
    });
    const fresh = await getGame(game.id);
    if (!fresh) return;
    if (engine.isEnded) {
        const winnerId = engine.winner === true ? black === 1 ? game.user1Id : game.user2Id : engine.winner === false ? black === 1 ? game.user2Id : game.user1Id : null;
        await endGame(fresh, winnerId, null);
        return;
    }
    await redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, fresh.timeLimitForEachTurn, "");
    await publishGame(game.id, "started", {
        game: await ReversiGames.packDetail(fresh)
    });
}
export async function putStone(gameId, user, pos, id) {
    const game = await getGame(gameId);
    if (!game || !game.isStarted || game.isEnded) return;
    if (game.user1Id !== user.id && game.user2Id !== user.id) return;
    const myColor = game.user1Id === user.id && game.black === 1 || game.user2Id === user.id && game.black === 2;
    const engine = restoreGame(game);
    if (engine.turn !== myColor || !engine.canPut(myColor, pos)) return;
    engine.putStone(pos);
    const logs = deserializeLogs(game.logs);
    const log = {
        time: Date.now(),
        player: myColor,
        operation: "put",
        pos
    };
    logs.push(log);
    const serialized = serializeLogs(logs);
    await ReversiGames.update(game.id, {
        logs: serialized,
        crc32: engine.calcCrc32()
    });
    await publishGame(game.id, "log", {
        ...log,
        id: id ?? null
    });
    const fresh = await getGame(game.id);
    if (!fresh) return;
    if (engine.isEnded) {
        const winnerId = engine.winner === true ? game.black === 1 ? game.user1Id : game.user2Id : engine.winner === false ? game.black === 1 ? game.user2Id : game.user1Id : null;
        await endGame(fresh, winnerId, null);
    } else if (engine.turn != null) {
        await redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? "1" : "0"}`, fresh.timeLimitForEachTurn, "");
    }
}
export async function surrender(gameId, user) {
    const game = await getGame(gameId);
    if (!game || game.isEnded) return;
    if (game.user1Id !== user.id && game.user2Id !== user.id) return;
    await endGame(game, game.user1Id === user.id ? game.user2Id : game.user1Id, "surrender");
}
export async function cancelGame(gameId, user) {
    const game = await getGame(gameId);
    if (!game || game.isStarted) return;
    if (game.user1Id !== user.id && game.user2Id !== user.id) return;
    await ReversiGames.delete(game.id);
    await publishGame(game.id, "canceled", {
        userId: user.id
    });
}
export async function checkTimeout(gameId) {
    const game = await getGame(gameId);
    if (!game || game.isEnded) return;
    const engine = restoreGame(game);
    if (engine.turn == null) return;
    const timer = await redisClient.exists(`reversi:game:turnTimer:${game.id}:${engine.turn ? "1" : "0"}`);
    if (timer === 0) {
        const winnerId = engine.turn ? game.black === 1 ? game.user2Id : game.user1Id : game.black === 1 ? game.user1Id : game.user2Id;
        await endGame(game, winnerId, "timeout");
    }
}
export async function checkCrc(gameId, crc32) {
    const game = await getGame(gameId);
    if (!game) return null;
    return crc32.toString() !== game.crc32 ? game : null;
}
export async function cleanupOpenGames() {
    await ReversiGames.delete({
        id: LessThan(genId(new Date(Date.now() - 1000 * 60 * 10))),
        isStarted: false,
        startedAt: IsNull()
    });
}
