/**
 * API Endpoints - Centralized URL constants
 */

import { normalizeAssetId, pickRootId } from "../utils/ids.js";
import { hasWindowsDrivePrefix, splitFilenameAndSubfolder, toPosixPath } from "../utils/path.js";

const DEFAULT_QUERY_LIMIT = 200;
const DEFAULT_QUERY_OFFSET = 0;

export interface AssetFilterParams {
    subfolder?: string | null;
    kind?: string | null;
    hasWorkflow?: boolean | null;
    minRating?: number | null;
    minSizeMB?: number | null;
    maxSizeMB?: number | null;
    minWidth?: number | null;
    minHeight?: number | null;
    maxWidth?: number | null;
    maxHeight?: number | null;
    workflowType?: string | null;
    workflowId?: string | null;
    workflowModel?: string | null;
    runsOn?: string | null;
    dateRange?: string | null;
    dateExact?: string | null;
}

export interface ListURLParams extends AssetFilterParams {
    q?: string;
    limit?: number;
    offset?: number;
    scope?: string;
    customRootId?: string | null;
    resolutionCompare?: string | null;
    sort?: string | null;
    cursor?: string | null;
    includeTotal?: boolean;
    groupStacks?: boolean;
    metadataMode?: string | null;
}

export const ENDPOINTS = {
    // Health & Config
    HEALTH: "/mjr/am/health",
    HEALTH_COUNTERS: "/mjr/am/health/counters",
    HEALTH_DB: "/mjr/am/health/db",
    STATUS: "/mjr/am/status",
    RUNTIME_EXECUTION: "/mjr/am/runtime/execution",
    CONFIG: "/mjr/am/config",
    VERSION: "/mjr/am/version",

    // Index & Search
    SCAN: "/mjr/am/scan",
    INDEX_FILES: "/mjr/am/index-files",
    INDEX_RESET: "/mjr/am/index/reset",
    DB_RESET: "/mjr/am/db/reset",
    SEARCH: "/mjr/am/search",
    LIST: "/mjr/am/list",
    ROOTS: "/mjr/am/roots",
    CUSTOM_ROOTS: "/mjr/am/custom-roots",
    CUSTOM_ROOTS_REMOVE: "/mjr/am/custom-roots/remove",
    BROWSER_FOLDER_OP: "/mjr/am/browser/folder-op",
    BROWSE_FOLDER: "/mjr/sys/browse-folder",
    FOLDER_INFO: "/mjr/am/folder-info",

    // Metadata
    METADATA: "/mjr/am/metadata",
    METADATA_CATALOG: "/mjr/am/metadata/catalog",
    METADATA_KEYS: "/mjr/am/metadata/keys",
    METADATA_PARSER_VERSION: "/mjr/am/metadata/parser-version",
    WORKFLOW_QUICK: "/mjr/am/workflow-quick",
    RETRY_SERVICES: "/mjr/am/retry-services",
    STAGE_TO_INPUT: "/mjr/am/stage-to-input",
    OPEN_IN_FOLDER: "/mjr/am/open-in-folder",
    COLLECT_FILES: "/mjr/am/collect-files",
    TOOLS_STATUS: "/mjr/am/tools/status",
    SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory",
    SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory",
    SETTINGS_WORKFLOW_ROOTS: "/mjr/am/settings/workflow-roots",
    SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback",
    SETTINGS_VECTOR_SEARCH: "/mjr/am/settings/vector-search",
    SETTINGS_VECTOR_SEARCH_UNLOAD: "/mjr/am/settings/vector-search/unload",
    SETTINGS_EXECUTION_GROUPING: "/mjr/am/settings/execution-grouping",
    SETTINGS_HUGGINGFACE: "/mjr/am/settings/huggingface",
    SETTINGS_AI_LOGGING: "/mjr/am/settings/ai-logging",
    SETTINGS_ROUTE_LOGGING: "/mjr/am/settings/route-logging",
    SETTINGS_STARTUP_LOGGING: "/mjr/am/settings/startup-logging",
    SETTINGS_LTXAV_RGB_FALLBACK: "/mjr/am/settings/ltxav-rgb-fallback",
    SETTINGS_JXL: "/mjr/am/settings/jxl",

    // View (ComfyUI native)
    VIEW: "/view",

    // Custom roots view (Majoor)
    CUSTOM_VIEW: "/mjr/am/custom-view",

    // Viewer helpers (Majoor)
    VIEWER_INFO: "/mjr/am/viewer/info",
    VIEWER_RESOURCE: "/mjr/am/viewer/resource",
    THUMBNAIL: "/mjr/am/thumbnail",

    // File upload
    UPLOAD_INPUT: "/mjr/am/upload_input",

    // File download
    DOWNLOAD: "/mjr/am/download",
    DOWNLOAD_CLEAN: "/mjr/am/download-clean",

    // Drag-out (OS) helpers
    BATCH_ZIP_CREATE: "/mjr/am/batch-zip",

    // Calendar helpers
    DATE_HISTOGRAM: "/mjr/am/date-histogram",

    // Asset operations
    ASSET_DELETE: "/mjr/am/asset/delete",
    ASSET_RENAME: "/mjr/am/asset/rename",

    // Watcher
    WATCHER_STATUS: "/mjr/am/watcher/status",
    WATCHER_TOGGLE: "/mjr/am/watcher/toggle",
    WATCHER_SCOPE: "/mjr/am/watcher/scope",
    WATCHER_SETTINGS: "/mjr/am/watcher/settings",

    // Collections
    COLLECTIONS: "/mjr/am/collections",

    // Stacks
    STACKS: "/mjr/am/stacks",
    STACKS_MERGE: "/mjr/am/stacks/merge",
    STACKS_DISSOLVE: "/mjr/am/stacks/dissolve",
    STACKS_AUTO_STACK: "/mjr/am/stacks/auto-stack",

    // Duplicate detection
    DUPLICATES_ALERTS: "/mjr/am/duplicates/alerts",
    DUPLICATES_ANALYZE: "/mjr/am/duplicates/analyze",
    DUPLICATES_MERGE_TAGS: "/mjr/am/duplicates/merge-tags",

    // DB backup/restore
    DB_BACKUPS: "/mjr/am/db/backups",
    DB_BACKUP_SAVE: "/mjr/am/db/backup-save",
    DB_BACKUP_RESTORE: "/mjr/am/db/backup-restore",

    // Vector / Semantic search
    VECTOR_SEARCH: "/mjr/am/vector/search",
    VECTOR_SIMILAR: "/mjr/am/vector/similar",
    VECTOR_ALIGNMENT: "/mjr/am/vector/alignment",
    VECTOR_AUTO_TAGS: "/mjr/am/vector/auto-tags",
    VECTOR_CAPTION: "/mjr/am/vector/caption",
    // Legacy alias kept for backward compatibility.
    VECTOR_ENHANCED_PROMPT: "/mjr/am/vector/enhanced-prompt",
    VECTOR_INDEX: "/mjr/am/vector/index",
    VECTOR_STATS: "/mjr/am/vector/stats",
    VECTOR_SUGGEST_COLLECTIONS: "/mjr/am/vector/suggest-collections",
    VECTOR_BACKFILL: "/mjr/am/db/backfill-missing-vectors",
    VECTOR_BACKFILL_STATUS: "/mjr/am/db/backfill-missing-vectors/status",

    // Hybrid search (FTS + semantic)
    HYBRID_SEARCH: "/mjr/am/search/hybrid",

    // Library audit
    AUDIT: "/mjr/am/audit",

    // Workflow library
    WORKFLOWS_CONTENT: "/mjr/am/workflows/content",
    WORKFLOWS_VALIDATE: "/mjr/am/workflows/validate",
    WORKFLOWS_VERSIONS: "/mjr/am/workflows/versions",
    WORKFLOWS_DIFF: "/mjr/am/workflows/diff",
    WORKFLOWS_SAVE: "/mjr/am/workflows/save",
    WORKFLOWS_DUPLICATE: "/mjr/am/workflows/duplicate",
    WORKFLOWS_MOVE: "/mjr/am/workflows/move",
    WORKFLOWS_DELETE: "/mjr/am/workflows/delete",
    WORKFLOWS_MARK_LOADED: "/mjr/am/workflows/mark-loaded",
    WORKFLOWS_FAVORITE: "/mjr/am/workflows/favorite",
    WORKFLOWS_INFO: "/mjr/am/workflows/info",
    WORKFLOWS_THUMBNAIL_CANDIDATES: "/mjr/am/workflows/thumbnail-candidates",
    WORKFLOWS_MODEL_FAMILIES: "/mjr/am/workflows/model-families",
    WORKFLOWS_TAGS: "/mjr/am/workflows/tags",
    WORKFLOWS_THUMBNAIL_SET: "/mjr/am/workflows/thumbnail/set",
    WORKFLOWS_OPEN_ROOT: "/mjr/am/workflows/open-root",
};

