import * as colors from 'colors';
import { mongoSystem } from '../mongo';
import { PlayerLogEvent, PlayerLogEvents } from './events/player.log.events';
import { ChatLogEvent, ChatLogEvents } from './events/chat.log.events';
import { PropertyEntity } from '../../entities/property.entity';
import { PropertyLogEvent, PropertyLogEvents } from './events/property.log.events';
import { FactionEntity } from '../../entities/faction.entity';
import { FactionLogEvent, FactionLogEvents } from './events/faction.log.events';
import { WhitelistLogEvent } from './events/whitelist.log.events';
import { AdminLogEvent } from './events/admin.log.events';
import { ItemEntity } from '../../entities/item.entity';
import { ItemLogEvent } from './events/item.log.events';
import { JSONStringifyRecursive } from '../../features/utility/JSONStringifyNoRecursive';
import { LawLogEvent } from './events/law.log.events';
import { VehicleLogEvent } from './events/vehicle.log.events';
import { VehicleSystem } from '../../features/vehicle';
import dayjs from 'dayjs';
import { CompanyEntity } from '../../entities/company.entity';
import { CompanyLogEvent } from './events/company.log.events';
import { VehicleEntity } from '../../entities/vehicle.entity';
import { ObjectLiteral } from 'typeorm';
import { DarkNetLogEvent } from './events/darknet.log.events';
import { DoorEntity } from '../../entities/door.entity';
import { DoorLogEvent } from './events/door.log.events';
import { PointToPointEntity } from '../../entities/point-to-point.entity';
import { PointToPointLogEvent } from './events/ptp.log.events';
import { FurnitureLogEvent } from './events/furniture.log.events';
import { FurnitureItemEntity } from '../../entities/furniture-item.entity';
import { mdcSystem } from '../../features/mdc';
import { MDCLogEvent } from './events/mdc.log.events';
import { DarknetOrderEntity } from '../../entities/darknet-order.entity';
import { GraffitiLogEvent } from './events/graffiti.log.events';
import { GraffitiEntity } from '../../entities/graffiti.entity';
import { PetLogEvent } from './events/pet.log.events';
import { PetEntity } from '../../entities/pet.entity';
import { BlackjackLogEvent } from './events/blackjack.log.events';
import { CorpseLogEvent } from './events/corpse.log.event';
import { MarkerLogEvent } from './events/marker.log.events';
import { InfoMarkerEntity } from '../../entities/info-markers.entity';
import { RouletteLogEvent } from './events/roulette.log.event';

colors.enable();

interface LogCollection {
    name: string;
    permission: string;
    hasEvents?: boolean;
    fields: {
        name: string;
        type: string;
        trace?: string;
        default?: boolean;
        required?: boolean;
    }[];
}

export type DarknetOrderData = {
    id: number;
    name: string;
    faction: {
        id: number;
        name: string;
    };
};

export type DarknetItems = {
    type: {
        id: number;
        name: string;
    };
    price: number;
    oocPrice: number;
    amount: number;
    unit: string;
}[];

export class LogSystem {
    lastInitTime: number;
    firstInitTime: number;

    static Collections: LogCollection[] = [
        {
            name: 'players',
            permission: 'LOG_VIEW_PLAYERS',
            hasEvents: true,
            fields: [
                { name: 'player', type: 'player', default: true, required: true },
                { name: 'ip', trace: 'player.ip', type: 'string' },
                { name: 'log', type: 'string', default: true },
                { name: 'target', type: 'player' },
            ],
        },
    ];

    init(): void {
        this.lastInitTime = +new Date();
        this.firstInitTime = +new Date();
    }

    initLog(feature: string, content: string): void {
        const now = +new Date();
        const time = ((now - this.lastInitTime) / 1000).toFixed(1).padStart(4, ' ') + 's';
        const totalTime = ((now - this.firstInitTime) / 1000).toFixed(1).padEnd(4, ' ') + 's';
        LogSystem.Collect(`${(totalTime + ' | ' + time).grey} [init:${feature}]`.green + `: ${content.yellow}`);
        this.lastInitTime = now;
    }

