import type { Locator, Page } from "playwright";
import { $msg } from "../../../src/common/translation.ts";
import { evalObsidianJson } from "./cli.ts";
import { captureObsidianDialogue, captureObsidianElement, withObsidianPage } from "./ui.ts";

export type SetupArtifact = {
    setupURI: string;
    setupPassphrase: string;
};

export type SetupState = {
    configured: boolean;
    databaseReady: boolean;
    appReady: boolean;
    suspended: boolean;
    remoteType: string;
    activeConfigurationId: string;
    remoteConfigurationCount: number;
    endpoint: string;
    bucket: string;
    bucketPrefix: string;
    p2pEnabled: boolean;
    p2pRelays: string;
    p2pRoomId: string;
};

export type SetupCaptureNames = {
    scenario: string;
    guide: string;
};

const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000);
const initialisationTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_INITIALISATION_TIMEOUT_MS ?? 120000);

export function modalByTitle(page: Page, title: string): Locator {
    return page.locator(".modal-container").filter({
        has: page.locator(".modal-title").filter({ hasText: title }),
    });
}

export async function captureGuideDialogue(port: number, filename: string, title: string): Promise<string> {
    return await captureObsidianElement(
        port,
        filename,
        (page) => modalByTitle(page, title).locator(".modal").first(),
        uiTimeoutMs
    );
}

export async function assertVerticalActionLayout(port: number, title: string): Promise<void> {
    await withObsidianPage(port, async (page) => {
        const actions = modalByTitle(page, title).locator(".vpk-action-dialog__actions").first();
        await actions.waitFor({ state: "visible", timeout: uiTimeoutMs });
        const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection);
        if (flexDirection !== "column") {
            throw new Error(`Expected vertically stacked actions in '${title}', received '${flexDirection}'.`);
        }
    });
}

export async function selectRadioOption(modal: Locator, title: string): Promise<void> {
    const radio = modal.locator("label").filter({ hasText: title }).locator('input[type="radio"]').first();
    await radio.check({ timeout: uiTimeoutMs });
}

export async function selectCheckbox(modal: Locator, title: string): Promise<void> {
    const checkbox = modal.locator("label").filter({ hasText: title }).locator('input[type="checkbox"]').first();
    await checkbox.check({ timeout: uiTimeoutMs });
}

