const modulename = 'Player';
import cleanPlayerName from '@shared/cleanPlayerName';
import { DatabaseActionWarnType, DatabasePlayerType, DatabaseWhitelistApprovalsType } from '@modules/Database/databaseTypes';
import { cloneDeep, union } from 'lodash-es';
import { now } from '@lib/misc';
import { parsePlayerIds } from '@lib/player/idUtils';
import consoleFactory from '@lib/console';
import consts from '@shared/consts';
import type FxPlayerlist from '@modules/FxPlayerlist';
const console = consoleFactory(modulename);


/**
 * Base class for ServerPlayer and DatabasePlayer.
 * NOTE: player classes are responsible to every and only business logic regarding the player object in the database.
 * In the future, when actions become part of the player object, also add them to these classes.
 */
export class BasePlayer {
    displayName: string = 'unknown';
    pureName: string = 'unknown';
    ids: string[] = [];
    hwids: string[] = [];
    license: null | string = null; //extracted for convenience
    dbData: false | DatabasePlayerType = false;
    isConnected: boolean = false;

    constructor(readonly uniqueId: Symbol) { }

    /**
     * Mutates the database data based on a source object to be applied
     * FIXME: if this is called for a disconnected ServerPlayer, it will not clean after 120s
     */
    protected mutateDbData(srcData: object) {
        if (!this.license) throw new Error(`cannot mutate database for a player that has no license`);
        this.dbData = txCore.database.players.update(this.license, srcData, this.uniqueId);
    }

    /**
     * Returns all available identifiers (current+db)
     */
    getAllIdentifiers() {
        if (this.dbData && this.dbData.ids) {
            return union(this.ids, this.dbData.ids);
        } else {
            return [...this.ids];
        }
    }

    /**
     * Returns all available hardware identifiers (current+db)
     */
    getAllHardwareIdentifiers() {
        if (this.dbData && this.dbData.hwids) {
            return union(this.hwids, this.dbData.hwids);
        } else {
            return [...this.hwids];
        }
    }

    /**
     * Returns all actions related to all available ids
     * NOTE: theoretically ServerPlayer.setupDatabaseData() guarantees that DatabasePlayer.dbData.ids array
     *  will contain the license but may be better to also explicitly add it to the array here?
     */
    getHistory() {
        if (!this.ids.length) return [];
        return txCore.database.actions.findMany(
            this.getAllIdentifiers(),
            this.getAllHardwareIdentifiers()
        );
    }

    /**
     * Saves notes for this player.
     * NOTE: Techinically, we should be checking this.isRegistered, but not available in BasePlayer
     */
    setNote(text: string, author: string) {
        if (!this.license) throw new Error(`cannot save notes for a player that has no license`);
        this.mutateDbData({
            notes: {
                text,
                lastAdmin: author,
                tsLastEdit: now(),
            }
        });
    }

    /**
     * Saves the whitelist status for this player
     * NOTE: Techinically, we should be checking this.isRegistered, but not available in BasePlayer
     */
    setWhitelist(enabled: boolean) {
        if (!this.license) throw new Error(`cannot set whitelist status for a player that has no license`);
        this.mutateDbData({
            tsWhitelisted: enabled ? now() : undefined,
        });

        //Remove entries from whitelistApprovals & whitelistRequests
        const allIdsFilter = (x: DatabaseWhitelistApprovalsType) => {
            return this.ids.includes(x.identifier);
        }
        txCore.database.whitelist.removeManyApprovals(allIdsFilter);
        txCore.database.whitelist.removeManyRequests({ license: this.license });
    }
}


type PlayerDataType = {
    name: string,
    ids: string[],
    hwids: string[],
}

/**
 * Class to represent a player that is or was connected to the currently running server process.
 */