export function appendAssetFilterQueryParams(url: string, filters: AssetFilterParams = {}): string {
    let nextUrl = String(url || "");
    // Use correct separator based on whether URL already has query params
    const _sep = () => {
        const s = nextUrl.includes("?") ? "&" : "?";
        return s;
    };
    const _append = (param: any) => {
        nextUrl += _sep() + param;
    };
    const {
        subfolder = null,
        kind = null,
        hasWorkflow = null,
        minRating = null,
        minSizeMB = null,
        maxSizeMB = null,
        minWidth = null,
        minHeight = null,
        maxWidth = null,
        maxHeight = null,
        workflowType = null,
        workflowId = null,
        workflowModel = null,
        runsOn = null,
        dateRange = null,
        dateExact = null,
    } = filters || {};

    if (subfolder) {
        _append(`subfolder=${encodeURIComponent(String(subfolder))}`);
    }
    if (kind) {
        _append(`kind=${encodeURIComponent(kind)}`);
    }
    if (hasWorkflow !== null && hasWorkflow !== undefined) {
        _append(`has_workflow=${encodeURIComponent(hasWorkflow ? "true" : "false")}`);
    }
    if (minRating !== null && minRating !== undefined && Number(minRating) > 0) {
        _append(`min_rating=${encodeURIComponent(String(minRating))}`);
    }
    if (minSizeMB !== null && minSizeMB !== undefined && Number(minSizeMB) > 0) {
        _append(`min_size_mb=${encodeURIComponent(String(minSizeMB))}`);
    }
    if (maxSizeMB !== null && maxSizeMB !== undefined && Number(maxSizeMB) > 0) {
        _append(`max_size_mb=${encodeURIComponent(String(maxSizeMB))}`);
    }
    if (minWidth !== null && minWidth !== undefined && Number(minWidth) > 0) {
        _append(`min_width=${encodeURIComponent(String(minWidth))}`);
    }
    if (minHeight !== null && minHeight !== undefined && Number(minHeight) > 0) {
        _append(`min_height=${encodeURIComponent(String(minHeight))}`);
    }
    if (maxWidth !== null && maxWidth !== undefined && Number(maxWidth) > 0) {
        _append(`max_width=${encodeURIComponent(String(maxWidth))}`);
    }
    if (maxHeight !== null && maxHeight !== undefined && Number(maxHeight) > 0) {
        _append(`max_height=${encodeURIComponent(String(maxHeight))}`);
    }
    if (workflowType) {
        _append(`workflow_type=${encodeURIComponent(String(workflowType))}`);
    }
    if (workflowId) {
        _append(`workflow_id=${encodeURIComponent(String(workflowId))}`);
    }
    if (workflowModel) {
        _append(`workflow_model=${encodeURIComponent(String(workflowModel))}`);
    }
    if (runsOn) {
        _append(`runs_on=${encodeURIComponent(String(runsOn))}`);
    }
    if (dateRange) {
        _append(`date_range=${encodeURIComponent(String(dateRange))}`);
    }
    if (dateExact) {
        _append(`date_exact=${encodeURIComponent(String(dateExact))}`);
    }
    return nextUrl;
}