export async function enterSetupURI(
    port: number,
    mode: "new" | "existing",
    artifact: SetupArtifact,
    captures: SetupCaptureNames
): Promise<string> {
    await withObsidianPage(port, async (page) => {
        const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" });
        await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs });
        await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs });

        const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync");
        await intro.waitFor({ state: "visible", timeout: uiTimeoutMs });
        if (mode === "new") {
            await selectRadioOption(intro, "I am setting this up for the first time");
            await intro
                .getByRole("button", { name: "Yes, I want to set up a new synchronisation" })
                .click({ timeout: uiTimeoutMs });
        } else {
            await selectRadioOption(intro, "I am adding a device to an existing synchronisation setup");
            await intro
                .getByRole("button", { name: "Yes, I want to add this device to my existing synchronisation" })
                .click({ timeout: uiTimeoutMs });
        }

        const method = modalByTitle(page, mode === "new" ? "Connection Method" : "Device Setup Method");
        await method.waitFor({ state: "visible", timeout: uiTimeoutMs });
        await selectRadioOption(method, "Use a Setup URI (Recommended)");
        await method.getByRole("button", { name: "Proceed with Setup URI" }).click({ timeout: uiTimeoutMs });

        const setup = modalByTitle(page, "Enter Setup URI");
        await setup.waitFor({ state: "visible", timeout: uiTimeoutMs });
        await setup.locator('input[placeholder^="obsidian://setuplivesync"]').fill(artifact.setupURI);
        await setup.locator('input[name="password"]').fill(artifact.setupPassphrase);
    });
    const screenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-${mode === "new" ? "first" : "second"}-setup-uri.png`,
        "Enter Setup URI"
    );
    await withObsidianPage(port, async (page) => {
        await modalByTitle(page, "Enter Setup URI")
            .getByRole("button", { name: "Test Settings and Continue" })
            .click({ timeout: uiTimeoutMs });
    });
    return screenshot;
}

export async function generateSetupURIFromDevice(
    port: number,
    setupPassphrase: string,
    captures: SetupCaptureNames
): Promise<{ artifact: SetupArtifact; screenshots: string[] }> {
    const opened = await withObsidianPage(port, async (page) => {
        return await page.evaluate(
            (commandId) =>
                (
                    globalThis as typeof globalThis & {
                        app?: { commands?: { executeCommandById(id: string): boolean } };
                    }
                ).app?.commands?.executeCommandById(commandId) === true,
            "obsidian-livesync:livesync-copysetupuri"
        );
    });
    if (!opened) throw new Error("The command for generating a Setup URI was not registered.");

    const promptTitle = "Encrypt your settings";
    await withObsidianPage(port, async (page) => {
        const prompt = modalByTitle(page, promptTitle);
        await prompt.waitFor({ state: "visible", timeout: uiTimeoutMs });
        await prompt.locator('input[type="password"]').fill(setupPassphrase);
    });
    const promptScreenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-copy-setup-uri-passphrase.png`,
        promptTitle
    );
    await withObsidianPage(port, async (page) => {
        const prompt = modalByTitle(page, promptTitle);
        await prompt.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs });
        await prompt.waitFor({ state: "hidden", timeout: uiTimeoutMs });
    });

    const resultTitle = "Your Setup URI is ready to be copied";
    const setupURI = await withObsidianPage(port, async (page) => {
        const result = modalByTitle(page, resultTitle);
        await result.waitFor({ state: "visible", timeout: uiTimeoutMs });
        return await result.locator("textarea[readonly]").inputValue();
    });
    if (!setupURI.startsWith("obsidian://setuplivesync?settings=")) {
        throw new Error("The first device did not generate a valid Setup URI.");
    }
    const resultScreenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-copy-setup-uri-result.png`,
        resultTitle
    );
    await withObsidianPage(port, async (page) => {
        const result = modalByTitle(page, resultTitle);
        await result.getByRole("button", { name: $msg("Ok"), exact: true }).click({ timeout: uiTimeoutMs });
        await result.waitFor({ state: "hidden", timeout: uiTimeoutMs });
    });

    return {
        artifact: { setupURI, setupPassphrase },
        screenshots: [promptScreenshot, resultScreenshot],
    };
}

export async function captureAndStartInitialisation(
    port: number,
    mode: "new" | "existing",
    captures: SetupCaptureNames
): Promise<string> {
    const p2pFirstDevice = mode === "new" && captures.guide === "p2p-setup";
    const p2pAdditionalDevice = mode === "existing" && captures.guide === "p2p-setup";
    const title = p2pFirstDevice
        ? "Setup Complete: Preparing This P2P Device"
        : p2pAdditionalDevice
          ? "Setup Complete: Preparing to Fetch from Another Device"
          : mode === "new"
            ? "Setup Complete: Preparing to Initialise Server"
            : "Setup Complete: Preparing to Fetch Synchronisation Data";
    const button = p2pFirstDevice
        ? "Restart and Prepare This Device"
        : p2pAdditionalDevice
          ? "Restart and Select Source Device"
          : mode === "new"
            ? "Restart and Initialise Server"
            : "Restart and Fetch Data";
    if (p2pAdditionalDevice) {
        await withObsidianPage(port, async (page) => {
            const modal = modalByTitle(page, title);
            await modal
                .getByText("After restarting, select an online source device for the initial Fetch.", {
                    exact: false,
                })
                .waitFor({ state: "visible", timeout: uiTimeoutMs });
            if ((await modal.getByText("downloaded from the server", { exact: false }).count()) !== 0) {
                throw new Error("P2P additional-device setup still describes the initial Fetch as a server download.");
            }
        });
    }
    const screenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-${mode === "new" ? "first-initialise" : "second-fetch"}.png`,
        title
    );
    await withObsidianPage(port, async (page) => {
        await modalByTitle(page, title).getByRole("button", { name: button }).click({ timeout: uiTimeoutMs });
    });
    return screenshot;
}

