import {Mongo} from 'meteor/mongo';
import {Meteor} from 'meteor/meteor';
import {ListingsDB} from "../../both/collections/listings";
import {LocationsDB} from "/imports/api/Listings/both/collections/locations";
import _ from "lodash";
import {ListingParentFromSubActivity} from "/imports/api/Activities/both/enums";
import {CategoriesDB} from "/imports/api/Listings/both/collections/categories";

Meteor.methods({
    async "listings.search.getListingForMapInfoWindow"(listingId: string, params: SearchListingsParams) {
        const result = await ListingsDB.rawCollection().aggregate([
            { $match: { _id: listingId } },
            ...getPreparePhotosArrayStage(params),
            { $project: SearchResultCardFieldProjection },
        ]).toArray();

        if(result.length) {
            // Order image refs by the same order as photos
            const listing = result[0];
            const keyedRefs = _.keyBy(listing.imageRefs, "_id");
            listing.imageRefs = _.filter(_.map(listing.photos, (_id) => keyedRefs[_id]), r => !!r);
            return listing;
        }

        return null;
    },

    async "listings.search"(params: SearchListingsParams) {
        const { page, pageSize } = params;

        let results = [];
        let mapResults = [];

        if(params.location && params.location.center.length && params.activities.length) {
            results = await ListingsDB.rawCollection().aggregate([
                ...getGeoNearStage(params),
                { $skip: page * pageSize },
                { $limit: pageSize },
                ...getPreparePhotosArrayStage(params),
                { $project: SearchResultCardFieldProjection },
            ]).toArray();

            mapResults = await ListingsDB.rawCollection().aggregate([
                ...getGeoNearStage(params),
                { $project: MapPinResultFieldProjection }
            ]).toArray();

            // Order image refs by the same order as photos
            _.forEach(results, (r) => {
                const keyedRefs = _.keyBy(r.imageRefs, "_id");
                r.imageRefs = _.filter(_.map(r.photos, (_id) => keyedRefs[_id]), r => !!r);
            });
        }

        // DEBUG Logs for result ordering
        // console.log("==================")
        // console.log(_.map(results, ({ sortWeight, name, sort_rating, distanceFromPoint }) => ({ name, sortWeight, sort_rating, distanceFromPoint })));

        return {
            listings: results,
            mapResults,
            count: mapResults.length
        };
    },

    "listings.getLocation"(locationId) {
        return LocationsDB.findOne({ _id: locationId }, { fields: { name: 1, center: 1 } });
    }
});

const ImageRefProjection = {
    _downloadRoute: 1,
    _collectionName: 1,
    "versions.small": 1
}

const SearchResultCardFieldProjection = {
    _id: 1,
    address: 1,
    imageRefs: 1,
    name: 1,
    reviews: 1,
    price_min: 1,
    photos: 1,
    maxAttendees: 1,

    featured_photo_id: 1,
    sortWeight: 1,
    distanceFromPoint: 1,
    sort_rating: 1
}

const MapPinResultFieldProjection = {
    _id: 1,
    address: 1
}

type SearchListingsParams = {
    spacesTagId: string | null
    activities: string[]
    attendeeRange: string | null
    exactAttendees: number | null
    types: string[]
    amenities: string[]
    styles: string[]
    priceFrom: number | null
    priceTo: number | null
    location: {
        center: [number, number]
        bounds?: [ [number, number], [number, number] ]
    },
    page: number
    pageSize: number
    itemsPerRow: 3
}