/**
 * Build view URL for asset
 */
export function buildViewURL(filename: any, subfolder: string | null = null, type = "output"): string {
    const fn = String(filename || "").trim();
    if (!fn) return "";
    let url = `${ENDPOINTS.VIEW}?filename=${encodeURIComponent(fn)}`;
    if (subfolder) {
        url += `&subfolder=${encodeURIComponent(subfolder)}`;
    }
    url += `&type=${encodeURIComponent(type)}`;
    return url;
}

export function buildThumbnailURL(asset: any, size = 384): string {
    const filepath = String(asset?.filepath || asset?.path || asset?.file_path || "").trim();
    if (!filepath) return "";
    const requestedSize = Math.max(64, Math.min(1024, Math.floor(Number(size) || 384)));
    return `${ENDPOINTS.THUMBNAIL}?filepath=${encodeURIComponent(filepath)}&size=${encodeURIComponent(String(requestedSize))}`;
}

/**
 * Build list URL with scope support.
 */
export function buildListURL(params: ListURLParams = {}): string {
    const {
        q = "*",
        limit = DEFAULT_QUERY_LIMIT,
        offset = DEFAULT_QUERY_OFFSET,
        scope = "output",
        subfolder = "",
        customRootId = null,
        kind = null,
        hasWorkflow = null,
        minRating = null,
        minSizeMB = null,
        maxSizeMB = null,
        resolutionCompare = null,
        minWidth = null,
        minHeight = null,
        maxWidth = null,
        maxHeight = null,
        workflowType = null,
        workflowId = null,
        workflowModel = null,
        runsOn = null,
        dateRange = null,
        dateExact = null,
        sort = null,
        cursor = null,
        includeTotal = true,
        groupStacks = false,
        metadataMode = null,
    } = params;

    let url = `${ENDPOINTS.LIST}?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}&scope=${encodeURIComponent(scope)}`;
    if (subfolder) {
        url += `&subfolder=${encodeURIComponent(subfolder)}`;
    }
    if (customRootId) {
        url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
    } else if (String(scope || "").toLowerCase() === "custom") {
        // Browser scope without selected root uses absolute filesystem paths.
        url += "&browser_mode=1";
    }
    url = appendAssetFilterQueryParams(url, {
        kind,
        hasWorkflow,
        minRating,
        minSizeMB,
        maxSizeMB,
        minWidth,
        minHeight,
        maxWidth,
        maxHeight,
        workflowType,
        workflowId,
        workflowModel,
        runsOn,
        dateRange,
        dateExact,
    });
    if (resolutionCompare) {
        url += `&resolution_compare=${encodeURIComponent(String(resolutionCompare))}`;
    }
    if (sort) {
        url += `&sort=${encodeURIComponent(String(sort))}`;
    }
    if (cursor) {
        url += `&cursor=${encodeURIComponent(String(cursor))}`;
    }
    if (includeTotal === false) {
        url += `&include_total=0`;
    }
    if (groupStacks) {
        url += "&group_stacks=1";
    }
    if (metadataMode) {
        url += `&metadata_mode=${encodeURIComponent(String(metadataMode).toUpperCase())}`;
    }
    return url;
}

