/* ============================================================
   HNK AI Tools — RunningHub Adapter (openapi/v2 "Standard Model API")
   Spec §17 (Adapter orchestration) · §11–§13 (states, progress, cancel) · §24

   Orchestrates a compiled request (from either mode's compiler) into a real
   generation: verify → upload → build payload → submit → poll → download.
   Drives the Free Generate state machine's stages for the progress UI, honours
   cancel (AbortSignal) and timeout, issues N sequential requests when the model
   has no batch variants, and normalizes every failure for the user (§24).

   Request-body shapes are ported 1:1 from the companion web app's proven
   openapi/v2 integration (same Enterprise-Shared key, same endpoints,
   confirmed against RunningHub's own API docs — see runninghub-config.js).

   Everything is injected — transport / sleep / now / config — so the whole flow
   is unit-testable with a fake transport and no real time or network.
   ============================================================ */
/* HNK-IIFE-WRAP: isolate module scope so top-level vars never collide
   under UXP shared-global <script> loading (browser-style). */
(function () {
"use strict";

var _CJS = (typeof module !== "undefined" && module.exports);
function dep(p, g) { return _CJS ? require(p) : globalThis.HNK[g]; }

var rhConfig = dep("./runninghub-config", "runninghubConfig");
var uploadSvc = dep("./runninghub-upload-service", "runninghubUploadService");
var taskSvc = dep("./runninghub-task-service", "runninghubTaskService");
var normalizer = dep("./runninghub-error-normalizer", "errorNormalizer");
var machine = dep("../free-generate/generate-state-machine", "generateStateMachine");
var registry = dep("../models/model-registry", "modelRegistry");

/* Ratio/size enums + per-family mappings — ported verbatim from the web
   app's confirmed rhV2Submit (see docs comment in runninghub-config.js). */
var RH_RATIO_ENUM = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "5:4", "4:5", "21:9", "1:4", "4:1", "1:8", "8:1"];
/* Z-Image Turbo's aspectRatio enum is a strict subset of RH_RATIO_ENUM —
   notably no "4:5" — so it needs its own narrower check. */
var RH_ZIMAGE_RATIO_ENUM = ["3:2", "2:3", "16:9", "9:16", "4:3", "3:4", "1:1"];
var RH_QWEN_SIZE_MAP = {
  "1:1":  { std: "1024*1024", hd: "1536*1536" },
  "2:3":  { std: "768*1152",  hd: "1024*1536" },
  "3:2":  { std: "1152*768",  hd: "1536*1024" },
  "3:4":  { std: "960*1280",  hd: "1080*1440" },
  "4:3":  { std: "1280*960",  hd: "1440*1080" },
  "9:16": { std: "720*1280",  hd: "1080*1920" },
  "16:9": { std: "1280*720",  hd: "1920*1080" }
};
var RH_WAN_RATIO_WH = {
  "1:1": [1, 1], "3:4": [3, 4], "4:3": [4, 3], "4:5": [4, 5], "5:4": [5, 4],
  "9:16": [9, 16], "16:9": [16, 9], "2:3": [2, 3], "3:2": [3, 2]
};

function rhRatio(ratio) { return RH_RATIO_ENUM.indexOf(ratio) !== -1 ? ratio : ""; }
/* Auto -> the cheapest tier; the Standard API requires a lowercase 1k/2k/4k
   on every call, never an empty/omitted value. */
function rhResolution(size) {
  var s = String(size || "").toLowerCase();
  return (s === "1k" || s === "2k" || s === "4k") ? s : "1k";
}
/* Some endpoints document a NARROWER resolution enum than 1k/2k/4k (config
   flag `resolutions`, e.g. Seedream v4.5 accepts only 2k|4k). Anything outside
   that enum maps to its smallest documented tier — never ship an out-of-enum
   value like "1k" to a 2k|4k-only endpoint. */
function rhResolutionFor(mc, size) {
  var r = rhResolution(size);
  if (mc && mc.resolutions && mc.resolutions.length && mc.resolutions.indexOf(r) === -1) r = mc.resolutions[0];
  return r;
}
function qwenSize(ratio, size) {
  var m = RH_QWEN_SIZE_MAP[ratio];
  if (!m) return "";
  var s = String(size || "").toLowerCase();
  return (s === "2k" || s === "4k") ? m.hd : m.std;
}
function wanWH(ratio, size) {
  var pr = RH_WAN_RATIO_WH[ratio];
  if (!pr) return null;
  var s = String(size || "").toLowerCase();
  var base = s === "4k" ? 2048 : s === "2k" ? 1536 : 1024;
  var rw = pr[0], rh = pr[1], w, h;
  if (rw >= rh) { w = base; h = Math.round(base * rh / rw); } else { h = base; w = Math.round(base * rw / rh); }
  return { w: Math.max(512, Math.min(4096, w)), h: Math.max(512, Math.min(4096, h)) };
}

/* Build the openapi/v2 JSON body for a compiled request against one model's
   config (apiPath + kind/sizeParam/whParam/imageParam/promptMax/quality/
   resolutions). */
function buildRequestBody(mc, request, uploadedUrls) {
  mc = mc || {};
  var body = { prompt: request.prompt || request.compiledPrompt || "" };
  if (mc.promptMax && body.prompt.length > mc.promptMax) body.prompt = body.prompt.slice(0, mc.promptMax);

  var ratio = (request.output && request.output.ratio) || "";
  var size = (request.output && request.output.size) || "";

  // Pure text-to-image endpoints (e.g. flux-2-dev's confirmed RunningHub
  // route) take no image field at all and no resolution — prompt +
  // aspectRatio + a constant outputFormat only, ported 1:1 from the web
  // app's rhV2SubmitT2I. Return early so the imageUrls/resolution logic
  // below (which every image-edit endpoint needs) never runs for these.
  if (mc.kind === "t2i") {
    var rT2i = rhRatio(ratio);
    if (rT2i) body.aspectRatio = rT2i;
    body.outputFormat = "png";
    if (mc.quality) body.quality = mc.quality;
    return body;
  }

  uploadedUrls = uploadedUrls || [];
  var imageParam = mc.imageParam || "imageUrls";
  if (imageParam === "image") body.image = uploadedUrls[0] || "";
  else if (imageParam === "imageUrl") body.imageUrl = uploadedUrls[0] || "";
  else body.imageUrls = uploadedUrls;

  if (mc.kind === "seedream") {
    body.resolution = rhResolutionFor(mc, size);
    body.sequentialImageGeneration = "disabled";
    body.maxImages = 1;
  } else if (mc.kind === "imagine") {
    var res3 = String(size || "").toLowerCase();
    body.resolution = (res3 === "2k" || res3 === "4k") ? "2k" : "1k";
    body.numImages = "1";
    var r3 = rhRatio(ratio); if (r3) body.aspectRatio = r3;
  } else if (mc.kind === "zimage") {
    body.aspectRatio = RH_ZIMAGE_RATIO_ENUM.indexOf(ratio) !== -1 ? ratio : "1:1";
    body.outputFormat = "png";
  } else if (mc.sizeParam) {
    var sz = qwenSize(ratio, size); if (sz) body.size = sz;
  } else if (mc.whParam) {
    var wh = wanWH(ratio, size); if (wh) { body.width = wh.w; body.height = wh.h; }
  } else {
    body.resolution = rhResolutionFor(mc, size);
    var r2 = rhRatio(ratio); if (r2) body.aspectRatio = r2;
  }
  if (mc.quality) body.quality = mc.quality;
  return body;
}

/* Upscale-kind models (Upscale Pro / Upscale Transparent) have no prompt at
   all — a single image plus a size-derived field. Kept separate from
   buildRequestBody since the shapes don't overlap. */
function buildUpscaleBody(mc, request, uploadedUrls) {
  var url = (uploadedUrls || [])[0] || "";
  var size = (request.output && request.output.size) || "";
  var s = String(size || "").toLowerCase();
  if (mc.kind === "upscale-transparent") {
    var body = { imageUrl: url };
    var wh = s === "4k" ? { w: 3840, h: 2160 } : s === "2k" ? { w: 2560, h: 1440 } : s === "1k" ? { w: 1920, h: 1080 } : null;
    if (wh) { body.outputWidth = wh.w; body.outputHeight = wh.h; }
    return body;
  }
  // "upscale" (Upscale Pro): a relative scale enum instead of absolute pixels.
  var scale = s === "4k" ? "6x" : s === "2k" ? "4x" : "2x";
  return { imageUrl: url, scale: scale };
}

/* Lightweight key check (spec §21 Save & Verify). Treats 401/403 as invalid and
   any other response as "key accepted". Returns { ok, error? }. */
async function verifyKey(deps, apiKey) {
  if (!apiKey) return { ok: false, error: { code: "invalid-key" } };
  deps = deps || {};
  deps.cfg = deps.cfg || rhConfig.resolve(deps.configOverride);
  try {
    var resp = await deps.transport({
      method: "POST",
      url: (deps.cfg.baseUrl || "").replace(/\/+$/, "") + deps.cfg.paths.query,
      headers: { "Content-Type": "application/json", "Authorization": "Bearer " + apiKey },
      body: JSON.stringify({ taskId: "__hnk_verify__" })
    });
    if (resp && (resp.status === 401 || resp.status === 403)) return { ok: false, error: { code: "invalid-key" } };
    return { ok: true };
  } catch (e) {
    return { ok: false, error: normalizer.normalize(e) };
  }
}

function _stage(onStage, m, extra) {
  if (typeof onStage === "function") onStage(m.stage, Object.assign({ label: machine.label(m.stage) }, extra || {}));
}

/* One task end-to-end → array of result refs. */
async function _runOnce(deps, request, onStage, m) {
  var apiKey = deps.apiKey;
  var cfg = deps.cfg;
  var mc = rhConfig.modelConfig(cfg, request.model) || {};

  // Prepare + upload images
  machine.advance(m); _stage(onStage, m); // PREPARING_IMAGES
  var refs = (request.images || []).map(function (im) { return im.ref; }).filter(Boolean);
  machine.advance(m); _stage(onStage, m, { total: refs.length }); // UPLOADING
  var uploaded = await uploadSvc.uploadAll(deps, apiKey, refs, function (i, n) {
    // Honour cancel between uploads (spec §13: cancel during upload).
    if (deps.signal && deps.signal.aborted) throw Object.assign(new Error("cancelled"), { code: "cancelled" });
    _stage(onStage, m, { current: i + 1, total: n });
  });

  // Build + submit
  machine.advance(m); _stage(onStage, m); // BUILDING_REQUEST
  var isUpscale = mc.kind === "upscale" || mc.kind === "upscale-transparent";
  var body = isUpscale ? buildUpscaleBody(mc, request, uploaded) : buildRequestBody(mc, request, uploaded);
  machine.advance(m); _stage(onStage, m); // SUBMITTING
  var taskId = await taskSvc.submit(deps, apiKey, mc.apiPath, body);
  deps._taskId = taskId; // for cancel cleanup

  // Poll (the SUCCESS response already carries `.results` — no separate
  // outputs call under openapi/v2).
  machine.advance(m); _stage(onStage, m); // PROCESSING
  var final = await taskSvc.pollUntilDone(deps, apiKey, taskId, function (elapsed) {
    _stage(onStage, m, { elapsedMs: elapsed });
  });

  // Download
  machine.advance(m); _stage(onStage, m); // DOWNLOADING_RESULT
  var resultList = (final && final.results) || [];
  var results = [];
  for (var i = 0; i < resultList.length; i++) {
    if (!resultList[i] || !resultList[i].url) continue;
    var ref = await taskSvc.download(deps, resultList[i].url);
    if (ref) results.push({ ref: ref, url: resultList[i].url });
  }
  return results;
}

/* Public entry. deps = { transport, cfg?, apiKey, sleep?, now?, signal? }.
   Returns { ok, results:[{ref}], model, error? }. */
async function generate(deps, request, opts) {
  opts = opts || {};
  var onStage = opts.onStage;
  deps = Object.assign({}, deps);
  deps.cfg = deps.cfg || rhConfig.resolve(deps.configOverride);
  deps.signal = opts.signal || deps.signal;

  var m = machine.create();
  _stage(onStage, m); // IDLE
  machine.advance(m, deps.now ? deps.now() : 0); _stage(onStage, m); // VALIDATING

  // Guard: model has no real apiPath yet — fail clearly instead of guessing.
  if (rhConfig.isPlaceholder(deps.cfg, request.model)) {
    machine.fail(m, "not-configured");
    return { ok: false, model: request.model, machine: m,
      error: { code: "not-configured", title: "This model is not connected to RunningHub yet.",
        message: "This model has no RunningHub endpoint path yet.",
        bullets: ["Add its endpoint path in Settings → RunningHub"] } };
  }

  var model = registry.getModel(request.model);
  var count = Math.max(1, request.requestCount || 1);
  var all = [];
  try {
    for (var r = 0; r < count; r++) {
      if (deps.signal && deps.signal.aborted) throw Object.assign(new Error("cancelled"), { code: "cancelled" });
      // fresh sub-machine per request so stages read correctly for the UI
      var sub = machine.create();
      var res = await _runOnce(deps, request, onStage, sub);
      all = all.concat(res);
    }
    machine.advance(m); // (VALIDATING -> ... ) keep top machine READY
    m.stage = "READY"; _stage(onStage, m);
    return { ok: true, results: all, model: request.model, machine: m };
  } catch (e) {
    if (e && e.code === "cancelled") {
      machine.cancel(m); _stage(onStage, m);
      if (deps._taskId) taskSvc.cancelTask(deps, deps.apiKey, deps._taskId);
      return { ok: false, cancelled: true, model: request.model, machine: m, error: { code: "cancelled" } };
    }
    if (e && e.code === "timeout") { machine.timeout(m); _stage(onStage, m); }
    else { machine.fail(m, e && e.message); _stage(onStage, m); }
    var n = normalizer.normalize(e, { modelName: model ? model.displayName : request.model,
      size: request.output && request.output.requestedSize,
      available: model ? model.capabilities.supportedSizes : undefined });
    return { ok: false, model: request.model, machine: m, error: n };
  }
}

function makeVerifier(deps) {
  var d = Object.assign({}, deps);
  d.cfg = d.cfg || rhConfig.resolve(d.configOverride);
  return function (apiKey) { return verifyKey(Object.assign({}, d, { apiKey: apiKey }), apiKey); };
}

var API = { generate: generate, verifyKey: verifyKey, makeVerifier: makeVerifier, buildRequestBody: buildRequestBody, buildUpscaleBody: buildUpscaleBody };

if (typeof module !== "undefined" && module.exports) module.exports = API;
else { globalThis.HNK = globalThis.HNK || {}; globalThis.HNK.runninghubAdapter = API; }
})();
