import {
    addNodeToHostGraph,
    createHostNode,
    getFirstSelectedHostCanvasNode,
    getHostGraph,
    importWorkflowIntoHostCanvas,
    refreshHostCanvasGraph,
} from "../../../app/hostAdapter.js";
import { copyTextToClipboard } from "../../../utils/dom.js";
import { writeWidgetValue } from "../workflowSidebar/widgetAdapters.js";
import {
    getNodeType,
    getNodeParamEntries,
    getNodeWidgetValueEntries,
} from "./workflowGraphMapData.js";

type LooseRecord = Record<string, any>;
type WidgetRecord = LooseRecord;

interface TransferResult {
    ok: boolean;
    count: number;
    reason: "no-target" | "no-widgets" | "ok" | "no-match";
    targetNode?: LooseRecord;
}

export async function copyNodeJson(node: any): Promise<boolean> {
    if (!node) return false;
    const text = JSON.stringify(_prepareNodeForExternalUse(node), null, 2);
    return copyTextToClipboard(text);
}

export async function copyNodeParamValue(value: any): Promise<boolean> {
    return copyTextToClipboard(formatParamClipboardValue(value));
}

export function formatParamClipboardValue(value: any): string {
    if (value == null) return "";
    if (typeof value === "string") return value;
    if (typeof value === "number" || typeof value === "boolean") return String(value);
    try {
        return JSON.stringify(value, null, 2);
    } catch {
        return String(value);
    }
}

export function importWorkflowToCanvas(workflow: any): boolean {
    return importWorkflowIntoHostCanvas(workflow);
}

export function importNodeToCurrentGraph(node: LooseRecord | null | undefined): boolean {
    const graph = getHostGraph();
    if (!node || !graph || typeof graph.add !== "function") return false;

    const nodeType = String(node?.type || node?.class_type || node?.comfyClass || "").trim();
    let created: LooseRecord | null = null;

    try {
        created = createHostNode(nodeType) as LooseRecord | null;
    } catch (e: any) {
        console.debug?.("[MFV Graph Map] createNode failed", e);
    }

    if (!created) return false;

    try {
        const clone =
            typeof structuredClone === "function"
                ? structuredClone(node)
                : JSON.parse(JSON.stringify(node));
        _normalizeExternalNodeClone(clone, node);
        delete clone.id;
        if (Array.isArray(clone.pos)) {
            clone.pos = [Number(clone.pos[0] || 0) + 32, Number(clone.pos[1] || 0) + 32];
        }
        if (typeof created.configure === "function") {
            created.configure(clone);
        } else {
            Object.assign(created, clone);
        }
        return addNodeToHostGraph(created);
    } catch (e: any) {
        console.debug?.("[MFV Graph Map] import node failed", e);
        return false;
    }
}

export function transferNodeParamsToSelectedCanvasNode(
    sourceNode: LooseRecord | null | undefined,
): TransferResult {
    const targetNode = getSingleSelectedCanvasNode();
    if (!sourceNode || !targetNode) return { ok: false, count: 0, reason: "no-target" };

    const sourceEntries = _getTransferParamEntries(sourceNode);
    const targetWidgets = Array.isArray(targetNode.widgets) ? targetNode.widgets : [];
    if (!sourceEntries.length || !targetWidgets.length) {
        return { ok: false, count: 0, reason: "no-widgets" };
    }

    const targetByName = new Map<string, { widget: WidgetRecord; index: number }>();
    targetWidgets.forEach((widget, index) => {
        for (const key of _widgetKeys(widget)) {
            if (!targetByName.has(key)) targetByName.set(key, { widget, index });
        }
    });

    const sourceType = _normalizeType(getNodeType(sourceNode));
    const targetType = _normalizeType(targetNode?.type || targetNode?.comfyClass || targetNode?.class_type);
    const allowPositionFallback = Boolean(sourceType && targetType && sourceType === targetType);
    const usedTargets = new Set<number>();
    let count = 0;

    for (const entry of sourceEntries) {
        const key = _normalizeName(entry.label);
        let target = key ? targetByName.get(key) : null;
        if ((!target || usedTargets.has(target.index)) && allowPositionFallback && Number.isInteger(entry.index)) {
            const positional = targetWidgets[entry.index];
            if (positional) target = { widget: positional, index: entry.index };
        }
        if (!target || usedTargets.has(target.index)) continue;
        if (writeWidgetValue(target.widget, _cloneValue(entry.value), targetNode)) {
            usedTargets.add(target.index);
            count += 1;
        }
    }

    refreshHostCanvasGraph();

    return {
        ok: count > 0,
        count,
        reason: count > 0 ? "ok" : "no-match",
        targetNode,
    };
}