export function buildCustomViewURL(filename: any, subfolder = "", rootId = ""): string {
    const fn = String(filename || "").trim();
    const rid = String(rootId || "").trim();
    if (!fn || !rid) return "";
    let url = `${ENDPOINTS.CUSTOM_VIEW}?root_id=${encodeURIComponent(rid)}&filename=${encodeURIComponent(fn)}`;
    if (subfolder) {
        url += `&subfolder=${encodeURIComponent(subfolder)}`;
    }
    return url;
}

export function buildBatchZipDownloadURL(token: any): string {
    return `${ENDPOINTS.BATCH_ZIP_CREATE}/${encodeURIComponent(String(token || ""))}`;
}

export function buildStacksURL({ limit = 50, offset = 0, includeTotal = true }: { limit?: number; offset?: number; includeTotal?: boolean } = {}): string {
    let url = `${ENDPOINTS.STACKS}?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`;
    if (includeTotal === false) url += "&include_total=0";
    return url;
}

export function buildStackURL(stackId: any): string {
    return `${ENDPOINTS.STACKS}/${encodeURIComponent(String(stackId || ""))}`;
}

export function buildStackMembersURL(stackId: any, options: { limit?: number } = {}): string {
    let url = `${buildStackURL(stackId)}/members`;
    const limit = Number(options?.limit || 0);
    if (Number.isFinite(limit) && limit > 0) {
        url += `?limit=${encodeURIComponent(String(Math.floor(limit)))}`;
    }
    return url;
}