    static Log(feature: string, content: string): void {
        LogSystem.Collect(`[${feature}]`.green + `: ${content.reset}`);
        // LogSystem.Collect(`LogSystem.Log is for debugging only, all relevant data needs to be stored using event functions`.red);
    }

    static ComposePlayerObject(player?: PlayerMp, options?: { state?: boolean }): Record<string, any> | undefined {
        if (!player) {
            return undefined;
        }

        const playerObject: {
            gameId: number;
            ip: string;
            sessionId?: number;
            accountId?: number;
            accountName?: string;
            characterId?: number;
            characterName?: string;
            state?: PlayerState;
        } = {
            gameId: player.id,
            ip: player.ip,
        };

        if (player.session) {
            playerObject.sessionId = player.session.id;
        }

        if (player.account) {
            playerObject.accountId = player.account.id;
            playerObject.accountName = player.account.name;
        }

        if (player.character) {
            playerObject.characterId = player.character.id;
            playerObject.characterName = player.character.name;
        }

        if (options?.state) {
            playerObject.state = player.state;
        }

        return JSON.parse(JSONStringifyRecursive(playerObject));
    }

    static ComposeGraffitiEntityObject(graffiti: GraffitiEntity): Record<string, any> | undefined {
        const graffitiObject: {
            id: number;
            content: string;
            font: string;
            colour: string;
            opacity: number;
            destroyed: Date;
            creator_id: number;
        } = {
            id: graffiti.id,
            content: graffiti.content,
            font: graffiti.font,
            colour: graffiti.colour,
            opacity: graffiti.opacity,
            destroyed: graffiti.destroyed,
            creator_id: graffiti.creator_id,
        };

        return JSON.parse(JSONStringifyRecursive(graffitiObject));
    }

    static ComposeDoorEntityObject(door: DoorEntity): Record<string, any> | undefined {
        const doorObject: {
            id: number;
            name: string;
            permission: string;
            lockType: string;
            property: {
                id: number;
                name: string;
            } | null;
        } = {
            id: door.id,
            name: door.name,
            permission: door.permission_name || 'null',
            lockType: door.lock_type,
            property: door.property
                ? {
                      id: door.property.id,
                      name: door.property.getName(),
                  }
                : null,
        };

        return JSON.parse(JSONStringifyRecursive(doorObject));
    }

    static ComposePTPEntityObject(ptp: PointToPointEntity): Record<string, any> | undefined {
        const ptpObject: {
            id: number;
            name: string;
            permission: string | null;
            lockStatus: boolean;
        } = {
            id: ptp.id,
            name: ptp.name,
            permission: ptp.permission,
            lockStatus: ptp.locked,
        };

        return JSON.parse(JSONStringifyRecursive(ptpObject));
    }

    static ComposePlayerPosition(player: PlayerMp): { x: number; y: number; z: number; dimension: number } {
        return {
            x: Math.round(player.position.x * 1000) / 1000,
            y: Math.round(player.position.y * 1000) / 1000,
            z: Math.round(player.position.z * 1000) / 1000,
            dimension: player.dimension,
        };
    }

    static ComposePlayerName(player: PlayerMp): string {
        if (!mp.players.exists(player)) {
            return 'Unknown Player';
        }
        let name = player.name;
        if (player.account) {
            name = player.account.name;
            if (player.character) {
                name = `${name} - ${player.character.name}`;
            }
        }
        name = `${name} [${player.id}]`;
        return name;
    }

    static ComposeFurnitureItemObject(item: FurnitureItemEntity): Record<string, any> {
        const itemObject = {
            id: item.id,
            created: mdcSystem.formatDate(item.created),
            creator: item.created_by_character,
            price: item.price,
            name: item.name,
            tag: item.tag,
            inventory:
                item.has_inventory && item.inventory
                    ? {
                          inventory_id: item.inventory.id,
                          items: item.inventory.items.map((item) => {
                              return {
                                  id: item.id,
                                  type: item.type.name,
                              };
                          }),
                      }
                    : false,
            prison_cell: item.prison_cell_id,
            property_id: item.property_id,
            position: {
                x: item.x,
                y: item.y,
                z: item.z,
                rx: item.rx,
                ry: item.ry,
                rz: item.rz,
            },
        };

        return JSON.parse(JSONStringifyRecursive(itemObject));
    }

