import {
    comboHasAnyAudioValue,
    comboHasAnyImageValue,
    comboHasAnyModel3DValue,
    comboHasAnyVideoValue,
    looksLikeAudioPath,
    looksLikeImagePath,
    looksLikeModel3DPath,
    looksLikeVideoPath,
} from "../utils/video.js";

const highlightState = new WeakMap();

const getHighlightState = (app: any) => {
    if (!app || typeof app !== "object") {
        return { node: null, prev: null };
    }
    let state = highlightState.get(app);
    if (!state) {
        state = { node: null, prev: null };
        highlightState.set(app, state);
    }
    return state;
};

export const getNodeUnderClientXY = (app: any, clientX: any, clientY: any) => {
    const canvasEl = app?.canvas?.canvas || document.querySelector("canvas");
    const graph = app?.canvas?.graph;
    const ds = app?.canvas?.ds;
    if (!canvasEl || !graph || !ds) return null;

    const rect = canvasEl.getBoundingClientRect();
    if (
        clientX < rect.left ||
        clientX > rect.right ||
        clientY < rect.top ||
        clientY > rect.bottom
    ) {
        return null;
    }

    const scale = Number(ds.scale) || 1;
    const off = ds.offset || [0, 0];
    const offX = Array.isArray(off) ? Number(off[0]) || 0 : Number(off?.x) || 0;
    const offY = Array.isArray(off) ? Number(off[1]) || 0 : Number(off?.y) || 0;
    const x = (clientX - rect.left) / scale - offX;
    const y = (clientY - rect.top) / scale - offY;

    if (typeof graph.getNodeOnPos === "function") {
        return graph.getNodeOnPos(x, y);
    }

    const nodes = graph._nodes || [];
    for (let i = nodes.length - 1; i >= 0; i--) {
        const n = nodes[i];
        if (!n?.pos || !n?.size) continue;
        const width = n.size[0];
        let height = n.size[1];
        if (n.flags && n.flags.collapsed) {
            height = 30;
        }
        if (x >= n.pos[0] && y >= n.pos[1] && x <= n.pos[0] + width && y <= n.pos[1] + height) {
            return n;
        }
    }
    return null;
};

export const applyHighlight = (app: any, node: any, markCanvasDirty: any) => {
    const state = getHighlightState(app);
    if (!node || state.node === node) return;
    clearHighlight(app, markCanvasDirty);
    state.node = node;
    state.prev = { color: node.color, bgcolor: node.bgcolor };
    node.bgcolor = "#3355ff";
    node.color = "#a9c4ff";
    markCanvasDirty(app);
};

export const clearHighlight = (app: any, markCanvasDirty: any) => {
    const state = getHighlightState(app);
    if (!state.node) return;
    try {
        if (state.prev) {
            state.node.color = state.prev.color;
            state.node.bgcolor = state.prev.bgcolor;
        }
    } catch (e: any) {
        console.debug?.(e);
    }
    state.node = null;
    state.prev = null;
    markCanvasDirty(app);
};

export const ensureComboHasValue = (widget: any, value: any) => {
    if (!widget || widget.type !== "combo" || !widget.options) return;
    let vals = widget.options.values;
    let target = widget.options;

    if (typeof vals === "function") {
        return;
    }

    if (!Array.isArray(vals)) {
        if (vals && typeof vals === "object" && Array.isArray(vals.values)) {
            target = vals;
            vals = vals.values;
        } else {
            return;
        }
    }

    const has = vals.some((v: any) => {
        if (typeof v === "string") return v === value;
        return (v?.content ?? v?.value ?? v?.text) === value;
    });
    if (has) return;

    if (vals.length === 0 || typeof vals[0] === "string") {
        vals.unshift(value);
    } else {
        vals.unshift({ content: value, text: value, value });
    }
    target.values = vals;
};

// Widget types that can never hold a file path
const _REJECT_TYPES = new Set([
    "number",
    "int",
    "float",
    "boolean",
    "toggle",
    "checkbox",
    "button",
    "hidden",
]);
// Terms that indicate an output/save widget (strong negative signal)
const _OUTPUTY_TERMS = ["output", "save", "export", "folder", "dir"];
// Generic path-like widget name terms
const _PATH_TERMS = ["file", "path"];
// Terms that suggest a file-path input
const _PATH_HINT_TERMS = ["path", "file", "input", "src", "source"];