export async function confirmRebuild(port: number, captures: SetupCaptureNames): Promise<string> {
    const isP2P = captures.guide === "p2p-setup";
    const title = isP2P
        ? "Final Confirmation: Prepare This Device for P2P"
        : "Final Confirmation: Overwrite Server Data with This Device's Files";
    const screenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-first-rebuild-confirmation.png`,
        title
    );
    await withObsidianPage(port, async (page) => {
        const modal = modalByTitle(page, title);
        if (isP2P) {
            await selectCheckbox(
                modal,
                "I understand that this resets only this device's local synchronisation database."
            );
            await selectRadioOption(modal, "I understand the risks and will proceed without a backup.");
            await modal
                .getByRole("button", { name: "I Understand, Prepare This Device" })
                .click({ timeout: uiTimeoutMs });
            return;
        }
        await selectCheckbox(
            modal,
            "I understand that all changes made on other smartphones or computers possibly could be lost."
        );
        await selectCheckbox(
            modal,
            "I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information."
        );
        await selectCheckbox(modal, "I understand that this action is irreversible once performed.");
        await selectRadioOption(modal, "I understand the risks and will proceed without a backup.");
        await modal.getByRole("button", { name: "I Understand, Overwrite Server" }).click({ timeout: uiTimeoutMs });
    });
    return screenshot;
}

export async function continueWithoutRemoteSettings(port: number, captures: SetupCaptureNames): Promise<string> {
    const title = "No Synchronisation Settings Found";
    const screenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-missing-remote-configuration.png`,
        title
    );
    await withObsidianPage(port, async (page) => {
        const modal = modalByTitle(page, title);
        await modal
            .getByText("This is normal for a new remote.", { exact: false })
            .waitFor({ state: "visible", timeout: uiTimeoutMs });
        await modal.getByRole("button", { name: "Cancel", exact: true }).waitFor({
            state: "visible",
            timeout: uiTimeoutMs,
        });
        await modal.getByRole("button", { name: "Use this device's settings" }).click({ timeout: uiTimeoutMs });
    });
    return screenshot;
}

export async function acknowledgeDisabledOptionalFeatures(port: number, captures: SetupCaptureNames): Promise<string> {
    const title = "All optional features are disabled";
    const screenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-optional-features-disabled.png`,
        title
    );
    await withObsidianPage(port, async (page) => {
        const modal = modalByTitle(page, title);
        await modal.getByRole("button", { name: "OK" }).click({ timeout: uiTimeoutMs });
        await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
    });
    return screenshot;
}

export async function confirmFastFetch(port: number, captures: SetupCaptureNames): Promise<string[]> {
    const firstTitle = "Data retrieval scheduled";
    await assertVerticalActionLayout(port, firstTitle);
    const firstScreenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-retrieval-method.png`,
        firstTitle
    );
    await withObsidianPage(port, async (page) => {
        await modalByTitle(page, firstTitle)
            .getByRole("button", { name: "Overwrite all with remote files" })
            .click({ timeout: uiTimeoutMs });
    });

    const secondTitle = "How to handle extra existing local files?";
    await assertVerticalActionLayout(port, secondTitle);
    const secondScreenshot = await captureGuideDialogue(
        port,
        `guide-${captures.guide}-local-file-policy.png`,
        secondTitle
    );
    await withObsidianPage(port, async (page) => {
        await modalByTitle(page, secondTitle)
            .getByRole("button", { name: "Keep local files even if not on remote" })
            .click({ timeout: uiTimeoutMs });
    });
    return [firstScreenshot, secondScreenshot];
}

function isConfiguredSetupReady(state: SetupState): boolean {
    return (
        state.configured &&
        state.databaseReady &&
        state.appReady &&
        !state.suspended &&
        state.activeConfigurationId !== "" &&
        state.remoteConfigurationCount === 1
    );
}

