import dayjs from 'dayjs';
import { getConnection, getRepository, MoreThan } from 'typeorm';
import { AuthTokenEntity } from '../../entities/auth-token.entity';
import { CharacterEntity } from '../../entities/character.entity';
import { AttributesEntity } from '../../entities/character-attributes.entity';
import { LogSystem } from '../../core/log';
import { FeatureSystem } from '../../core/feature-system';
import { swrpApiSystem } from '../sw-rp-api';
import { sessionSystem } from '../session';
import { playerSystem } from '../player';
import { permissionSystem } from '../permission';
import { factionSystem } from '../faction';
import { ChatSystem } from '../chat';
import { Gender } from '../../../shared/const/gender.const';

export class LoginSystem extends FeatureSystem {
    constructor() {
        super();
        mp.events.add('playerJoin',         this.playerJoin.bind(this));
        mp.events.add('playerQuit',         this.playerQuit.bind(this));
        mp.events.add('login:init',         this.initLogin.bind(this));
        mp.events.add('login:attempt',      this.loginAttempt.bind(this));
        mp.events.add('login:character',    this.loginCharacter.bind(this));
        mp.events.add('login:spawn',        this.loginSpawn.bind(this));
        mp.events.add('loginSuccess',       this.loginSuccess.bind(this));
    }

    static IsLoggedIn(player: PlayerMp): player is PlayerMpLoggedIn {
        return !!(player.character && player.account && player.session);
    }

    // -------------------------------------------------------------------------
    // Player join / quit
    // -------------------------------------------------------------------------

    async playerJoin(player: PlayerMp): Promise<void> {
        player.setDimension(1); // NoLogIn routing bucket
        LogSystem.PlayerEvent(player, 'join', `joined the server`);
    }

    playerQuit(player: PlayerMp): void {
        if (player.state.loginTimeOut) {
            clearTimeout(player.state.loginTimeOut);
        }
    }

    // -------------------------------------------------------------------------
    // login:init — client sends stored token; validate or show form
    // -------------------------------------------------------------------------

    async initLogin(player: PlayerMp, token: string): Promise<void> {
        if (!token || !token.length) {
            return player.call('login:show', []);
        }

        const tokenRepo = getRepository(AuthTokenEntity);
        const activeToken = await tokenRepo.findOne({
            where: {
                persistent: true,
                expires: MoreThan(new Date()),
                token,
            },
            order: { id: 'DESC' },
        }).catch(() => null);

        if (!activeToken) {
            return player.call('login:show', []);
        }

        const validatedUser = await swrpApiSystem.validate(activeToken.token);
        if (!validatedUser || validatedUser.id !== activeToken.account_id) {
            return player.call('login:show', []);
        }

        const alreadyOnline = mp.players.toArray().find((p) => p.account?.id === activeToken.account_id);
        if (alreadyOnline) {
            return player.call('login:show', []);
        }

        player.account = validatedUser;
        player.setVariable('accountName', activeToken.account_username);

        await getRepository(AuthTokenEntity)
            .update(activeToken.id, { last_used: () => 'NOW()' })
            .catch(() => {});

        LogSystem.PlayerEvent(player, 'login', `auto-login via token #${activeToken.id}`);

        mp.events.call('loginSuccess', player);
        player.call('login:success', []);
    }

    // -------------------------------------------------------------------------
    // login:attempt — manual username/password login
    // -------------------------------------------------------------------------

    async loginAttempt(
        player: PlayerMp,
        username: string,
        password: string,
        otp: string | null,
        remember: boolean,
    ): Promise<void> {
        let response;
        try {
            response = await swrpApiSystem.authenticate(username, password, Boolean(remember), otp);
        } catch (err) {
            console.error(err);
            return player.call('login:error', ['System Error']);
        }

        if (response.result === 'otp') {
            return player.call('login:otp', [false]);
        }

        if (response.result === 'error') {
            player.state.loginAttempts = (player.state.loginAttempts ?? 0) + 1;
            LogSystem.PlayerEvent(player, 'login_fail', `invalid login for ${username}`);
            if (player.state.loginAttempts >= 5) {
                player.kick('Too many invalid login attempts');
                return;
            }
            if (otp) player.call('login:otp', [true]);
            return player.call('login:error', [response.reason]);
        }

        if (response.result !== 'success') {
            return player.call('login:error', ['Server Error']);
        }

        const alreadyOnline = mp.players.toArray().find((p) => p.account?.id === response.account.id);
        if (alreadyOnline) {
            return player.call('login:error', ["You're already online."]);
        }

        const session = await getConnection()
            .createQueryBuilder()
            .insert()
            .into(AuthTokenEntity)
            .values([{
                account_id:       response.account.id,
                account_username: response.account.name,
                token:            response.token,
                expires:          dayjs().add(1, remember ? 'week' : 'day').toDate(),
                persistent:       Boolean(remember),
                serial:           player.socialClub || 'fivem',
                socialclub_id:    player.socialClub || 'fivem',
                ip:               player.ip,
            }])
            .execute()
            .catch(() => null);

        if (!session || !session.identifiers[0].id) {
            return player.call('login:error', ['Server Error']);
        }

        player.account = response.account;
        player.setVariable('accountName', response.account.name);

        LogSystem.PlayerEvent(player, 'login', `logged in as ${username}`);

        mp.events.call('loginSuccess', player);
        player.call('login:success', [response.token]);
    }