/**
 * Generic scored widget picker.
 * @param {object} node - LiteGraph node
 * @param {string} droppedExt - Extension of the dropped file (without leading dot)
 * @param {object} cfg
 * @param {Set<string>}    cfg.exactNames        - Widget names that match perfectly (+100)
 * @param {string[]}       cfg.knownNodeIncludes - Node-type substrings that flag a known loader
 * @param {string[]}       cfg.mediaTerms        - Media-specific widget name terms
 * @param {Array<{terms:string[], score:number}>} cfg.extraTerms - Extra scoring groups
 * @param {Set<string>}    cfg.exactSingleNames  - Names with value-conditional scoring
 * @param {Function}       cfg.looksLikeFn       - (value, ext) → bool
 * @param {Function}       cfg.comboChecker      - (widget, ext) → bool
 * @param {string}         cfg.scoreKey          - Debug property attached to the winning widget
 */
const _pickBestPathWidget = (node: any, droppedExt: any, cfg: any) => {
    const widgets = node?.widgets;
    if (!Array.isArray(widgets) || !widgets.length) return null;

    const ext = String(droppedExt || "")
        .toLowerCase()
        .replace(/^\./, "");
    const nodeType = String(node?.type || "").toLowerCase();
    const isKnownNode = cfg.knownNodeIncludes.some((p: any) => nodeType.includes(p));

    const candidates = [];
    for (const w of widgets) {
        if (!w) continue;
        const type = String(w?.type || "").toLowerCase();
        const value = w?.value;

        if (_REJECT_TYPES.has(type)) continue;
        if (typeof value === "number" || typeof value === "boolean") continue;

        const stringLikeByType = type === "text" || type === "string" || type === "combo";
        const stringLikeByCallback = typeof w?.callback === "function" && typeof value === "string";
        if (!stringLikeByType && !stringLikeByCallback) continue;

        const name = String(w?.name || w?.label || "")
            .toLowerCase()
            .trim();

        let score = 0;
        if (cfg.exactNames.has(name)) score += 100;

        if (name === "file" && isKnownNode && type === "combo" && cfg.comboChecker(w, ext)) {
            score += 100;
        }

        const hasMediaHint = cfg.mediaTerms.some((t: any) => name.includes(t));
        const hasPathHint = _PATH_HINT_TERMS.some((t: any) => name.includes(t));
        if (hasMediaHint && hasPathHint) score += 80;
        if (_PATH_TERMS.some((t: any) => name.includes(t))) score += 35;

        for (const { terms, score: pts } of cfg.extraTerms) {
            if (terms.some((t: any) => name.includes(t))) score += pts;
        }

        if (_OUTPUTY_TERMS.some((t: any) => name.includes(t))) score -= 90;

        if (cfg.exactSingleNames.has(name)) {
            const empty = typeof value === "string" && value.trim() === "";
            if (empty) score += 25;
            else if (cfg.looksLikeFn(value, ext)) score += 25;
            else score -= 10;
        }

        if (isKnownNode) score += 15;
        const emptyValue = typeof value === "string" && value.trim() === "";
        if (emptyValue) score += 3;
        if (type === "combo" && cfg.comboChecker(w, ext)) score += 12;

        candidates.push({ w, score, emptyValue, combo: type === "combo" });
    }

    if (!candidates.length) return null;
    candidates.sort((a: any, b: any) => {
        if (b.score !== a.score) return b.score - a.score;
        if (b.emptyValue !== a.emptyValue) return b.emptyValue ? 1 : -1;
        if (b.combo !== a.combo) return b.combo ? 1 : -1;
        return 0;
    });

    const best = candidates[0];
    if (!best || best.score < 20) return null;
    try {
        best.w[cfg.scoreKey] = best.score;
    } catch (e: any) {
        console.debug?.(e);
    }
    return best.w;
};

const _VIDEO_CFG = {
    exactNames: new Set([
        "video_path", "input_video", "source_video", "video", "driven_video", "footage",
        "input_path", "directory", "folder_path", "folder", "path", "video_directory"
    ]),
    knownNodeIncludes: [
        "loadvideo", "vhs_loadvideo", "videoloader", "sadtalker", "wav2lip", "reactor",
        "multiimageloader", "ltxdirector", "ltxsequencer", "ltxkeyframer"
    ],
    mediaTerms: ["video", "footage", "clip", "movie"],
    extraTerms: [{ terms: ["media", "clip", "footage", "drive"], score: 45 }],
    exactSingleNames: new Set(["video"]),
    looksLikeFn: looksLikeVideoPath,
    comboChecker: comboHasAnyVideoValue,
    scoreKey: "__mjrVideoPickScore",
};