    static ComposeVehicleObject(vehicle?: VehicleMp): Record<string, any> {
        if (!vehicle) {
            return {};
        }

        const speed = Math.floor(VehicleSystem.GetVehicleMph(vehicle));
        const vehicleObject: VehicleLogComposition = {
            gameId: vehicle.id,
            model: VehicleSystem.GetVehicleNameFromHash(vehicle.model) || String(vehicle.model),
            plate: vehicle.numberPlate,
            bodyHealth: vehicle.bodyHealth,
            engineHealth: vehicle.engineHealth,
            occupants: LogSystem.ComposeVehicleOccupants(vehicle),
            speedInMph: speed,
        };

        if (vehicle.vehicleEntity) {
            vehicleObject.id = vehicle.vehicleEntity.id;
            if (vehicle.vehicleEntity.owner) {
                vehicleObject.owner = vehicle.vehicleEntity.owner;
            }
        }

        return JSON.parse(JSONStringifyRecursive(vehicleObject));
    }

    static ComposeVehiclePosition(vehicle: VehicleMp): { x: number; y: number; z: number; heading: number; dimension: number } {
        return {
            x: Math.round(vehicle.position.x * 1000) / 1000,
            y: Math.round(vehicle.position.z * 1000) / 1000,
            z: Math.round(vehicle.position.z * 1000) / 1000,
            heading: Math.round(vehicle.heading),
            dimension: vehicle.dimension,
        };
    }

    static ComposeVehicleOccupants(vehicle: VehicleMp): any {
        const occupants: {
            seatId: number;
            player: any;
        }[] = [];

        vehicle.getVehicleOccupants().forEach((player) => {
            const composed = LogSystem.ComposePlayerObject(player);
            occupants.push({
                seatId: player.seat,
                player: composed,
            });
        });

        return JSON.parse(JSONStringifyRecursive(occupants));
    }

    static ComposeVehicleName(vehicle: VehicleMp | VehicleEntity): string {
        let name = VehicleSystem.GetVehicleName(vehicle);
        if (!(vehicle instanceof VehicleEntity) && vehicle.vehicleEntity) {
            name += ` (#${vehicle.vehicleEntity.id}`;
            if (vehicle.vehicleEntity.owner) {
                name += `, ${vehicle.vehicleEntity.owner})`;
            } else {
                name += ')';
            }

            name += ` [${vehicle.id}]`;
        } else if (vehicle instanceof VehicleEntity) {
            name += ` (Entity #${vehicle.id})`;
        } else {
            name += ` [${vehicle.id}]`;
        }

        return name;
    }

    static async PlayerEvent(player: PlayerMp, event: PlayerLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
        await mongoSystem.insertOne('logs', 'players', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            event,
            log,
            position: LogSystem.ComposePlayerPosition(player),
            data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
        });