function getFiltersQuery(params: SearchListingsParams) {
    const query: Mongo.Selector<unknown> = { status: "active", published: true };

    const anActivity = params.activities[0];
    const activity = ListingParentFromSubActivity[anActivity];

    if(activity) {
        query.activities = activity;
    }

    if (params.types?.length) {
        const matchingCategories = CategoriesDB.find({ name: { $in: params.types } }).fetch();
        const idsToCheck = _.map(matchingCategories, val => val._id);
        query.subcategories = { $all: idsToCheck }
    }

    if(params.exactAttendees) {
        query.maxAttendees = { $gte: params.exactAttendees };
    } else if(params.attendeeRange) {
        if(params.attendeeRange === "1 to 25") {
            query.maxAttendees = { $gte: 1 };
        } else if(params.attendeeRange === "26 to 50") {
            query.maxAttendees = { $gte: 26 };
        } else if(params.attendeeRange === "51 to 100") {
            query.maxAttendees = { $gte: 51 };
        } else if(params.attendeeRange === "100plus") {
            query.maxAttendees = { $gte: 100 };
        }
    }

    if(params.priceFrom) {
        query.price_min = { $gte: params.priceFrom }
    }
    if(params.priceTo) {
        query.price_max = { $lte: params.priceTo }
    }

    if(params.styles?.length) {
        query.styles = { $all: params.styles }
    }

    if(params.amenities?.length) {
        query.amenities = { $all: params.amenities }
    }

    if(params.spacesTagId) {
        query.spaceTagIds = params.spacesTagId;
    }

    return query;
}

function getGeoNearStage(params: SearchListingsParams) {
    return [
        {
            $geoNear: {
                near: { type: "Point", coordinates: params.location.center },
                key: "address",
                spherical: true,
                distanceField: "distanceFromPoint",
                maxDistance: 45000,
                query: getFiltersQuery(params)
            }
        },
        { $set:
            { sortWeight:
                { $divide:
                    [
                        { $ceil:
                            { $divide:
                                [
                                    { $add: [ "$distanceFromPoint", 1 ] },
                                    15000
                                ]
                            }
                        },
                        { $add: [ "$sort_rating", 1 ] }
                    ]
                }
            }
        },
        { $sort: { sortWeight: 1, distanceFromPoint: 1 } },
    ]
}

function getFilterNullFromArray(input: any) {
    return {
        $filter: {
            input,
            as: "a",
            cond: { $ne: [ "$$a", null ] }
        }
    }
}

function getPrependSpaceTagTaggedImagesStages(params: SearchListingsParams) {

    if(!params.spacesTagId) {
        return [];
    }

    return [
        {
            $lookup: {
                from: "spaces_tags_images_links",
                as: "spacesTagImageLinks",
                let: { listingId: "$_id" },
                pipeline: [
                    { $match:
                        { $expr:
                            { $and:
                                [
                                    { $eq: [ "$spaceTagId", params.spacesTagId ] },
                                    { $eq: [ "$listingId", "$$listingId" ] }
                                ]
                            }
                        }
                    }
                ]
            }
        },
        { $set:
            { spacesTagImageLinks: "$spacesTagImageLinks.imageId" }
        },
        {
            $set: {
                photos: {
                    $setDifference: [
                        { $concatArrays: [ "$spacesTagImageLinks", "$photos" ] },
                        []
                    ]
                }
            }
        }
    ]
}

function getPreparePhotosArrayStage(params: SearchListingsParams) {
    // Add the feature_photo_id to the front of the photos array and use setDifference to remove the created
    // duplicate photo id. Remove null entries possibly caused if no featured_image_id was set to begin with.
    return [
        {
            $set: {
                photos: getFilterNullFromArray({
                    $setDifference: [
                        { $concatArrays: [ [ "$featured_photo_id" ], "$photos" ] },
                        []
                    ]
                })
            },
        },
        ...getPrependSpaceTagTaggedImagesStages(params),
        { $set: { photos: { $slice: [ "$photos", 7 ] } } },
        // Use lookup to create imageRefs field and limit to 7 results.
        // TODO: extend this with the necessary stages that will consider the spaceTag selected
        {
            $lookup: {
                from: "images",
                as: "imageRefs",
                let: { photos: "$photos" },
                pipeline: [
                    // https://stackoverflow.com/questions/22797768/does-mongodbs-in-clause-guarantee-order
                    { $match: { $expr: { $in: [ "$_id", "$$photos" ] } } },
                    { $project: ImageRefProjection }
                ]
            }
        }
    ];
}