export class ServerPlayer extends BasePlayer {
    readonly #fxPlayerlist: FxPlayerlist;
    // readonly psid: string; //TODO: calculate player session id (sv mutex, netid, rollover id) here
    readonly netid: number;
    readonly tsConnected = now();
    readonly isRegistered: boolean;
    readonly #minuteCronInterval?: ReturnType<typeof setInterval>;
    // #offlineDbDataCacheTimeout?: ReturnType<typeof setTimeout>;

    constructor(
        netid: number,
        playerData: PlayerDataType,
        fxPlayerlist: FxPlayerlist
    ) {
        super(Symbol(`netid${netid}`));
        this.#fxPlayerlist = fxPlayerlist;
        this.netid = netid;
        this.isConnected = true;
        if (
            playerData === null
            || typeof playerData !== 'object'
            || typeof playerData.name !== 'string'
            || !Array.isArray(playerData.ids)
            || !Array.isArray(playerData.hwids)
        ) {
            throw new Error(`invalid player data`);
        }

        //Processing identifiers
        //NOTE: ignoring IP completely
        const { validIdsArray, validIdsObject } = parsePlayerIds(playerData.ids);
        this.license = validIdsObject.license;
        this.ids = validIdsArray;
        this.hwids = playerData.hwids.filter(x => {
            return typeof x === 'string' && consts.regexValidHwidToken.test(x);
        });

        //Processing player name
        const { displayName, pureName } = cleanPlayerName(playerData.name);
        this.displayName = displayName;
        this.pureName = pureName;

        //If this player is eligible to be on the database
        if (this.license) {
            this.#setupDatabaseData();
            this.isRegistered = !!this.dbData;
            this.#minuteCronInterval = setInterval(this.#minuteCron.bind(this), 60_000);
        } else {
            this.isRegistered = false;
        }
        console.log(167, this.isRegistered)
    }


    /**
     * Registers or retrieves the player data from the database.
     * NOTE: if player has license, we are guaranteeing license will be added to the database ids array
     */
    #setupDatabaseData() {
        if (!this.license || !this.isConnected) return;

        //Make sure the database is ready - this should be impossible
        if (!txCore.database.isReady) {
            console.error(`Players database not yet ready, cannot read db status for player id ${this.displayName}.`);
            return;
        }

        //Check if player is already on the database
        try {
            const dbPlayer = txCore.database.players.findOne(this.license);
            if (dbPlayer) {
                //Updates database data
                this.dbData = dbPlayer;
                this.mutateDbData({
                    displayName: this.displayName,
                    pureName: this.pureName,
                    tsLastConnection: this.tsConnected,
                    ids: union(dbPlayer.ids, this.ids),
                    hwids: union(dbPlayer.hwids, this.hwids),
                });
            } else {
                //Register player to the database
                console.log(`Registering '${this.displayName}' to players database.`);
                const toRegister = {
                    license: this.license,
                    ids: this.ids,
                    hwids: this.hwids,
                    displayName: this.displayName,
                    pureName: this.pureName,
                    playTime: 0,
                    tsLastConnection: this.tsConnected,
                    tsJoined: this.tsConnected,
                };
                txCore.database.players.register(toRegister);
                this.dbData = toRegister;
                console.verbose.ok(`Adding '${this.displayName}' to players database.`);

            }
            setImmediate(this.#sendInitialData.bind(this));
        } catch (error) {
            console.error(`Failed to load/register player ${this.displayName} from/to the database with error:`);
            console.dir(error);
        }
    }

    /**
     * Prepares the initial player data and reports to FxPlayerlist, which will dispatch to the server via command.
     * TODO: adapt to be used for admin auth and player tags.
     */
    #sendInitialData() {
        if (!this.isRegistered) return;
        if (!this.dbData) throw new Error(`cannot send initial data for a player that has no dbData`);

        let oldestPendingWarn: undefined | DatabaseActionWarnType;
        const actionHistory = this.getHistory();
        for (const action of actionHistory) {
            if (action.type !== 'warn' || action.revocation.timestamp !== null) continue;
            if (!action.acked) {
                oldestPendingWarn = action;
                break;
            }
        }

