import { ObjectId } from "mongodb";
import MissionType from "../../../types/MissionType";
import DBFleet from "../../../types/db/DBFleet";
import { updateFleet } from "../../db/fleet";
import { sendMail } from "../../db/mails";
import { getAllShips } from "../../db/ships";
import getDistanceBetween from "../../utils/getDistanceBetween";
import { getRandomInRange, weightedRandom } from "../../utils/math";
import { Sector } from "./LocationManager";
import { Planet } from "./PlanetManager";
import SystemManager from "./SystemManager";
import { getAllDefenses } from "../../db/defenses";

export type Fleet = {
    id: ObjectId,
    source: Planet | SystemManager,
    destination: Planet | SystemManager | Sector,
    departureTime: Date,
    arrivalTime: Date,
    returning: boolean,
    mission: MissionType,
    ships: Array<{ id: string, amount: number }>,
    cargo: Array<{ id: string, amount: number }>,
    additionalData?: string
}

export type BattleFleet = {
    id: string,
    hitpoints: number,
    attack: number,
    defense: number
}

export default class FleetManager {
    data: Fleet;

    constructor(id: ObjectId, source: Planet | SystemManager, destination: Planet | SystemManager | Sector, departureTime: Date, arrivalTime: Date, returning: boolean, mission: MissionType, ships: Array<{ id: string, amount: number }>, cargo: Array<{ id: string, amount: number }>, additionalData?: string) {
        this.data = {
            id,
            source,
            destination,
            departureTime,
            arrivalTime,
            returning,
            mission,
            ships,
            cargo,
            additionalData
        }
    }

    isSourcePlanet(): this is { data: { source: Planet } } {
        return 'resources' in this.data.source;
    }

    isSourceSystem(): this is { data: { source: SystemManager } } {
        return 'structures' in this.data.source;
    }

    isDestinationPlanet(): this is { data: { destination: Planet } } {
        return 'resources' in this.data.destination;
    }

    isDestinationSystem(): this is { data: { destination: SystemManager } } {
        return 'structures' in this.data.destination;
    }

    getSourceOwner() {
        if(this.isSourcePlanet()) return this.data.source.system.data.ownedBy;
        if(this.isSourceSystem()) return this.data.source.data.ownedBy;
    }

    getDestinationOwner() {
        if(this.isDestinationPlanet()) return this.data.destination.system.data.ownedBy;
        if(this.isDestinationSystem()) return this.data.destination.data.ownedBy;
    }

    async checkStatus(): Promise<{ finished: boolean, fleet: Fleet }> {
        if(this.data.arrivalTime.getTime() < Date.now()) {
            const finished = await this.finish();
            return { finished, fleet: this.data };
        }

        return { finished: false, fleet: this.data };
    }

