const crypto = require("crypto");
const dbus = require("dbus-next");

const BUS_PREFIX = "io.stillhq.Swai.Instance";
const OBJECT_PATH = "/io/stillhq/Swai/Instance";
const INTERFACE_NAME = "io.stillhq.Swai.Instance";
const MAX_REQUEST_BYTES = 1024 * 1024;

const REQUEST_FIELDS = [
    "app_id",
    "app_name",
    "main_url",
    "allowed_urls",
    "not_allowed_urls",
    "allow_multi_window",
    "reuse_window_for_ecosystem_navigation",
    "ecosystem",
    "login_urls",
    "swai_file_path",
    "use_dynamic_titlebar_colors",
    "use_manifest",
    "custom_user_agent",
];

function getBusName(ecosystemId) {
    const digest = crypto
        .createHash("sha256")
        .update(String(ecosystemId))
        .digest("hex")
        .slice(0, 32);
    return `${BUS_PREFIX}.i${digest}`;
}

function serializeWebApp(webapp) {
    const request = {};
    for (const field of REQUEST_FIELDS) {
        if (webapp[field] !== undefined) request[field] = webapp[field];
    }
    return JSON.stringify(request);
}

function normalizeWebAppRequest(payload) {
    if (typeof payload !== "string"
        || Buffer.byteLength(payload, "utf8") > MAX_REQUEST_BYTES) {
        throw new Error("invalid ecosystem request size");
    }

    const parsed = JSON.parse(payload);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error("ecosystem request must be an object");
    }

    for (const field of ["app_id", "app_name", "main_url"]) {
        if (typeof parsed[field] !== "string" || !parsed[field]) {
            throw new Error(`ecosystem request is missing ${field}`);
        }
    }

    const mainURL = new URL(parsed.main_url);
    if (mainURL.protocol !== "http:" && mainURL.protocol !== "https:") {
        throw new Error("ecosystem request main_url must use HTTP or HTTPS");
    }

    const request = {};
    for (const field of REQUEST_FIELDS) {
        if (parsed[field] !== undefined) request[field] = parsed[field];
    }

    for (const field of ["allowed_urls", "not_allowed_urls", "login_urls"]) {
        if (request[field] !== undefined
            && (!Array.isArray(request[field])
                || request[field].some((value) => typeof value !== "string"))) {
            throw new Error(`${field} must be an array of strings`);
        }
    }

    return request;
}

function withTimeout(promise, timeoutMs, message) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
        Promise.resolve(promise).then(
            (value) => {
                clearTimeout(timer);
                resolve(value);
            },
            (error) => {
                clearTimeout(timer);
                reject(error);
            },
        );
    });
}

class SwaiInstanceInterface extends dbus.interface.Interface {
    constructor(onRequest) {
        super(INTERFACE_NAME);
        this.onRequest = onRequest;
    }

    async OpenApp(payload) {
        return this.onRequest(payload);
    }
}

SwaiInstanceInterface.configureMembers({
    methods: {
        OpenApp: { inSignature: "s", outSignature: "b" },
    },
});

class EcosystemLauncher {
    static bus = null;
    static busName = null;
    static instanceInterface = null;
    static requestHandler = null;
    static pendingRequests = [];

    static getEcosystemId(webapp) {
        return webapp.ecosystem || webapp.app_id;
    }

    static async launch(webapp) {
        const ecosystemId = this.getEcosystemId(webapp);
        const busName = getBusName(ecosystemId);
        let bus = null;

        try {
            bus = dbus.sessionBus();
            bus.on("error", (error) => {
                console.warn(`Ecosystem D-Bus connection error: ${error.message}`);
            });
            const reply = await withTimeout(
                bus.requestName(busName, dbus.NameFlag.DO_NOT_QUEUE),
                1500,
                "timed out connecting to the session bus",
            );
            const isPrimary = reply === dbus.RequestNameReply.PRIMARY_OWNER
                || reply === dbus.RequestNameReply.ALREADY_OWNER;

            if (!isPrimary) {
                const sent = await this._sendRequestWithRetry(bus, busName, webapp);
                bus.disconnect();
                return !sent;
            }

            this.bus = bus;
            this.busName = busName;
            this.instanceInterface = new SwaiInstanceInterface(
                (payload) => this._receiveRequest(payload),
            );
            bus.export(OBJECT_PATH, this.instanceInterface);
            console.log(`Claimed ecosystem D-Bus service: ${busName}`);
            return true;
        } catch (error) {
            try {
                if (bus) bus.disconnect();
            } catch (_) {
                // Ignore cleanup errors after a failed connection.
            }
            console.warn(
                `Ecosystem D-Bus unavailable; continuing without single-instance handoff: ${error.message}`,
            );
            return true;
        }
    }

    static async _sendRequest(bus, busName, webapp) {
        const proxy = await bus.getProxyObject(busName, OBJECT_PATH);
        const instance = proxy.getInterface(INTERFACE_NAME);
        const accepted = await instance.OpenApp(serializeWebApp(webapp));
        if (accepted) {
            console.log(`Sent ${webapp.app_name} to the running ecosystem instance`);
        }
        return accepted;
    }

    static async _sendRequestWithRetry(bus, busName, webapp) {
        let lastError;
        for (let attempt = 0; attempt < 5; attempt += 1) {
            try {
                return await this._sendRequest(bus, busName, webapp);
            } catch (error) {
                lastError = error;
                await new Promise((resolve) => setTimeout(resolve, 50));
            }
        }
        throw lastError;
    }

    static async _receiveRequest(payload) {
        try {
            const request = normalizeWebAppRequest(payload);
            if (this.requestHandler) {
                await this.requestHandler(request);
            } else {
                this.pendingRequests.push(request);
            }
            return true;
        } catch (error) {
            console.warn(`Rejected ecosystem request: ${error.message}`);
            return false;
        }
    }

    static setRequestHandler(handler) {
        this.requestHandler = handler;
        if (!handler || this.pendingRequests.length === 0) return;

        const queued = this.pendingRequests.splice(0);
        for (const request of queued) {
            Promise.resolve(handler(request)).catch((error) => {
                console.error("Failed to process queued ecosystem request:", error);
            });
        }
    }

    static async cleanup() {
        this.setRequestHandler(null);
        this.pendingRequests.length = 0;

        if (!this.bus) return;
        try {
            if (this.instanceInterface) {
                this.bus.unexport(OBJECT_PATH, this.instanceInterface);
            }
            if (this.busName) await this.bus.releaseName(this.busName);
            this.bus.disconnect();
        } catch (error) {
            console.warn("Failed to clean up ecosystem D-Bus service:", error.message);
        } finally {
            this.bus = null;
            this.busName = null;
            this.instanceInterface = null;
        }
    }
}

module.exports = EcosystemLauncher;
module.exports.getBusName = getBusName;
module.exports.serializeWebApp = serializeWebApp;
module.exports.normalizeWebAppRequest = normalizeWebAppRequest;