export function buildStackByJobURL(jobId: any): string {
    return `${ENDPOINTS.STACKS}/by-job/${encodeURIComponent(String(jobId || ""))}`;
}

export function buildNodeContextMembersURL(
    sourceNodeId: any,
    options: { jobId?: string; job_id?: string; latest?: boolean; limit?: number } = {},
): string {
    const nodeId = String(sourceNodeId || "").trim();
    let url = `${ENDPOINTS.STACKS}/by-node/${encodeURIComponent(nodeId)}/members`;
    const params = [];
    const jobId = String(options?.jobId || options?.job_id || "").trim();
    if (jobId) params.push(`job_id=${encodeURIComponent(jobId)}`);
    if (options?.latest === false) params.push("latest=0");
    const limit = Number(options?.limit || 0);
    if (Number.isFinite(limit) && limit > 0) {
        params.push(`limit=${encodeURIComponent(String(Math.trunc(limit)))}`);
    }
    if (params.length) url += `?${params.join("&")}`;
    return url;
}

export function buildStackCoverURL(stackId: any): string {
    return `${buildStackURL(stackId)}/cover`;
}

export function buildStackRenameURL(stackId: any): string {
    return `${buildStackURL(stackId)}/rename`;
}

export function buildViewerResourceURL(assetOrContext: any, relpath = ""): string {
    const ctx = (assetOrContext && typeof assetOrContext === "object" ? assetOrContext : {}) as Record<string, any>;
    const rel = String(relpath || "").trim();
    if (!rel) return "";

    const assetId = normalizeAssetId(ctx?.asset_id ?? ctx?.id ?? null);
    let url = `${ENDPOINTS.VIEWER_RESOURCE}?relpath=${encodeURIComponent(rel)}`;
    if (assetId) {
        return `${url}&asset_id=${encodeURIComponent(assetId)}`;
    }

    const filepath = toPosixPath(
        String(
            ctx?.filepath ||
                ctx?.path ||
                ctx?.fullpath ||
                ctx?.full_path ||
                ctx?.file_info?.filepath ||
                ctx?.file_info?.path ||
                "",
        ).trim(),
    );
    if (filepath) {
        return `${url}&filepath=${encodeURIComponent(filepath)}`;
    }

    const filename = String(ctx?.filename || ctx?.name || ctx?.file_info?.filename || "").trim();
    if (!filename) return "";
    url += `&filename=${encodeURIComponent(filename)}`;

    const subfolder = String(ctx?.subfolder || ctx?.file_info?.subfolder || "").trim();
    if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;

    const type = String(ctx?.type || ctx?.file_info?.type || "")
        .trim()
        .toLowerCase();
    if (type && type !== "custom") {
        url += `&type=${encodeURIComponent(type)}`;
    }

    const rootId = String(pickRootId(ctx) || ctx?.root_id || ctx?.custom_root_id || "").trim();
    if (rootId) {
        url += `&root_id=${encodeURIComponent(rootId)}`;
    }
    return url;
}