export function getSingleSelectedCanvasNode(): LooseRecord | null {
    return (getFirstSelectedHostCanvasNode() as LooseRecord | null) || null;
}

function _getTransferParamEntries(sourceNode: LooseRecord): Array<{ label: any; value: any; index?: number }> {
    const paramEntries = getNodeParamEntries(sourceNode);
    if (Array.isArray(paramEntries) && paramEntries.length) {
        return paramEntries.map(([label, value]) => ({ label, value }));
    }
    return getNodeWidgetValueEntries(sourceNode);
}

function _prepareNodeForExternalUse(node: LooseRecord): LooseRecord {
    const clone =
        typeof structuredClone === "function"
            ? structuredClone(node)
            : JSON.parse(JSON.stringify(node));
    _normalizeExternalNodeClone(clone, node);
    return clone;
}

function _normalizeExternalNodeClone(clone: LooseRecord, sourceNode: LooseRecord) {
    const promptInputs =
        sourceNode?._mjrPromptInputs && typeof sourceNode._mjrPromptInputs === "object"
            ? sourceNode._mjrPromptInputs
            : null;
    if (promptInputs) {
        const normalizedWidgets = _buildKnownWidgetValuesFromPrompt(sourceNode, promptInputs);
        if (normalizedWidgets) clone.widgets_values = normalizedWidgets;
    }
    delete clone._mjrPromptInputs;
    delete clone._mjrSubgraphProxyParams;
}

function _buildKnownWidgetValuesFromPrompt(sourceNode: LooseRecord, promptInputs: LooseRecord): any[] | null {
    const type = _normalizeType(getNodeType(sourceNode));
    const current = getNodeWidgetValueEntries(sourceNode);
    const currentByLabel = new Map(current.map((entry) => [_normalizeName(entry.label), entry.value]));
    if (type.includes("ksamplerselect")) {
        if (promptInputs.sampler_name == null) return null;
        return [_cloneValue(promptInputs.sampler_name)];
    }
    if (type.includes("ksampler")) {
        const controlAfterGenerate =
            promptInputs.control_after_generate ?? currentByLabel.get("control_after_generate") ?? current[1]?.value;
        return [
            _cloneValue(promptInputs.seed),
            _cloneValue(controlAfterGenerate),
            _cloneValue(promptInputs.steps),
            _cloneValue(promptInputs.cfg),
            _cloneValue(promptInputs.sampler_name),
            _cloneValue(promptInputs.scheduler),
            _cloneValue(promptInputs.denoise),
        ];
    }
    return null;
}

function _widgetKeys(widget: WidgetRecord): string[] {
    return [widget?.name, widget?.label, widget?.options?.name, widget?.options?.label]
        .map(_normalizeName)
        .filter(Boolean);
}

function _normalizeName(value: any): string {
    return String(value ?? "")
        .trim()
        .toLowerCase()
        .replace(/\s+/g, "_");
}

function _normalizeType(value: any): string {
    return String(value ?? "").trim().toLowerCase();
}

function _cloneValue(value: any): any {
    if (value == null || typeof value !== "object") return value;
    try {
        return typeof structuredClone === "function"
            ? structuredClone(value)
            : JSON.parse(JSON.stringify(value));
    } catch {
        return value;
    }
}