        LogSystem.Collect(`[Player:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
    }

    static async GraffitiEvent(graffiti: GraffitiEntity, event: GraffitiLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
        await mongoSystem.insertOne('logs', 'graffiti', {
            date: new Date(),
            graffiti: graffiti ? LogSystem.ComposeGraffitiEntityObject(graffiti) : undefined,
            event,
            log,
            data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
        });

        LogSystem.Collect(`[Graffiti:${event}]`.green + ` ${log.reset}`);
    }

    static async VehicleEvent(vehicle: VehicleMp | VehicleEntity, event: VehicleLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'vehicles', {
            date: new Date(),
            vehicle: vehicle instanceof VehicleEntity ? { id: vehicle.id } : LogSystem.ComposeVehicleObject(vehicle),
            event,
            log,
            position: vehicle instanceof VehicleEntity ? null : LogSystem.ComposeVehiclePosition(vehicle),
            player: LogSystem.ComposePlayerObject(player),
        });

        LogSystem.Collect(`[Vehicle:${event}]`.green + `: ${LogSystem.ComposeVehicleName(vehicle).yellow} ${log.reset}`);
    }

    static async ChatEvent(player: PlayerMp, event: ChatLogEvent, log: string, target?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'chat', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            event,
            log,
            position: LogSystem.ComposePlayerPosition(player),
            target: LogSystem.ComposePlayerObject(target),
        });

        LogSystem.Collect(`[Chat:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
    }

    static async BlackjackEvent(event: BlackjackLogEvent, tableId: number, roundId: string, log: string, player?: PlayerMp) {
        await mongoSystem.insertOne('logs', 'blackjack', {
            date: new Date(),
            tableId,
            roundId,
            log,
            player: LogSystem.ComposePlayerObject(player),
            position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
        });

        LogSystem.Collect(`[Blackjack:${event}]`.green + `: ${tableId}/${roundId} ${log.reset}`);
    }

    static async RouletteEvent(event: RouletteLogEvent, tableId: number, gameId: string, log: string, player?: PlayerMp) {
        await mongoSystem.insertOne('logs', 'roulette', {
            date: new Date(),
            tableId,
            gameId,
            log,
            player: LogSystem.ComposePlayerObject(player),
            position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
        });

        LogSystem.Collect(`[Roulette:${event}]`.green + `: ${tableId}/${gameId} ${log.reset}`);
    }

    static async CorpseEvent(event: CorpseLogEvent, corpseId: number, log: string, player?: PlayerMp) {
        await mongoSystem.insertOne('logs', 'corpses', {
            date: new Date(),
            corpseId,
            log,
            player: LogSystem.ComposePlayerObject(player),
            position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
        });

        LogSystem.Collect(`[Corpse:${event}]`.green + `: ${corpseId} ${log.reset}`);
    }

    static async MarkerEvent(event: MarkerLogEvent, marker: InfoMarkerEntity, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'marker', {
            date: new Date(),
            marker,
            log,
            player: LogSystem.ComposePlayerObject(player),
            position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
        });

        LogSystem.Collect(`[Marker:${event}]`.green + `: ${marker.id} ${log.reset}`);
    }

    static async ItemEvent(item: ItemEntity, event: ItemLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'items', {
            date: new Date(),
            item,
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
            position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
        });