const _IMAGE_CFG = {
    exactNames: new Set([
        "image", "image_path", "input_image", "source_image", "ref_image", "pose_image",
        "hint_image", "target_image", "ipadapter_image", "input_path", "directory",
        "folder_path", "folder", "path", "image_directory"
    ]),
    knownNodeIncludes: [
        "loadimage", "loadimagemask", "imageloader", "reactor", "roop", "ipadapter",
        "controlnet", "instantid", "pulid", "multiimageloader", "ltxdirector",
        "ltxsequencer", "ltxkeyframer"
    ],
    mediaTerms: ["image", "img", "mask", "frame", "photo", "picture", "face", "ipadapter"],
    extraTerms: [{ terms: ["media", "source", "first", "last", "target", "reference"], score: 35 }],
    exactSingleNames: new Set(["image", "face"]),
    looksLikeFn: looksLikeImagePath,
    comboChecker: comboHasAnyImageValue,
    scoreKey: "__mjrImagePickScore",
};

const _AUDIO_CFG = {
    exactNames: new Set([
        "audio_path", "input_audio", "source_audio", "audio", "driven_audio", "voice",
        "bgm", "soundtrack", "input_path", "directory", "folder_path", "folder",
        "path", "audio_directory"
    ]),
    knownNodeIncludes: [
        "loadaudio",
        "vhs_loadaudioupload",
        "vhs_loadaudio",
        "audioloader",
        "inputaudio",
        "sadtalker",
        "wav2lip",
        "multiimageloader",
        "ltxdirector",
        "ltxsequencer",
        "ltxkeyframer"
    ],
    mediaTerms: ["audio", "sound", "music", "voice", "speech", "wav", "mp3"],
    extraTerms: [{ terms: ["media", "track", "drive"], score: 45 }],
    exactSingleNames: new Set(["audio", "voice"]),
    looksLikeFn: looksLikeAudioPath,
    comboChecker: comboHasAnyAudioValue,
    scoreKey: "__mjrAudioPickScore",
};

const _MODEL3D_CFG = {
    exactNames: new Set([
        "model_path",
        "input_model",
        "source_model",
        "mesh_path",
        "input_mesh",
        "geometry_path",
        "scene_path",
        "point_cloud_path",
        "splat_path",
        "model",
        "mesh",
        "geometry",
        "input_path",
        "directory",
        "folder_path",
        "folder",
        "path",
        "model_directory"
    ]),
    knownNodeIncludes: [
        "load3d",
        "loadmodel",
        "loadmesh",
        "loadobj",
        "loadgltf",
        "loadglb",
        "loadstl",
        "loadply",
        "pointcloud",
        "meshloader",
        "modelloader",
        "tripo3d",
        "unique3d",
        "multiimageloader",
        "ltxdirector",
        "ltxsequencer",
        "ltxkeyframer"
    ],
    mediaTerms: ["model", "mesh", "geometry", "scene", "object", "point", "cloud", "splat"],
    extraTerms: [{ terms: ["asset", "resource"], score: 30 }],
    exactSingleNames: new Set(["model", "mesh", "geometry"]),
    looksLikeFn: looksLikeModel3DPath,
    comboChecker: comboHasAnyModel3DValue,
    scoreKey: "__mjrModel3DPickScore",
};

export const pickBestMediaPathWidget = (node: any, payload: any, droppedExt: any) => {
    const kind = String(payload?.kind || "").toLowerCase();
    const cfg =
        kind === "model3d"
            ? _MODEL3D_CFG
            : kind === "audio"
              ? _AUDIO_CFG
              : kind === "image"
                ? _IMAGE_CFG
                : _VIDEO_CFG;
    return _pickBestPathWidget(node, droppedExt, cfg);
};

export const getInputSlotUnderClientXY = (app: any, node: any, clientX: any, clientY: any) => {
    if (!node || !node.inputs || !node.inputs.length) return null;
    const canvasEl = app?.canvas?.canvas || document.querySelector("canvas");
    const ds = app?.canvas?.ds;
    if (!canvasEl || !ds) return null;

    const rect = canvasEl.getBoundingClientRect();
    const scale = Number(ds.scale) || 1;
    const off = ds.offset || [0, 0];
    const offX = Array.isArray(off) ? Number(off[0]) || 0 : Number(off?.x) || 0;
    const offY = Array.isArray(off) ? Number(off[1]) || 0 : Number(off?.y) || 0;

    const x = (clientX - rect.left) / scale - offX;
    const y = (clientY - rect.top) / scale - offY;

    const nodeX = node.pos[0];
    const nodeY = node.pos[1];

    const titleHeight = node.constructor.title_height || 30;
    const slotHeight = 20;

    for (let i = 0; i < node.inputs.length; i++) {
        const input = node.inputs[i];
        const slotY = nodeY + titleHeight + i * slotHeight + slotHeight / 2;

        const dx = x - nodeX;
        const dy = y - slotY;

        if (Math.abs(dx) <= 18 && Math.abs(dy) <= 12) {
            return { index: i, input };
        }
    }
    return null;
};
