/**
 * openContextHelp.js
 *
 * Registers the acurity.openContextHelp command (default keybinding: F1 when
 * an Acurity file is active).
 *
 * Resolution order for the help base location:
 *   1. acurity.help.webRoot setting (if set) — used as-is for HTTP URLs or
 *      converted to a file:// URI for local paths.
 *   2. Auto-discover: {workspaceFolder}/help/AcurityHelp.htm
 *   3. Prompt the user to locate AcurityHelp.htm via a file picker.  After a
 *      successful pick the user is offered the option to save the directory to
 *      acurity.help.webRoot so future invocations skip the prompt.
 *
 * URL format (recognised token):   {base}/AcurityHelp.htm#{page.htm}
 * URL format (unrecognised token):  {base}/AcurityHelp.htm#search-{TOKEN}
 * Both fragments are percent-encoded.
 * AcurityHelp.htm handles frame-loading the individual page so the user gets
 * the full navigation menu alongside the topic content.
 */

import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawn } from "child_process";
import * as vscode from "vscode";
import { getHelpPage } from "../../../core/src/builtins/helpRegistry.js";
import { log } from "../../../core/src/logger.js";

// ---------------------------------------------------------------------------
// URL launcher
// ---------------------------------------------------------------------------

/**
 * Opens a URL in the system default browser, preserving the fragment.
 *
 * On Windows, both vscode.env.openExternal and `cmd /c start` use ShellExecute
 * for file:// URIs, which resolves the path as a filesystem entry and strips
 * the #fragment before handing it to the browser.
 *
 * The workaround: write a minimal HTML trampoline to %TEMP%, open that file
 * (no fragment — opens cleanly via ShellExecute), and let the browser execute
 * the inline `window.location.replace(realUrl)` which preserves the fragment.
 *
 * HTTP/HTTPS URLs are unaffected; they use vscode.env.openExternal directly.
 *
 * @param {string} url  Fully-formed URL string (file:// or http/https)
 */
function openUrlInBrowser(url) {
    if (process.platform !== "win32") {
        const cmd = process.platform === "darwin" ? "open" : "xdg-open";
        spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
        return;
    }

    if (!url.startsWith("file://")) {
        // HTTP/HTTPS on Windows — cmd start handles these correctly.
        spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
        return;
    }

    // Windows + file:// — use a JS-redirect trampoline to preserve the fragment.
    const tmpFile = path.join(os.tmpdir(), "acurity-help-redirect.html");
    // The URL is already a well-formed file:// string; only escape double-quotes
    // for embedding inside a JS string literal.
    const jsUrl = url.replace(/"/g, '\\"');
    fs.writeFileSync(
        tmpFile,
        `<!DOCTYPE html><html><head><meta charset="utf-8">` +
        `<script>window.location.replace("${jsUrl}");</script>` +
        `</head><body><p>Redirecting to Acurity help&hellip;</p></body></html>`
    );
    const tmpUri = "file:///" + tmpFile.replace(/\\/g, "/");
    spawn("cmd", ["/c", "start", "", tmpUri], { detached: true, stdio: "ignore" }).unref();
}

// ---------------------------------------------------------------------------

// Word extraction
// ---------------------------------------------------------------------------

/**
 * Returns the bare keyword at the cursor position, stripping a leading `#` if
 * one immediately precedes the word (HASH-ON mode keywords like `#IF`, `#VAR`).
 *
 * Returns null when the cursor is not on an alphanumeric word.
 *
 * @param {vscode.TextDocument} document
 * @param {vscode.Position} position
 * @returns {{ bare: string, display: string }|null} Object with the bare
 *   keyword (no `#`) used for registry lookup, and the display form (with `#`
 *   prefix if present in the source) used for user-facing messages. Returns
 *   null when the cursor is not on an alphanumeric word.
 */
function getTokenAtPosition(document, position) {
    const range = document.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/);
    if (!range) return null;

    const word = document.getText(range);
    const lineText = document.lineAt(position.line).text;
    const startChar = range.start.character;

    // If the character immediately before the word is '#', include it so that
    // the registry lookup ("IF", "VAR", etc.) strips it transparently.
    // We don't change the lookup key — getHelpPage always receives the bare word;
    // the # is recorded here solely for the "no result" message below.
    const hasHash = startChar > 0 && lineText[startChar - 1] === "#";

    return hasHash ? { bare: word, display: "#" + word } : { bare: word, display: word };
}