        if (oldestPendingWarn) {
            this.#fxPlayerlist.dispatchInitialPlayerData(this.netid, oldestPendingWarn);
        }
    }

    /**
     * Sets the dbData.
     * Used when some other player instance mutates the database and we need to sync all players 
     * with the same license.
     */
    syncUpstreamDbData(srcData: DatabasePlayerType) {
        this.dbData = cloneDeep(srcData)
    }

    /**
     * Returns a clone of this.dbData.
     * If the data is not available, it means the player was disconnected and dbData wiped to save memory,
     * so start an 120s interval to wipe it from memory again. This period can be considered a "cache"
     * FIXME: review dbData optimization, 50k players would be up to 50mb
     */
    getDbData() {
        if (this.dbData) {
            return cloneDeep(this.dbData);
        } else if (this.license && this.isRegistered) {
            const dbPlayer = txCore.database.players.findOne(this.license);
            if (!dbPlayer) return false;

            this.dbData = dbPlayer;
            // clearTimeout(this.#offlineDbDataCacheTimeout); //maybe not needed?
            // this.#offlineDbDataCacheTimeout = setTimeout(() => {
            //     this.dbData = false;
            // }, 120_000);
            return cloneDeep(this.dbData);
        } else {
            return false;
        }
    }


    /**
     * Updates dbData play time every minute
     */
    #minuteCron() {
        //FIXME: maybe use UIntXarray or mnemonist.Uint16Vector circular buffers to save memory
        //TODO: rough draft of a playtime tracking system written before note above
        // let list: [day: string, mins: number][] = [];
        // const today = new Date;
        // const currDay = today.toISOString().split('T')[0];
        // if(!list.length){
        //     list.push([currDay, 1]);
        //     return;
        // }
        // if(list.at(-1)![0] === currDay){
        //     list.at(-1)![1]++;
        // } else {
        //     //FIXME: move this cutoff to a const in the database or playerlist manager
        //     const cutoffTs = today.setUTCHours(0, 0, 0, 0) - 1000 * 60 * 60 * 24 * 28; 
        //     const cutoffIndex = list.findIndex(x => new Date(x[0]).getTime() < cutoffTs);
        //     list = list.slice(cutoffIndex);
        //     list.push([currDay, 1]);
        // }


        if (!this.dbData || !this.isConnected) return;
        try {
            this.mutateDbData({ playTime: this.dbData.playTime + 1 });
        } catch (error) {
            console.warn(`Failed to update playtime for player ${this.displayName}:`);
            console.dir(error);
        }
    }


    /**
     * Marks this player as disconnected, clears dbData (mem optimization) and clears minute cron
     */
    disconnect() {
        this.isConnected = false;
        // this.dbData = false;
        clearInterval(this.#minuteCronInterval);
    }
}


/**
 * Class to represent players stored in the database.
 */
export class DatabasePlayer extends BasePlayer {
    readonly isRegistered = true; //no need to check because otherwise constructor throws

    constructor(license: string, srcPlayerData?: DatabasePlayerType) {
        super(Symbol(`db${license}`));
        if (typeof license !== 'string') {
            throw new Error(`invalid player license`);
        }

        //Set dbData either from constructor params, or from querying the database
        if (srcPlayerData) {
            this.dbData = srcPlayerData;
        } else {
            const foundData = txCore.database.players.findOne(license);
            if (!foundData) {
                throw new Error(`player not found in database`);
            } else {
                this.dbData = foundData;
            }
        }

        //fill in data
        this.license = license;
        this.ids = this.dbData.ids;
        this.hwids = this.dbData.hwids;
        this.displayName = this.dbData.displayName;
        this.pureName = this.dbData.pureName;
    }

    /**
     * Returns a clone of this.dbData
     */
    getDbData() {
        return cloneDeep(this.dbData);
    }
}


export type PlayerClass = ServerPlayer | DatabasePlayer;
