/**
 * API Client - Safe fetch wrapper (never throws)
 */

import { SETTINGS_KEY } from "../app/settingsStore.js";
import {
    buildWorkflowContentURL,
    buildWorkflowDiffURL,
    buildWorkflowValidateURL,
    buildWorkflowVersionsURL,
    ENDPOINTS,
} from "./endpoints.js";
import { createApiFetchClient } from "./fetchUtils.js";
import { normalizeAssetId, pickRootId } from "../utils/ids.js";
import { createTTLCache } from "../utils/ttlCache.js";
import {
    readAuthToken as _readAuthToken,
    ensureWriteAuthToken,
    normalizeWriteAuthFailure as _normalizeWriteAuthFailure,
    invalidateAuthTokenCache,
    hasRuntimeSecurityToken,
    setRuntimeSecurityToken,
} from "./clientAuth.js";

/**
 * @template T
 * @typedef {{
 *  ok: boolean,
 *  data: (T|null),
 *  error?: (string|null),
 *  code?: string,
 *  meta?: any,
 *  status?: number
 * }} ApiResult
 */

/**
 * Compact viewer-oriented media info returned by `/mjr/am/viewer/info`.
 *
 * @typedef {{
 *  kind?: ('image'|'video'|'audio'|'model3d'|'unknown'),
 *  mime?: (string|null),
 *  width?: (number|null),
 *  height?: (number|null),
 *  fps?: (number|string|null),
 *  fps_raw?: (string|null),
 *  frame_count?: (number|null),
 *  duration_s?: (number|null),
 *  loader?: (string|null),
 *  previewable?: (boolean|null),
 *  interactive?: (boolean|null),
 *  resource_endpoint?: (string|null)
 * }} ViewerInfo
 */

export { setRuntimeSecurityToken, ensureWriteAuthToken, hasRuntimeSecurityToken };

const TAGS_CACHE_TTL_MS = 30_000;
const DEFAULT_TAGS_CACHE_TTL_MS = TAGS_CACHE_TTL_MS;
const CLIENT_GLOBAL_KEY = "__MJR_API_CLIENT__";
const SETTINGS_FAST_CACHE_TTL_MS = 2000;
const MAX_BATCH_ASSET_IDS = 200;
export const VECTOR_BACKFILL_DEFAULT_POLL_INTERVAL_MS = 1000;
export const VECTOR_BACKFILL_DEFAULT_POLL_TIMEOUT_MS = 30 * 60_000;
export const VECTOR_BACKFILL_MAX_POLL_TIMEOUT_MS = 12 * 60 * 60_000;
const SETTINGS_CACHE_KEY = "settings";
const TAGS_CACHE_KEY = "available-tags";
const _obsCache = createTTLCache({ ttlMs: SETTINGS_FAST_CACHE_TTL_MS, maxSize: 1 });
const _rtSyncCache = createTTLCache({ ttlMs: SETTINGS_FAST_CACHE_TTL_MS, maxSize: 1 });
const _tagsCache = createTTLCache({ ttlMs: () => _getTagsCacheTTL(), maxSize: 1 });

const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);

function _coerceBool(value: any, fallback = false) {
    if (typeof value === "boolean") return value;
    if (typeof value === "number") return value !== 0;
    if (typeof value === "string") {
        const s = value.trim().toLowerCase();
        if (TRUE_VALUES.has(s)) return true;
        if (FALSE_VALUES.has(s)) return false;
    }
    return Boolean(fallback);
}

function _getTagsCacheTTL() {
    try {
        const raw = localStorage?.getItem?.(SETTINGS_KEY) || "{}";
        const parsed = JSON.parse(raw);
        const ttl =
            parsed?.cache?.tagsTTLms ??
            parsed?.cache?.tagsTTL ??
            parsed?.cache?.tags_ttl_ms ??
            null;
        const n = Number(ttl);
        if (!Number.isFinite(n)) return DEFAULT_TAGS_CACHE_TTL_MS;
        return Math.max(1_000, Math.min(10 * 60_000, Math.floor(n)));
    } catch {
        return DEFAULT_TAGS_CACHE_TTL_MS;
    }
}