    async finish() {
        if(this.data.returning) {
            for(const ship of this.data.ships) {
                this.data.source.ships.addShips(ship.id, ship.amount);
            }
            await this.data.source.resources.updateAmount(this.data.cargo);
            await this.data.source.ships.sync();
            await this.data.source.resources.sync();
            await sendMail(
                null,
                this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id,
                this.data.arrivalTime,
                "Fleet Returned",
                `Your fleet from ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `${this.data.destination.name} ${"expedition" in this.data.destination ? "sector" : "planet"}`} has returned.\n
                Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n
                Cargo: ${this.data.cargo.length > 0 ? this.data.cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}`
            );
            return true;
        } else {
            switch(this.data.mission) {
                case 'ATTACK':
                    if(!("expedition" in this.data.destination)) {
                        const enemyShips = this.data.destination.ships.ships;
                        return await this.battleResults(enemyShips.map(ship => { return { id: ship.data.id, amount: ship.amount } }), this.data.destination.defenses.defenses.map(defense => { return { id: defense.data.id, amount: defense.amount } }));
                    } else {
                        throw new Error("Cannot attack sector.");
                    }
                case 'TRANSPORT':
                    await (this.data.destination as Planet | SystemManager).resources.updateAmount(this.data.cargo);
                    await (this.data.destination as Planet | SystemManager).resources.sync();
                    const cargo = JSON.parse(JSON.stringify(this.data.cargo)) as Array<{ id: string, amount: number }>;
                    this.data.cargo = [];
                    const arrived = new Date(this.data.arrivalTime);
                    await this.initiateReturn();
                    await sendMail(
                        null,
                        this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id,
                        arrived,
                        "Fleet Arrived",
                        `Your fleet has arrived at ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `planet ${this.data.destination.name}`}.\n
                        Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n
                        Cargo delivered: ${cargo.length > 0 ? cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}\n
                        Fleet will return at ${this.data.arrivalTime}`
                    );
                    return false;
                case 'TRANSFER':
                    await (this.data.destination as Planet | SystemManager).resources.updateAmount(this.data.cargo);
                    await (this.data.destination  as Planet | SystemManager).resources.sync();
                    for(const ship of this.data.ships) {
                        (this.data.destination as Planet | SystemManager).ships.addShips(ship.id, ship.amount);
                    }
                    await (this.data.destination as Planet | SystemManager).ships.sync();
                    await sendMail(
                        null,
                        this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id,
                        this.data.arrivalTime,
                        "Fleet Arrived",
                        `Your fleet has arrived at ${this.data.destination instanceof SystemManager ? `${this.data.destination.data.name} system` : `planet ${this.data.destination.name}`}.\n
                        Ships: ${this.data.ships.map(ship => `${ship.amount} ${ship.id}`).join(', ')}\n
                        Cargo delivered: ${this.data.cargo.length > 0 ? this.data.cargo.map(cargo => `${cargo.amount} ${cargo.id}`).join(', ') : 'None'}\n
                        Ships will stay at the destination.`
                    );
                    return true;
                case 'EXPEDITION':
                    await this.expeditionResults();

                    return false;
                case 'MINE':
                    const system = this.data.destination as SystemManager;
                    const asteroid = system.asteroids.asteroids.find(a => a.id.equals(this.data.additionalData ?? ""));
                    if(!asteroid) throw new Error("Asteroid not found.");
                    for(const res of asteroid.resources) {
                        const resource = this.data.cargo.find(r => r.id === res.id);
                        if(!resource) this.data.cargo.push(res);
                        else resource.amount += res.amount;
                    }

                    await this.initiateReturn();
                    await sendMail(
                        null,
                        this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id,
                        this.data.arrivalTime,
                        "Asteroid mined",
                        `Your fleet has arrived at AS-${this.data.additionalData} asteroid.\n
                        Following resources were added to the cargo inventory:\n${asteroid.resources.map(r => `${r.id} - ${r.amount}`).join('\n')}\n
                        Fleet will return at ${this.data.arrivalTime}`
                    );

                    system.asteroids.asteroids = system.asteroids.asteroids.filter(a => !a.id.equals(asteroid.id));
                    await system.asteroids.sync();
                    return false;
            }
        }
    }

    async initiateReturn(isByPlayer: boolean = false) {
        this.data.returning = true;
        const elapsedTime = Date.now() - this.data.departureTime.getTime();
        this.data.departureTime = isByPlayer ? new Date() : new Date(this.data.arrivalTime);
        const travelTime = getDistanceBetween(this.data.destination, this.data.source);
        this.data.arrivalTime = new Date(this.data.departureTime.getTime() + (isByPlayer ? elapsedTime : 1000 * travelTime));
        await this.sync();
    }

    private async sendMail(user: ObjectId, title: string, description: string) {
        await sendMail(
            null,
            user,
            this.data.arrivalTime,
            title,
            description
        );
    }