export function buildDateHistogramURL(params: AssetFilterParams & { scope?: string; customRootId?: string | null; month?: string } = {}): string {
    const {
        scope = "output",
        customRootId = null,
        month = "",
        kind = null,
        hasWorkflow = null,
        minRating = null,
    } = params;

    let url = `${ENDPOINTS.DATE_HISTOGRAM}?scope=${encodeURIComponent(scope)}&month=${encodeURIComponent(String(month || ""))}`;
    if (customRootId) {
        url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
    }
    return appendAssetFilterQueryParams(url, {
        subfolder: params.subfolder ?? null,
        kind,
        hasWorkflow,
        minRating,
        minSizeMB: params.minSizeMB ?? null,
        maxSizeMB: params.maxSizeMB ?? null,
        minWidth: params.minWidth ?? null,
        minHeight: params.minHeight ?? null,
        maxWidth: params.maxWidth ?? null,
        maxHeight: params.maxHeight ?? null,
        workflowType: params.workflowType ?? null,
        workflowId: params.workflowId ?? null,
        workflowModel: params.workflowModel ?? null,
        runsOn: params.runsOn ?? null,
        dateRange: params.dateRange ?? null,
        dateExact: params.dateExact ?? null,
    });
}

export function buildAssetViewURL(asset: Record<string, any> | null | undefined): string {
    const mtime = asset?.mtime;
    const withMtime = (url: any) => {
        if (!url || !mtime) return url;
        return `${url}${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(mtime)}`;
    };
    const rawPath = toPosixPath(
        String(
            asset?.filepath ||
                asset?.path ||
                asset?.fullpath ||
                asset?.full_path ||
                asset?.file_info?.filepath ||
                asset?.file_info?.path ||
                "",
        ).trim(),
    );
    const declaredType = String(asset?.type || asset?.file_info?.type || "")
        .toLowerCase()
        .trim();
    const assetId = Number(asset?.id ?? asset?.asset_id);
    const isTemporaryAsset = declaredType === "temp" || rawPath.toLowerCase().includes("/temp/");
    if (!isTemporaryAsset && Number.isSafeInteger(assetId) && assetId > 0) {
        return withMtime(`/mjr/am/viewer/asset/${encodeURIComponent(String(assetId))}`);
    }
    let filename = String(
        asset?.filename || asset?.name || asset?.file_info?.filename || "",
    ).trim();
    let subfolder = String(asset?.subfolder || asset?.file_info?.subfolder || "").trim();
    const pickFromPath = (p: any) => {
        const out = { type: "", subfolder: "", filename: "" };
        const normalized = toPosixPath(p);
        if (!normalized) return out;
        const lower = normalized.toLowerCase();
        const idxOut = lower.indexOf("/output/");
        const idxIn = lower.indexOf("/input/");
        const idxTemp = lower.indexOf("/temp/");
        let baseIdx = -1;
        if (idxOut >= 0) {
            out.type = "output";
            baseIdx = idxOut + "/output/".length;
        } else if (idxIn >= 0) {
            out.type = "input";
            baseIdx = idxIn + "/input/".length;
        } else if (idxTemp >= 0) {
            out.type = "temp";
            baseIdx = idxTemp + "/temp/".length;
        }
        if (baseIdx >= 0) {
            const rel = normalized.slice(baseIdx);
            const slash = rel.lastIndexOf("/");
            if (slash >= 0) {
                out.subfolder = rel.slice(0, slash);
                out.filename = rel.slice(slash + 1);
            } else {
                out.filename = rel;
            }
        } else {
            const split = splitFilenameAndSubfolder(normalized);
            out.filename = split.filename;
        }
        return out;
    };
    const fromPath = pickFromPath(rawPath);
    // If filename accidentally contains a relative path, split it for /view API.
    if (!subfolder && filename.includes("/")) {
        const idx = filename.lastIndexOf("/");
        if (idx > 0) {
            subfolder = filename.slice(0, idx);
            filename = filename.slice(idx + 1);
        }
    }
    if (!filename && fromPath.filename) filename = fromPath.filename;
    if (!subfolder && fromPath.subfolder) subfolder = fromPath.subfolder;
    if (!filename) return "";

    let type = String(asset?.type || asset?.file_info?.type || "")
        .toLowerCase()
        .trim();
    if (type !== "input" && type !== "output" && type !== "temp" && type !== "custom") type = "";
    if (!type && fromPath.type) type = fromPath.type;
    if (!type && rawPath) {
        if (rawPath.includes("/input/")) type = "input";
        else if (rawPath.includes("/output/")) type = "output";
        else if (rawPath.includes("/temp/")) type = "temp";
    }
    if (!type) type = "output";

    const hasNativeBucket =
        rawPath.includes("/output/") || rawPath.includes("/input/") || rawPath.includes("/temp/");
    const subfolderLooksAbsolute = hasWindowsDrivePrefix(subfolder) || subfolder.startsWith("/");
    // Fallback: non-native output/input roots (or broken absolute subfolder values)
    // cannot be served by ComfyUI `/view`, so use backend filepath streaming URL.
    if (rawPath && type !== "custom" && (subfolderLooksAbsolute || !hasNativeBucket)) {
        return withMtime(buildDownloadURL(rawPath, { inline: true }));
    }

    if (type === "custom") {
        const rid = String(pickRootId(asset) || "").trim();
        if (rid) return withMtime(buildCustomViewURL(filename, subfolder, rid));
        if (rawPath) {
            return withMtime(
                `${ENDPOINTS.CUSTOM_VIEW}?filepath=${encodeURIComponent(rawPath)}&browser_mode=1`,
            );
        }
        // Fallback for malformed custom assets without root id.
        const fallbackType = fromPath.type || "output";
        return withMtime(buildViewURL(filename, subfolder, fallbackType));
    }
    // Prefer path-based type when explicit type conflicts with obvious filepath bucket.
    if (rawPath.includes("/output/")) type = "output";
    if (rawPath.includes("/input/")) type = "input";
    if (rawPath.includes("/temp/")) type = "temp";
    return withMtime(buildViewURL(filename, subfolder, type));
}