// ---------------------------------------------------------------------------
// Base URL resolution
// ---------------------------------------------------------------------------

/**
 * Converts a local file path or URL string into a base URI string (no trailing
 * slash) that can be used to build the final AcurityHelp URL.
 *
 * @param {string} raw  Value from acurity.help.webRoot or a picked file path.
 * @returns {string}
 */
function toBaseUri(raw) {
    const trimmed = raw.replace(/[/\\]+$/, "");
    // Already a URI scheme (http://, https://, file://, etc.) — use as-is.
    if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(trimmed)) {
        return trimmed;
    }
    // Local path — build a file:// URI manually so the Windows drive-letter
    // colon (C:) is preserved literally.  vscode.Uri.file().toString() encodes
    // it as %3A which Windows ShellExecute/cmd cannot resolve.
    const fwdSlash = trimmed.replace(/\\/g, "/");
    const absolute = fwdSlash.startsWith("/") ? fwdSlash : `/${fwdSlash}`;
    return `file://${absolute}`;
}

/**
 * Resolves the base URL for the help system.  Follows the resolution order
 * documented at the top of this file.
 *
 * Returns null when the user cancels the file picker.
 *
 * @returns {Promise<string|null>}
 */
async function resolveHelpBase() {
    // 1. Configured webRoot
    const config  = vscode.workspace.getConfiguration("acurity.help");
    const webRoot = (config.get("webRoot") || "").trim();
    if (webRoot) {
        return toBaseUri(webRoot);
    }

    // 2. Auto-discover in workspace folders
    const folders = vscode.workspace.workspaceFolders || [];
    for (const folder of folders) {
        const candidate = path.join(folder.uri.fsPath, "help", "AcurityHelp.htm");
        if (fs.existsSync(candidate)) {
            return toBaseUri(path.join(folder.uri.fsPath, "help"));
        }
    }

    // 3. Help location unknown — ask before opening the file picker.
    const answer = await vscode.window.showInformationMessage(
        "Acurity help location is not configured. Would you like to locate AcurityHelp.htm now?",
        "Locate file",
        "Cancel"
    );
    if (answer !== "Locate file") return null;

    const picked = await vscode.window.showOpenDialog({
        canSelectFiles: true,
        canSelectMany: false,
        filters: { "Acurity Help File": ["htm", "html"] },
        title: "Locate AcurityHelp.htm"
    });

    if (!picked || picked.length === 0) return null;

    const helpDir = path.dirname(picked[0].fsPath);

    // Save immediately — the user confirmed intent in the preceding prompt.
    await config.update("webRoot", helpDir, vscode.ConfigurationTarget.Global);

    return toBaseUri(helpDir);
}

// ---------------------------------------------------------------------------
// Command handler
// ---------------------------------------------------------------------------

/**
 * Opens the contextual help page for the token at the cursor.
 * Bound to F1 when an Acurity document is active.
 */
async function openContextHelp() {
    const editor = vscode.window.activeTextEditor;
    if (!editor) return;

    const token = getTokenAtPosition(editor.document, editor.selection.active);
    if (!token) {
        vscode.window.showInformationMessage("No keyword found at cursor position.");
        return;
    }

    const base = await resolveHelpBase();
    if (!base) return; // user cancelled file picker

    const page = getHelpPage(token.bare);
    const fragment = page
        ? encodeURIComponent(page)
        : `search-${encodeURIComponent(token.bare.toUpperCase())}`;

    const url = `${base}/AcurityHelp.htm#${fragment}`;

    log(`[openContextHelp] token: ${token.display}, page: ${page ?? "(search)"}, url: ${url}`);

    openUrlInBrowser(url);
}

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

/**
 * Registers the acurity.openContextHelp command with the extension context.
 *
 * @param {vscode.ExtensionContext} context
 */
function registerOpenContextHelpCommand(context) {
    context.subscriptions.push(
        vscode.commands.registerCommand("acurity.openContextHelp", openContextHelp)
    );
}

export { registerOpenContextHelpCommand };