        LogSystem.Collect(`[Item:${event}]`.green + `: ${item.inventory_id}/${item.id} ${log.reset}`);
    }

    static async CommandEvent(player: PlayerMp, command: string | null, log: string): Promise<void> {
        await mongoSystem.insertOne('logs', 'commands', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            command,
            log,
            position: LogSystem.ComposePlayerPosition(player),
        });

        LogSystem.Collect(`[Command:${command}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
    }

    static async PropertyEvent(property: PropertyEntity, event: PropertyLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'properties', {
            date: new Date(),
            property: {
                id: property.id,
            },
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[Property(${property.id}):${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async PetEvent(pet: PetEntity, event: PetLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'pets', {
            date: new Date(),
            pet: {
                id: pet.id,
            },
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[Pet(${pet.id}):${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async MDCEvent(player: PlayerMp, event: MDCLogEvent, log: string, data?: any): Promise<void> {
        await mongoSystem.insertOne('logs', 'mdc', {
            date: new Date(),
            event,
            player: LogSystem.ComposePlayerObject(player),
            faction: player.character?.faction
                ? {
                      id: player.character.faction.id,
                  }
                : undefined,
            log,
            data: !data ? undefined : JSON.parse(JSONStringifyRecursive(data)),
        });

        const eventLog = `[MDC:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;

        LogSystem.Collect(eventLog);
    }

    static async FurnitureEvent(
        player: PlayerMp,
        event: FurnitureLogEvent,
        dimension: number,
        log: string,
        furnitureItem?: FurnitureItemEntity,
        property?: PropertyEntity,
    ): Promise<void> {
        await mongoSystem.insertOne('logs', 'furniture', {
            date: new Date(),
            event,
            log,
            dimension,
            player: LogSystem.ComposePlayerObject(player),
            item: furnitureItem ? LogSystem.ComposeFurnitureItemObject(furnitureItem) : undefined,
            property: property
                ? {
                      id: property.id,
                      currentFurnitureCount: property.furnitureItems ? property.furnitureItems.length : 0,
                  }
                : undefined,
        });

        const eventLog = `[Furniture:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;

        LogSystem.Collect(eventLog);
    }

    static async FactionEvent(faction: FactionEntity, event: FactionLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'factions', {
            date: new Date(),
            faction: {
                id: faction.id,
            },
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[Faction(${faction.id}):${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async CompanyEvent(company: CompanyEntity, event: CompanyLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'companies', {
            date: new Date(),
            company: {
                id: company.id,
            },
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[Company(${company.id}):${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async Error(system: string, event: string, log: string, trace: any, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'errors', {
            date: new Date(),
            system,
            event,
            log,
            trace: JSONStringifyRecursive(trace),
            player: LogSystem.ComposePlayerObject(player, { state: true }),
        });
        LogSystem.Collect(`[Error:${system}/${event}]`.red + ` ${log.red}`);
        console.error(trace);
    }

    static async WhitelistEvent(event: WhitelistLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'whitelist', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            event,
            log,
        });

        let eventLog = `[Whitelist:${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async AdminLog(event: AdminLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'admin', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            event,
            log,
            target: LogSystem.ComposePlayerObject(target),
        });

        let eventLog = `[Admin:${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }
        if (target) {
            eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async DarkNetLog(event: DarkNetLogEvent, log: string, metadata?: DarknetOrderData, order?: DarknetOrderEntity, item?: DarknetItems): Promise<void> {
        await mongoSystem.insertOne('logs', 'darknet', {
            date: new Date(),
            event,
            log,
            metadata,
            order,
            item,
        });

        const eventLog = `[DarkNet:${event}]`.green + `: ${log}`;

        LogSystem.Collect(eventLog);
    }

    static async DoorLog(door: DoorEntity, event: DoorLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'door', {
            date: new Date(),
            door: LogSystem.ComposeDoorEntityObject(door),
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[DoorSystem:${event}]`.green + `: Door #${door.id}`.yellow + ` ${log.reset}`;

        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async PTPLog(ptp: PointToPointEntity, event: PointToPointLogEvent, log: string, player?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'point_to_point', {
            date: new Date(),
            ptp: LogSystem.ComposePTPEntityObject(ptp),
            event,
            log,
            player: LogSystem.ComposePlayerObject(player),
        });

        let eventLog = `[PointToPoint:${event}]`.green + `: PTP #${ptp.id}`.yellow + ` ${log.reset}`;

        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static async LawLog(event: LawLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
        await mongoSystem.insertOne('logs', 'law', {
            date: new Date(),
            player: LogSystem.ComposePlayerObject(player),
            event,
            log,
            target: LogSystem.ComposePlayerObject(target),
        });

        let eventLog = `[Law:${event}]`.green + `: ${log}`;
        if (player) {
            eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
        }
        if (target) {
            eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
        }

        LogSystem.Collect(eventLog);
    }

    static GetLogEvents(): {
        players: string[];
        properties: string[];
        chat: string[];
        factions: string[];
    } {
        return {
            players: Object.keys(PlayerLogEvents),
            properties: Object.keys(PropertyLogEvents),
            chat: Object.keys(ChatLogEvents),
            factions: Object.keys(FactionLogEvents),
        };
    }

    static Collect(raw: string): void {
        const now = dayjs(new Date());
        const timeFormat = now.format('hh:mm:ss');

        console.log('['.white + `${timeFormat}`.grey + '] '.white + raw);
    }
}

process.on('uncaughtException', (error: Error, origin: string) => {
    LogSystem.Error('Node.js', 'uncaughtException', 'Uncaught Exception', { origin, error });
});

export const logSystem = new LogSystem();