    async battleResults(enemyFleet: { id: string, amount: number }[], enemyDefenses: { id: string, amount: number }[] = []) {
        const allShips = await getAllShips();
        const allDefenses = await getAllDefenses();

        const playerStats = this.data.ships.reduce((acc, ship) => {
            const dbShip = allShips.find(s => s.id === ship.id);
            if(!dbShip) return acc;
            acc.attack += dbShip.structure.attack * ship.amount;
            acc.defense += dbShip.structure.defense * ship.amount;
            acc.hitpoints += dbShip.structure.hitpoints * ship.amount;
            return acc;
        }, { attack: 0, defense: 0, hitpoints: 0 });

        const enemyDefensesStats = enemyDefenses.reduce((acc, defense) => {
            const dbDefense = allDefenses.find(d => d.id === defense.id);
            if(!dbDefense) return acc;
            acc.defense += dbDefense.structure.defense * defense.amount;
            acc.hitpoints += dbDefense.structure.hitpoints * defense.amount;
            return acc;
        }, { attack: 0, defense: 0, hitpoints: 0 });

        const enemyStats = enemyFleet.reduce((acc, ship) => {
            const dbShip = allShips.find(s => s.id === ship.id);
            if(!dbShip) return acc;
            acc.attack += dbShip.structure.attack * ship.amount;
            acc.defense += dbShip.structure.defense * ship.amount;
            acc.hitpoints += dbShip.structure.hitpoints * ship.amount;
            return acc;
        }, enemyDefensesStats);

        const playerShipsStructure: BattleFleet[] = [];
        for(const playerShip of this.data.ships) {
            for(let i = 0; i < playerShip.amount; i++) playerShipsStructure.push({
                id: playerShip.id,
                hitpoints: allShips.find(s => s.id === playerShip.id)?.structure.hitpoints ?? 0,
                attack: allShips.find(s => s.id === playerShip.id)?.structure.attack ?? 0,
                defense: allShips.find(s => s.id === playerShip.id)?.structure.defense ?? 0
            });
        }

        const enemyShipsStructure: BattleFleet[] = [];
        for(const enemyShip of enemyFleet) {
            for(let i = 0; i < enemyShip.amount; i++) enemyShipsStructure.push({
                id: enemyShip.id,
                hitpoints: allShips.find(s => s.id === enemyShip.id)?.structure.hitpoints ?? 0,
                attack: allShips.find(s => s.id === enemyShip.id)?.structure.attack ?? 0,
                defense: allShips.find(s => s.id === enemyShip.id)?.structure.defense ?? 0
            });
        }

        if(playerStats.attack > enemyStats.defense * 2 && playerStats.defense > enemyStats.attack * 2) {
            enemyShipsStructure.forEach((_, index) => enemyShipsStructure.splice(index, 1));
        } else if(enemyStats.attack > playerStats.defense * 2 && enemyStats.defense > playerStats.attack * 2) {
            playerShipsStructure.forEach((_, index) => playerShipsStructure.splice(index, 1));
        } else {
            roundLoop: for(let i = 0; i < 3; i++) {
                for(const playerShip of playerShipsStructure) {
                    const enemyShip = enemyShipsStructure[Math.floor(Math.random() * enemyShipsStructure.length)];
        
                    if(!enemyShip) break roundLoop;
                    const typeCount = playerShipsStructure.filter(s => s.id === playerShip.id).length;
                    const additionalShipAttack = typeCount > 1 ? Math.floor(Math.random() * typeCount) * playerShip.attack : 0;
                    const playerDamage = Math.max(0, (playerShip.attack + additionalShipAttack) - enemyShip.defense);
                    enemyShip.hitpoints -= playerDamage;
                }
        
                for(const enemyShip of enemyShipsStructure) {
                    const playerShip = playerShipsStructure[Math.floor(Math.random() * playerShipsStructure.length)];
        
                    if(!playerShip) break roundLoop;
                    const typeCount = enemyShipsStructure.filter(s => s.id === enemyShip.id).length;
                    const additionalShipAttack = typeCount > 1 ? Math.floor(Math.random() * typeCount) * enemyShip.attack : 0;
                    const enemyDamage = Math.max(0, (enemyShip.attack + additionalShipAttack) - playerShip.defense);
                    playerShip.hitpoints -= enemyDamage;
                }

                playerShipsStructure.forEach((ship, index) => {
                    if(ship.hitpoints <= 0) playerShipsStructure.splice(index, 1);
                });

                enemyShipsStructure.forEach((ship, index) => {
                    if(ship.hitpoints <= 0) enemyShipsStructure.splice(index, 1);
                });
            }
        }

        const playerBalance = playerStats.defense - enemyStats.attack;
        const enemyBalance = enemyStats.defense - playerStats.attack;

        const playerShipsLeft = playerShipsStructure.reduce((acc, ship) => { acc[ship.id] = (acc[ship.id] ?? 0) + 1; return acc }, {} as { [key: string]: number });
        const enemyShipsLeft = enemyShipsStructure.reduce((acc, ship) => { acc[ship.id] = (acc[ship.id] ?? 0) + 1; return acc }, {} as { [key: string]: number });

        const resourcesStolen: { id: string, amount: number }[] = [];

        const previousShips = JSON.parse(JSON.stringify(this.data.ships)) as Array<{ id: string, amount: number }>;
        if(playerShipsStructure.length > 0) {
            this.data.ships = Object.keys(playerShipsLeft).map(id => { return { id, amount: playerShipsLeft[id] } });
            if(playerBalance > enemyBalance) {
                const enemyResources = await (this.data.destination as Planet | SystemManager).resources;
                await enemyResources.calculateCurrentAvailableResources();
                let cargoSpaceFree = this.data.ships.reduce((acc, ship) => {
                    const dbShip = allShips.find(s => s.id === ship.id);
                    if(!dbShip) return acc;
                    return acc + dbShip.capacity.solid * ship.amount;
                }, 0);
    
                for(const res of enemyResources.resources) {
                    if(cargoSpaceFree <= 0) break;
                    const amount = Math.min(Math.floor(Math.random() * res.amount), cargoSpaceFree);
                    cargoSpaceFree -= amount;
                    this.data.cargo.push({ id: res.id, amount });
                    resourcesStolen.push({ id: res.id, amount });
                    enemyResources.setAmount([{ id: res.id, amount: res.amount - amount }]);
                };
            }

            await this.initiateReturn();
        } else this.data.ships = [];

        await this.sendMail(
            this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id,
            `Battle Results (${playerBalance > enemyBalance ? "Victory" : playerBalance < enemyBalance ? "Defeat" : "Draw"})`,
            `Results of battle at ${(this.data.destination instanceof SystemManager ? this.data.destination.data.name : this.data.destination.name)}:\n
            Player ships:\n${previousShips.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n
            Enemy ships:\n${enemyFleet.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n
            Enemy defenses:\n${enemyDefenses.map(defense => `${defense.amount} ${defense.id}`).join('\n')}\n
            Player stats: ${playerStats.hitpoints} HP, ${playerStats.attack} ATK, ${playerStats.defense} DEF\n
            Enemy stats: ${enemyStats.hitpoints} HP, ${enemyStats.attack} ATK, ${enemyStats.defense} DEF\n\n
            Player ships left:\n${Object.keys(playerShipsLeft).map(key => `${key} - ${playerShipsLeft[key]}`).join('\n')}\n
            Enemy ships left:\n${Object.keys(enemyShipsLeft).map(key => `${key} - ${enemyShipsLeft[key]}`).join('\n')}\n
            ${playerBalance > enemyBalance ? `Resources stolen:\n${resourcesStolen.map(res => `${res.id} - ${res.amount}`).join('\n')}` : ""}`
        );

        if(!("expedition" in this.data.destination)) {
            await this.sendMail(
                this.data.destination instanceof SystemManager ? this.data.destination.data.ownedBy.id : this.data.destination.system.data.ownedBy.id,
                `Battle Results (${playerBalance < enemyBalance ? "Victory" : playerBalance > enemyBalance ? "Defeat" : "Draw"})`,
                `Results of battle at ${(this.data.destination instanceof SystemManager ? this.data.destination.data.name : this.data.destination.name)}:\n
                Enemy ships:\n${previousShips.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n
                Player ships:\n${enemyFleet.map(ship => `${ship.amount} ${ship.id}`).join('\n')}\n
                Player defenses:\n${enemyDefenses.map(defense => `${defense.amount} ${defense.id}`).join('\n')}\n
                Enemy stats: ${playerStats.hitpoints} HP, ${playerStats.attack} ATK, ${playerStats.defense} DEF\n
                Player stats: ${enemyStats.hitpoints} HP, ${enemyStats.attack} ATK, ${enemyStats.defense} DEF\n\n
                Enemy ships left:\n${Object.keys(playerShipsLeft).map(key => `${key} - ${playerShipsLeft[key]}`).join('\n')}\n
                Player ships left:\n${Object.keys(enemyShipsLeft).map(key => `${key} - ${enemyShipsLeft[key]}`).join('\n')}\n
                ${playerBalance > enemyBalance ? `Resources stolen:\n${resourcesStolen.map(res => `${res.id} - ${res.amount}`).join('\n')}` : ""}`
            );
        }

        return !(playerShipsStructure.length > 0);
    }

