import Axios from "axios";
import ipaddr from "ipaddr.js";
import { Resolver } from "dns";
import { promisify } from "util";
import { LocalUrlError } from "./LocalUrlError";
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable";

class MapFetcher {
    async fetchMap(mapUrl: string): Promise<ITiledMap> {
        // Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)

        if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) {
            throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
        }

        // Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
        // returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
        // target to different servers (and one could trick Axios.get into loading resources on the internal network
        // despite isLocalUrl checking that.
        // We can deem this problem not that important because:
        // - We make sure we are only passing "GET" requests
        // - The result of the query is never displayed to the end user
        const res = await Axios.get(mapUrl, {
            maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
            timeout: 10000, // Timeout after 10 seconds
        });

        if (!isTiledMap(res.data)) {
            //TODO fixme
            //throw new Error("Invalid map format for map " + mapUrl);
            console.error("Invalid map format for map " + mapUrl);
        }
        /* eslint-disable-next-line @typescript-eslint/no-unsafe-return */
        return res.data;
    }

    /**
     * Returns true if the domain name is localhost of *.localhost
     * Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
     *
     * @private
     */
    async isLocalUrl(url: string): Promise<boolean> {
        const urlObj = new URL(url);
        if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
            return true;
        }

        let addresses = [];
        if (!ipaddr.isValid(urlObj.hostname)) {
            const resolver = new Resolver();
            addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
        } else {
            addresses = [ urlObj.hostname ];
        }

        for (const address of addresses) {
            const addr = ipaddr.parse(address);
            if (addr.range() !== "unicast") {
                return true;
            }
        }

        return false;
    }
}

export const mapFetcher = new MapFetcher();