function invalidateObsCache() {
    _obsCache.clear();
}

function invalidateRatingTagsSyncCache() {
    _rtSyncCache.clear();
}

function invalidateTagsCache() {
    _tagsCache.clear();
}

function _normalizeTagCacheKey(raw: any) {
    const value = String(raw ?? "")
        .trim()
        .toLowerCase();
    return value || "";
}

function _dedupeTagList(tags: any) {
    const next = [];
    const seen = new Set();
    for (const raw of Array.isArray(tags) ? tags : []) {
        const value = String(raw ?? "").trim();
        if (!value) continue;
        const key = _normalizeTagCacheKey(value);
        if (!key || seen.has(key)) continue;
        seen.add(key);
        next.push(value);
    }
    return next;
}

// Best-effort cache invalidation when settings change (ComfyUI settings, dev tools, etc.).
try {
    const w = typeof window !== "undefined" ? window : null;
    if (w && !(w as any)[CLIENT_GLOBAL_KEY]) {
        (w as any)[CLIENT_GLOBAL_KEY] = { initialized: true };

        w.addEventListener?.("storage", (event) => {
            try {
                if (event?.key === SETTINGS_KEY) {
                    invalidateObsCache();
                    invalidateRatingTagsSyncCache();
                    invalidateTagsCache();
                    invalidateAuthTokenCache();
                }
            } catch (e) {
                console.debug?.(e);
            }
        });

        w.addEventListener?.("mjr-settings-changed", () => {
            invalidateObsCache();
            invalidateRatingTagsSyncCache();
            invalidateTagsCache();
            invalidateAuthTokenCache();
        });
    }
} catch (e) {
    console.debug?.(e);
}

const _readObsEnabled = () => {
    const cached = _obsCache.get(SETTINGS_CACHE_KEY);
    if (cached !== undefined) {
        return cached;
    }
    const now = Date.now();
    try {
        const raw = localStorage?.getItem?.(SETTINGS_KEY);
        if (!raw) {
            _obsCache.set(SETTINGS_CACHE_KEY, false, { at: now });
            return false;
        }
        const parsed = JSON.parse(raw);
        const value = !!parsed?.observability?.enabled;
        _obsCache.set(SETTINGS_CACHE_KEY, value, { at: now });
        return value;
    } catch {
        _obsCache.set(SETTINGS_CACHE_KEY, false, { at: now });
        return false;
    }
};

const _readRatingTagsSyncEnabled = () => {
    const cached = _rtSyncCache.get(SETTINGS_CACHE_KEY);
    if (cached !== undefined) {
        return cached;
    }
    const now = Date.now();
    try {
        const raw = localStorage?.getItem?.(SETTINGS_KEY);
        if (!raw) {
            _rtSyncCache.set(SETTINGS_CACHE_KEY, true, { at: now });
            return true;
        }
        const parsed = JSON.parse(raw);
        const configured = parsed?.ratingTagsSync?.enabled;
        const value =
            configured === undefined || configured === null ? true : _coerceBool(configured, true);
        _rtSyncCache.set(SETTINGS_CACHE_KEY, value, { at: now });
        return value;
    } catch {
        _rtSyncCache.set(SETTINGS_CACHE_KEY, true, { at: now });
        return true;
    }
};

const _apiFetchClient = createApiFetchClient({
    readObsEnabled: _readObsEnabled as () => boolean,
    readAuthToken: _readAuthToken as () => string,
    ensureWriteAuthToken: ensureWriteAuthToken as () => Promise<string>,
    normalizeWriteAuthFailure: _normalizeWriteAuthFailure,
});
const fetchAPI = _apiFetchClient.fetchAPI;

export async function get(url: string, options: Record<string, any> = {}) {
    return _apiFetchClient.get(url, options);
}