    async expeditionResults() {
        const expeditionRandom = Math.random(); //TODO: make use of "expedition" from DBSector

        const allShips = await getAllShips();
        const shipCount = this.data.ships.reduce((acc, ship) => acc += ship.amount, 0);
        const totalShipCapacity = this.data.ships.reduce((acc, ship) => acc + (allShips.find(s => s.id === ship.id)?.capacity.solid ?? 0) * ship.amount, 0);
        const currentCargo = this.data.cargo.reduce((acc, res) => acc + res.amount, 0);

        const expeditionShipPositiveRandom = weightedRandom(shipCount > 100_000 ? 100_000 : shipCount);
        const valueAdded = Math.floor(Math.pow(Math.E * expeditionShipPositiveRandom, 4 * Math.PI) + 10)

        if(expeditionRandom < 0.02) { // 2% chance; lost all ships, black hole
            this.data.ships = [];
            await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered a black hole. All ships were lost.`);
            return;
        }

        if(expeditionRandom < 0.1) { // 8% chance; found ships
            const ships: { id: string, amount: number }[] = [];
            ships.push({ id: 'transporter', amount: getRandomInRange(0, 20) + Math.floor(valueAdded ) });
            ships.push({ id: 'fighter', amount: getRandomInRange(0, 20) + Math.floor(valueAdded ) });

            for(const s of ships) {
                const ship = this.data.ships.find((sh => sh.id === s.id));
                if(!ship) this.data.ships.push(s);
                else ship.amount += s.amount;
            }

            await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered abandoned shipyard. Following ships were added to the fleet:\n${ships.map(s => `${s.id} - ${s.amount}`).join(', ')}`);
            await this.initiateReturn();
            return;
        }