export async function readSetupState(cliBinary: string, environment: NodeJS.ProcessEnv): Promise<SetupState> {
    return await evalObsidianJson<SetupState>(
        cliBinary,
        [
            "(()=>{",
            "const core=app.plugins.plugins['obsidian-livesync'].core;",
            "const settings=core.services.setting.currentSettings();",
            "return JSON.stringify({",
            "configured:settings.isConfigured===true,",
            "databaseReady:core.services.database.isDatabaseReady(),",
            "appReady:core.services.appLifecycle.isReady(),",
            "suspended:core.services.appLifecycle.isSuspended(),",
            "remoteType:settings.remoteType||'',",
            "activeConfigurationId:settings.activeConfigurationId||'',",
            "remoteConfigurationCount:Object.keys(settings.remoteConfigurations||{}).length,",
            "endpoint:settings.endpoint||'',",
            "bucket:settings.bucket||'',",
            "bucketPrefix:settings.bucketPrefix||'',",
            "p2pEnabled:settings.P2P_Enabled===true,",
            "p2pRelays:settings.P2P_relays||'',",
            "p2pRoomId:settings.P2P_roomID||'',",
            "});",
            "})()",
        ].join(""),
        environment
    );
}

export async function waitForConfiguredSetup(
    cliBinary: string,
    environment: NodeJS.ProcessEnv,
    timeoutMs = initialisationTimeoutMs
): Promise<SetupState> {
    const deadline = Date.now() + timeoutMs;
    let lastState: SetupState | undefined;
    let lastError: unknown;
    while (Date.now() < deadline) {
        try {
            lastState = await readSetupState(cliBinary, environment);
            if (isConfiguredSetupReady(lastState)) return lastState;
        } catch (error) {
            lastError = error;
        }
        await new Promise((resolve) => setTimeout(resolve, 500));
    }
    throw new Error(
        `Timed out waiting for configured Setup URI state: ${JSON.stringify(lastState)}${
            lastError instanceof Error ? `; last error: ${lastError.message}` : ""
        }`
    );
}

export async function finishInitialisation(
    port: number,
    cliBinary: string,
    environment: NodeJS.ProcessEnv
): Promise<SetupState> {
    const message = "Do you want to resume file and database processing, and restart obsidian now?";
    const deadline = Date.now() + initialisationTimeoutMs;
    let readySince: number | undefined;
    while (Date.now() < deadline) {
        const resumeVisible = await withObsidianPage(port, async (page) => {
            return await modalByTitle(page, "Confirmation").filter({ hasText: message }).isVisible();
        }).catch(() => false);
        if (resumeVisible) {
            await withObsidianPage(port, async (page) => {
                const modal = modalByTitle(page, "Confirmation").filter({ hasText: message });
                await modal.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs });
                await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
            });
            return await waitForConfiguredSetup(cliBinary, environment);
        }
        try {
            const state = await readSetupState(cliBinary, environment);
            if (isConfiguredSetupReady(state)) {
                readySince ??= Date.now();
                if (Date.now() - readySince >= 1000) return state;
            } else {
                readySince = undefined;
            }
        } catch {
            // Obsidian may be reloading while the scheduled operation runs.
            readySince = undefined;
        }
        await new Promise((resolve) => setTimeout(resolve, 500));
    }
    throw new Error("Timed out waiting for Setup URI initialisation to finish.");
}

export async function resumeCompatibilityReviewIfShown(port: number): Promise<boolean> {
    const title = "Synchronisation paused for compatibility review";
    const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000);
    let available = false;
    while (Date.now() < deadline && !available) {
        available = await withObsidianPage(port, async (page) => {
            const modal = modalByTitle(page, title);
            if (await modal.isVisible()) return true;
            const reminder = page.locator(".notice.livesync-compatibility-review-notice");
            if (!(await reminder.isVisible())) return false;
            await reminder.getByRole("link", { name: "Review why" }).click({ timeout: uiTimeoutMs });
            await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
            return true;
        }).catch(() => false);
        if (!available) await new Promise((resolve) => setTimeout(resolve, 200));
    }
    if (!available) return false;
    await withObsidianPage(port, async (page) => {
        const modal = modalByTitle(page, title);
        await modal.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs });
        await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
    });
    return true;
}