    // -------------------------------------------------------------------------
    // loginSuccess — after auth, load character list
    // -------------------------------------------------------------------------

    async loginSuccess(player: PlayerMpLoggedIn): Promise<void> {
        if (!mp.players.exists(player)) return;

        await sessionSystem.createSession(player);

        const characters = await getRepository(CharacterEntity)
            .createQueryBuilder('character')
            .select(['character.id', 'character.name', 'character.level', 'faction.logo', 'character.killed', 'character.last_online', 'appearanceData.id'])
            .where({ account_id: player.account.id })
            .leftJoin('character.faction', 'faction')
            .leftJoin('character.appearanceData', 'appearanceData')
            .getMany()
            .catch(() => [] as CharacterEntity[]);

        if (!characters.length) {
            player.kick("You don't have any characters. Create one at southwest-roleplay.dev first.");
            return;
        }

        if (characters.length === 1) {
            await this.loadCharacter(player, characters[0].id);
            return;
        }

        const formatted = characters.map((c) => ({
            id: c.id,
            name: c.name,
            level: c.level,
            faction: c.faction ? { logo: c.faction.logo } : undefined,
            killed: c.killed,
            hasAppearance: Boolean(c.appearanceData?.id),
            lastOnline: c.last_online ? dayjs(c.last_online).format('DD/MM/YYYY') : null,
        }));

        player.call('login:characters', [JSON.stringify(formatted)]);
    }

    // -------------------------------------------------------------------------
    // login:character — player selected a character from the list
    // -------------------------------------------------------------------------

    async loginCharacter(player: PlayerMp, characterId: number): Promise<void> {
        if (!player.account) return;
        await this.loadCharacter(player as PlayerMpLoggedIn, characterId);
    }

    // -------------------------------------------------------------------------
    // login:spawn — player chose a public spawn point (no saved spawn)
    // -------------------------------------------------------------------------

    async loginSpawn(player: PlayerMp, spawnId: number): Promise<void> {
        if (!player.character?.id) return;

        const validSpawn = playerSystem.publicSpawns.find((s) => s.id === spawnId);
        if (!validSpawn) return;

        await getRepository(CharacterEntity)
            .update(player.character.id, { spawn: `public:${spawnId}` })
            .catch(() => {});

        player.character.spawn = `public:${spawnId}`;
        await this.loadCharacter(player as PlayerMpLoggedIn, player.character.id);
    }

    // -------------------------------------------------------------------------
    // loadCharacter — the main character setup + spawn
    // -------------------------------------------------------------------------

    async loadCharacter(player: PlayerMpLoggedIn, characterId: number): Promise<boolean> {
        const character = await getRepository(CharacterEntity)
            .createQueryBuilder('character')
            .where({ account_id: player.account.id, id: characterId })
            .leftJoinAndSelect('character.appearanceData', 'appearanceData')
            .leftJoinAndSelect('character.bankAccounts', 'bankAccounts')
            .leftJoinAndSelect('character.outfits', 'outfits')
            .leftJoinAndSelect('character.phones', 'phones')
            .leftJoinAndSelect('character.faction', 'faction')
            .getOne()
            .catch(() => null);

        if (!character) {
            player.call('login:characterError', ["Couldn't load the character."]);
            return false;
        }

        player.character = character;

        // Load attributes
        player.character.attributes = (
            await getRepository(AttributesEntity)
                .findOne({ character_name: character.name })
                .catch(() => undefined)
        )?.attributes;

        // No spawn saved — show spawn selection to player
        if (!player.character.spawn) {
            const spawns = playerSystem.publicSpawns.slice().sort((a, b) => {
                if (a.recommended && !b.recommended) return -1;
                if (!a.recommended && b.recommended) return 1;
                return a.name.localeCompare(b.name);
            });
            player.call('login:spawns', [JSON.stringify(spawns)]);
            return true;
        }

        // Set player identity
        player.setVariable('icName', character.name.replace('_', ' '));
        player.setVariable('characterName', character.name.replace('_', ' '));
        player.setVariable('characterId', characterId);
        player.setVariable('factionId', character.faction_id);

        // Basic faction wiring
        if (character.faction_id) {
            player.character.faction = factionSystem.getFactionFromId(character.faction_id) ?? character.faction;
            if (player.character.faction) {
                player.character.factionRank = player.character.faction.ranks?.find(
                    (r) => r.id === character.faction_rank,
                );
            }
        }

        // Set gender-appropriate freemode model via client event
        const modelName = `mp_${Number(character.gender) === Gender.MALE ? 'm' : 'f'}_freemode_01`;
        player.call('_mp:setModel', [modelName]);

        // Notify welcome
        player.call('_mp:notify', [`Welcome back, ${player.account.name}`]);

        // Permissions
        await permissionSystem.loadPlayerPermissions(player).catch(() => {});

        // Move to public routing bucket
        player.setDimension(0);

        // Spawn at saved location
        await playerSystem.spawn(player, true);

        // Update session
        await sessionSystem.setSessionCharacter(player).catch(() => {});

        // Update last online
        await getRepository(CharacterEntity)
            .update(character.id, { last_online: new Date() })
            .catch(() => {});

        LogSystem.PlayerEvent(player, 'character_loaded', `${player.account.name} selected "${character.name}"`);

        player.call('login:characterLoaded', []);
        mp.events.call('onCharacterLoaded', player);

        return true;
    }
}

export const loginSystem = new LoginSystem();