export async function post(url: string, body: unknown, options: Record<string, any> = {}) {
    return _apiFetchClient.post(url, body, options);
}

/**
 * Update asset rating (0-5 stars)
 */
export async function updateAssetRating(assetId: any, rating: any, options: Record<string, any> = {}) {
    const enabled = _readRatingTagsSyncEnabled();
    const asset = assetId && typeof assetId === "object" ? assetId : null;
    const resolvedId = asset ? asset.id : assetId;
    const normalizedId = normalizeAssetId(resolvedId);
    const payload: Record<string, any> = {
        rating: Math.max(0, Math.min(5, Number(rating) || 0)),
    };
    if (normalizedId) {
        payload.asset_id = normalizedId;
    } else if (asset) {
        payload.filepath = asset.filepath || asset.path || asset?.file_info?.filepath || "";
        payload.type = asset.type || "output";
        payload.root_id = pickRootId(asset);
    }
    return fetchAPI("/mjr/am/asset/rating", {
        ...options,
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            ...(enabled ? { "X-MJR-RTSYNC": "on" } : {}),
        },
        body: JSON.stringify(payload),
    });
}

/**
 * Update asset tags
 */
export async function updateAssetTags(assetId: any, tags: any, options: Record<string, any> = {}) {
    const enabled = _readRatingTagsSyncEnabled();
    const asset = assetId && typeof assetId === "object" ? assetId : null;
    const resolvedId = asset ? asset.id : assetId;
    const normalizedId = normalizeAssetId(resolvedId);
    const isWorkflowAsset = String(asset?.kind || asset?.type || "").trim().toLowerCase() === "workflow";
    const workflowFilepath = String(asset?.filepath || asset?.path || asset?.file_info?.filepath || "").trim();
    const payload: Record<string, any> = {
        tags: Array.isArray(tags) ? tags : [],
    };
    if (isWorkflowAsset && workflowFilepath) {
        payload.filepath = workflowFilepath;
    } else if (normalizedId) {
        payload.asset_id = normalizedId;
    } else if (asset) {
        payload.filepath = asset.filepath || asset.path || asset?.file_info?.filepath || "";
        payload.type = asset.type || "output";
        payload.root_id = pickRootId(asset);
    }
    const endpoint = isWorkflowAsset && workflowFilepath ? ENDPOINTS.WORKFLOWS_TAGS : "/mjr/am/asset/tags";
    const result = await fetchAPI(endpoint, {
        ...options,
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            ...(enabled ? { "X-MJR-RTSYNC": "on" } : {}),
        },
        body: JSON.stringify(payload),
    });
    if (result?.ok) {
        invalidateTagsCache();
    }
    return result;
}

/**
 * Get all available tags from the database
 */
export async function getAvailableTags() {
    const cachedTags = _tagsCache.get(TAGS_CACHE_KEY);
    if (Array.isArray(cachedTags)) {
        return { ok: true, data: cachedTags, error: null, code: "OK", meta: { cached: true } };
    }

    const result = await get("/mjr/am/tags");
    if (result?.ok && Array.isArray(result.data)) {
        const deduped = _dedupeTagList(result.data);
        _tagsCache.set(TAGS_CACHE_KEY, deduped);
        return { ...result, data: deduped };
    }
    return result;
}

/**
 * Get full asset metadata by ID
 */
export async function getAssetMetadata(assetId: any, options: Record<string, any> = {}) {
    const id = encodeURIComponent(normalizeAssetId(assetId));
    return get(`/mjr/am/asset/${id}`, {
        ...options,
        dedupeKey: options?.dedupeKey || `meta:${id}`,
    });
}

/**
 * Get compact viewer media info by asset ID (fps/frame count/dimensions/etc).
 */