/**
 * Build download URL for asset
 */
export function buildDownloadURL(filepath: any, options: Record<string, any> = {}) {
    if (!filepath) return "";
    const inline = !!options?.inline;
    let url = `${ENDPOINTS.DOWNLOAD}?filepath=${encodeURIComponent(filepath)}`;
    if (inline) url += "&preview=1";
    return url;
}

export function buildWorkflowContentURL(filepath: any): string {
    const fp = String(filepath || "").trim();
    if (!fp) return "";
    return `${ENDPOINTS.WORKFLOWS_CONTENT}?filepath=${encodeURIComponent(fp)}`;
}

export function buildWorkflowValidateURL(filepath: any): string {
    const fp = String(filepath || "").trim();
    if (!fp) return "";
    return `${ENDPOINTS.WORKFLOWS_VALIDATE}?filepath=${encodeURIComponent(fp)}`;
}

export function buildWorkflowVersionsURL(filepath: any): string {
    const fp = String(filepath || "").trim();
    if (!fp) return "";
    return `${ENDPOINTS.WORKFLOWS_VERSIONS}?filepath=${encodeURIComponent(fp)}`;
}

export function buildWorkflowDiffURL(filepath: any, version_filepath: any = ""): string {
    const fp = String(filepath || "").trim();
    if (!fp) return "";
    let url = `${ENDPOINTS.WORKFLOWS_DIFF}?filepath=${encodeURIComponent(fp)}`;
    const version = String(version_filepath || "").trim();
    if (version) url += `&version_filepath=${encodeURIComponent(version)}`;
    return url;
}

/**
 * Build download URL for asset with ComfyUI metadata stripped.
 */
export function buildCleanDownloadURL(filepath: any) {
    if (!filepath) return "";
    return `${ENDPOINTS.DOWNLOAD_CLEAN}?filepath=${encodeURIComponent(filepath)}`;
}