        if(expeditionRandom < 0.2) { // 10% chance; found resources           
            let totalCapacityUsed = 0;
            let resources = [];

            const resToAdd = valueAdded * 3 + currentCargo > totalShipCapacity ? totalShipCapacity / 3 : valueAdded;

            do {
                resources = [{
                    id: "coal",
                    amount: getRandomInRange(0, 10000) + resToAdd
                }, {
                    id: "iron",
                    amount: getRandomInRange(0, 10000) + resToAdd
                }, {
                    id: "gold",
                    amount: getRandomInRange(0, 10000) + resToAdd
                }];

                totalCapacityUsed = resources.reduce((acc, res) => acc + res.amount, 0);
            } while((totalCapacityUsed + currentCargo) > totalShipCapacity);

            for(const res of resources) {
                const resource = this.data.cargo.find(r => r.id === res.id);
                if(!resource) this.data.cargo.push(res);
                else resource.amount += res.amount;
            }

            await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered resource-rich asteroid. Following resources were added to the cargo inventory:\n${resources.map(r => `${r.id} - ${r.amount}`).join('\n')}`);
            await this.initiateReturn();
            return;
        }

        if(expeditionRandom < 0.35) { // 15% chance; pirates/aliens
            const pirates: { id: string, amount: number }[] = [
                { id: 'fighter', amount: getRandomInRange(0, 100) + valueAdded },
                { id: 'transporter', amount: getRandomInRange(0, 100) + valueAdded }
            ];

            await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered drunk pirates. After attempting to communicate with them, they attacked your fleet.`)
            await this.battleResults(pirates);
            return;
        }

        if(expeditionRandom < 0.36) { // 1% chance; rich rouge planet
            const oneResourceMax = totalShipCapacity / 3;

            let currentCargo = this.data.cargo.reduce((acc, res) => {
                return acc + res.amount;
            }, 0);

            const addedResources: { id: string, amount: number }[] = [{
                id: "coal",
                amount: 0
            }, {
                id: "iron",
                amount: 0
            }, {
                id: "gold",
                amount: 0
            }];

            while(currentCargo < totalShipCapacity) {
                if((addedResources.find(r => r.id === "coal")?.amount ?? 0) < oneResourceMax) {
                    (addedResources.find(r => r.id === "coal") as { amount: number }).amount++;
                    currentCargo++;
                    if(currentCargo >= totalShipCapacity) break;
                }

                if((addedResources.find(r => r.id === "iron")?.amount ?? 0) < oneResourceMax) {
                    (addedResources.find(r => r.id === "iron") as { amount: number }).amount++;
                    currentCargo++;
                    if(currentCargo >= totalShipCapacity) break;
                }

                if((addedResources.find(r => r.id === "gold")?.amount ?? 0) < oneResourceMax) {
                    (addedResources.find(r => r.id === "gold") as { amount: number }).amount++;
                    currentCargo++;
                    if(currentCargo >= totalShipCapacity) break;
                }
            }

            for(const res of addedResources) {
                const resource = this.data.cargo.find(r => r.id === res.id);
                if(!resource) this.data.cargo.push(res);
                else resource.amount += res.amount;
            }

            await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector encountered resource-rich rouge planet. Your fleet could not extract all resources. Following resources were added to the cargo inventory:\n${addedResources.map(r => `${r.id} - ${r.amount}`).join('\n')}`);
            await this.initiateReturn();
            return;
        } 

        await this.sendMail(this.data.source instanceof SystemManager ? this.data.source.data.ownedBy.id : this.data.source.system.data.ownedBy.id, "Expedition Results", `Your expedition to ${(this.data.destination as Sector).name} sector scanned the sector for a long time, yet it haven't found anything.`);
        await this.initiateReturn();
        return;
    }

    async sync() {
        const source = this.data.source instanceof SystemManager ? this.data.source.data._id : this.data.source._id;
        const destination = this.data.destination instanceof SystemManager ? this.data.destination.data._id : this.data.destination._id;

        const data: DBFleet = {
            _id: this.data.id,
            source,
            destination,
            departureTime: this.data.departureTime,
            arrivalTime: this.data.arrivalTime,
            returning: this.data.returning,
            mission: this.data.mission,
            ships: this.data.ships,
            cargo: this.data.cargo,
            additionalData: this.data.additionalData
        }

        await updateFleet(data);
    }
}