/** @returns {Promise<ApiResult<ViewerInfo>>} */
export async function getViewerInfo(assetId: any, options: Record<string, any> = {}) {
    const id = normalizeAssetId(assetId);
    if (!id) return { ok: false, data: null, error: "Missing assetId", code: "INVALID_INPUT" };
    let url = `/mjr/am/viewer/info?asset_id=${encodeURIComponent(id)}`;
    if (options.refresh) url += "&refresh=1";
    const { refresh: _ignored, ...fetchOpts } = options;
    return get(url, fetchOpts);
}

/**
 * Batch fetch assets by ID (no per-asset tool invocations / self-heal).
 */
export async function getAssetsBatch(assetIds: any, options: Record<string, any> = {}) {
    const ids = Array.isArray(assetIds) ? assetIds : [];
    const cleaned = [];
    for (const id of ids) {
        const n = Number(id);
        if (!Number.isFinite(n)) continue;
        cleaned.push(Math.trunc(n));
        if (cleaned.length >= MAX_BATCH_ASSET_IDS) break;
    }
    if (!cleaned.length) return { ok: true, data: [], error: null, code: "OK" };
    return post("/mjr/am/assets/batch", { asset_ids: cleaned }, options);
}

export async function getWorkflowContent(filepath: any, options: Record<string, any> = {}) {
    const url = buildWorkflowContentURL(filepath);
    if (!url) {
        return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
    }
    return get(url, options);
}

export async function validateWorkflow(filepath: any, options: Record<string, any> = {}) {
    const url = buildWorkflowValidateURL(filepath);
    if (!url) {
        return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
    }
    return get(url, options);
}

export async function listWorkflowVersions(filepath: any, options: Record<string, any> = {}) {
    const url = buildWorkflowVersionsURL(filepath);
    if (!url) {
        return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
    }
    return get(url, options);
}

export async function diffWorkflow(filepath: any, version_filepath: any = "", options: Record<string, any> = {}) {
    const url = buildWorkflowDiffURL(filepath, version_filepath);
    if (!url) {
        return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
    }
    return get(url, options);
}

export async function saveWorkflow(
    {
        workflow = null,
        name = "",
        category = "",
        overwrite = false,
        filepath = "",
        task = "",
        model_family = "",
        provider = "",
        runs_on = "",
        notes = "",
    }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(
        ENDPOINTS.WORKFLOWS_SAVE,
        { workflow, name, category, overwrite, filepath, task, model_family, provider, runs_on, notes },
        options,
    );
}

export async function duplicateWorkflow(
    { filepath = "", name = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_DUPLICATE, { filepath, name }, options);
}

export async function moveWorkflow(
    { filepath = "", name = "", category = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_MOVE, { filepath, name, category }, options);
}

export async function deleteWorkflow(
    { filepath = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_DELETE, { filepath }, options);
}

export async function markWorkflowLoaded(
    { filepath = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_MARK_LOADED, { filepath }, options);
}

export async function setWorkflowFavorite(
    { filepath = "", favorite = false }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_FAVORITE, { filepath, favorite: !!favorite }, options);
}

export async function setWorkflowInfo(
    {
        filepath = "",
        task = "",
        model_family = "",
        provider = "",
        runs_on = "",
        notes = "",
    }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_INFO, { filepath, task, model_family, provider, runs_on, notes }, options);
}

export async function listWorkflowThumbnailCandidates(
    { filepath = "", limit = 12 }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    const safeLimit = Math.max(1, Math.min(50, Number(limit) || 12));
    const url = `${ENDPOINTS.WORKFLOWS_THUMBNAIL_CANDIDATES}?filepath=${encodeURIComponent(
        String(filepath || "").trim(),
    )}&limit=${encodeURIComponent(String(safeLimit))}`;
    return get(url, options);
}

export async function listWorkflowModelFamilies(options: Record<string, any> = {}) {
    return get(ENDPOINTS.WORKFLOWS_MODEL_FAMILIES, options);
}

export async function listWorkflows(
    { q = "*", limit = 100, offset = 0, sort = "mtime" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100));
    const safeOffset = Math.max(0, Number(offset) || 0);
    const url = `${ENDPOINTS.LIST}?scope=workflow&q=${encodeURIComponent(
        String(q || "*"),
    )}&limit=${encodeURIComponent(String(safeLimit))}&offset=${encodeURIComponent(
        String(safeOffset),
    )}&sort=${encodeURIComponent(String(sort || "mtime"))}`;
    return get(url, options);
}

export async function setWorkflowThumbnail(
    { filepath = "", source_filepath = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    return post(ENDPOINTS.WORKFLOWS_THUMBNAIL_SET, { filepath, source_filepath }, options);
}

export async function hydrateAssetRatingTags(assetId: any) {
    const id = normalizeAssetId(assetId);
    if (!id) return { ok: false, error: "Missing assetId" };
    return get(`/mjr/am/asset/${encodeURIComponent(id)}?hydrate=rating_tags`);
}

/**
 * Get metadata for a file reference (preferred over absolute paths).
 * Works on /view URLs where ComfyUI provides type/filename/subfolder.
 */
export async function getFileMetadataScoped(
    {
        type = "output",
        filename = "",
        subfolder = "",
        root_id = "",
        rootId = "",
        filepath = "",
    } = {},
    options = {},
) {
    const t =
        String(type || "output")
            .trim()
            .toLowerCase() || "output";
    const fn = String(filename || "").trim();
    const sub = String(subfolder || "").trim();
    const rid = String(root_id || rootId || "").trim();
    const fp = String(filepath || "").trim();
    if (!fn) return { ok: false, data: null, error: "Missing filename", code: "INVALID_INPUT" };
    let url = `/mjr/am/metadata?type=${encodeURIComponent(t)}&filename=${encodeURIComponent(fn)}`;
    if (fp) url += `&filepath=${encodeURIComponent(fp)}`;
    if (sub) url += `&subfolder=${encodeURIComponent(sub)}`;
    if (rid) url += `&root_id=${encodeURIComponent(rid)}`;
    return get(url, options);
}

export async function getFolderInfo(
    { filepath = "", root_id = "", subfolder = "" }: Record<string, any> = {},
    options: Record<string, any> = {},
) {
    try {
        if ((globalThis as any).__mjrFolderInfoSupported === false) {
            return {
                ok: false,
                data: null,
                error: "Folder info endpoint unavailable",
                code: "UNAVAILABLE",
            };
        }
        if ((globalThis as any).__mjrFolderInfoSupported == null) {
            const rr = await get("/mjr/am/routes");
            if (rr?.ok && Array.isArray(rr.data)) {
                const hasRoute = rr.data.some(
                    (r: any) => String(r?.path || "").trim() === "/mjr/am/folder-info",
                );
                (globalThis as any).__mjrFolderInfoSupported = !!hasRoute;
                if (!hasRoute) {
                    return {
                        ok: false,
                        data: null,
                        error: "Folder info endpoint unavailable",
                        code: "UNAVAILABLE",
                    };
                }
            } else {
                // Soft-fail: keep null so future calls can retry route discovery.
                (globalThis as any).__mjrFolderInfoSupported = null;
            }
        }
    } catch (e) {
        console.debug?.(e);
    }

    const fp = String(filepath || "").trim();
    const rid = String(root_id || "").trim();
    const sub = String(subfolder || "").trim();
    let url = ENDPOINTS.FOLDER_INFO;
    const params = [];
    if (fp) {
        params.push(`filepath=${encodeURIComponent(fp)}`);
        params.push("browser_mode=1");
    } else {
        if (rid) params.push(`root_id=${encodeURIComponent(rid)}`);
        if (sub) params.push(`subfolder=${encodeURIComponent(sub)}`);
    }
    if (params.length) url += `?${params.join("&")}`;
    const res = await get(url, options);
    try {
        if (!res?.ok && Number(res?.status || 0) === 404) {
            (globalThis as any).__mjrFolderInfoSupported = false;
        }
    } catch (e) {
        console.debug?.(e);
    }
    return res;
}

export * from "./clientOps.js";
