/**
 * Shared AI-tool runner. Used by Sharpen / Harmonize / Upscale / Style /
 * Bg-Remove / etc. — every tool that flattens the document, POSTs the
 * PNG to a server-side image endpoint, and drops the result back in
 * as a new layer.
 *
 * Handles all the orchestration around the request:
 *
 *  - Button busy state: swap label for "<verbing>…" + whirlpool
 *    spinner, lock width so the button doesn't visually jump.
 *  - Endpoint+model selection from the tool's own picker (or the
 *    global fallback) so the backend knows which model to invoke.
 *  - Response handling: decode the returned PNG, push it as a new
 *    layer, save state, composite, refresh the layer panel.
 *  - Error reporting: surface failures via toast. Detects "needs
 *    img2img server" and "package not installed" failure modes and
 *    surfaces an action-toast that opens Cookbook to fix.
 *
 * @param {{
 *   flatten:                    () => HTMLCanvasElement,
 *   saveState:                  (label?: string) => void,
 *   createLayer:                (name: string, w: number, h: number) => object,
 *   composite:                  () => void,
 *   renderLayerPanel:           () => void,
 *   deriveBusyLabel:            (layerName: string) => string,
 *   getSelectedAIEndpoint:      (type: string | null) => { endpoint?: string, model?: string },
 *   openCookbookForDependency:  (pkg: string) => void,
 *   openCookbookForImg2img:     () => void,
 *   spinnerModule:              object,
 *   uiModule:                   object | null,
 * }} deps
 *
 * @returns {(endpoint: string, extraPayload: object, layerName: string, btn: HTMLButtonElement, opts?: { busyLabel?: string }) => Promise<void>}
 */
import { state } from './state.js';

const KNOWN_DEPS = ['realesrgan', 'rembg'];

export function createApplyImageTool({
  flatten, saveState, createLayer, composite, renderLayerPanel,
  deriveBusyLabel, getSelectedAIEndpoint,
  openCookbookForDependency, openCookbookForImg2img,
  spinnerModule, uiModule,
}) {
  return async function applyImageTool(endpoint, extraPayload, layerName, btn, opts) {
    const origHTML = btn.innerHTML;
    const origWidth = btn.offsetWidth;  // lock width so the button doesn't jump
    btn.disabled = true;
    btn.classList.add('ge-btn-processing');
    btn.style.minWidth = origWidth + 'px';
    // Swap label for a "<verbing>…" text + whirlpool while the
    // request runs. Falls back to deriving a busy label from
    // layerName when the caller didn't supply one.
    const busyLabel = (opts && opts.busyLabel) || deriveBusyLabel(layerName);
    btn.innerHTML = '';
    let btnSpinner = null;
    try {
      btnSpinner = spinnerModule.create('', 'clean', 'whirlpool');
      const sp = btnSpinner.createElement();
      btn.appendChild(sp);
      const txt = document.createElement('span');
      txt.className = 'ge-btn-busy-label';
      txt.textContent = busyLabel;
      btn.appendChild(txt);
      btnSpinner.start();
    } catch { btn.textContent = busyLabel; }
    // Tool-specific model picker — pulled from the per-tool select
    // (harmonize/style) if available, otherwise the global
    // fallback. Derived from the endpoint URL.
    if (!extraPayload._endpoint) {
      const m = /\/api\/image\/([\w-]+)/.exec(endpoint || '');
      const type = m ? m[1].replace('upscale-ai', 'upscale').replace('remove-bg', 'rembg') : null;
      const sel = getSelectedAIEndpoint(type);
      if (sel.endpoint) extraPayload._endpoint = sel.endpoint;
      if (sel.model && !extraPayload._model) extraPayload._model = sel.model;
    }
    try {
      const flatCanvas = flatten();
      const imageB64 = flatCanvas.toDataURL('image/png').split(',')[1];
      const body = { image: imageB64, ...extraPayload };
      const res = await fetch(endpoint, {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        let err = res.statusText;
        try { const e = await res.json(); err = e.detail || e.error || err; } catch {}
        throw new Error(err);
      }
      const data = await res.json();
      if (data.error) throw new Error(data.error);
      if (!data.image) throw new Error('No image returned');
      const img = new Image();
      img.onload = () => {
        if (!state.editorOpen) return; // user closed mid-decode (v2 review HIGH-4)
        saveState();
        const layer = createLayer(layerName, state.imgWidth, state.imgHeight);
        layer.ctx.drawImage(img, 0, 0);
        state.layers.push(layer);
        state.activeLayerId = layer.id;
        composite();
        renderLayerPanel();
        if (uiModule) uiModule.showToast(layerName + ' complete', 4500);
      };
      img.onerror = () => { if (uiModule) uiModule.showToast('Failed to load result', 6000); };
      img.src = 'data:image/png;base64,' + data.image;
    } catch (e) {
      // Detect known failure modes and surface an action-toast.
      const msg = (e?.message || '').toLowerCase();
      const needsImg2Img = (
        msg.includes('img2img') ||
        msg.includes('diffusion server') ||
        msg.includes("doesn't expose")
      );
      let depMatch = null;
      for (const pkg of KNOWN_DEPS) {
        if (msg.includes(`${pkg} not installed`) || msg.includes(`no module named '${pkg}'`)) {
          depMatch = pkg; break;
        }
      }
      if (uiModule) {
        if (depMatch && uiModule.showToast.length >= 2) {
          uiModule.showToast(layerName + ' failed: ' + depMatch + ' is not installed on the server.', {
            duration: 9000,
            action: `Install ${depMatch}`,
            onAction: () => openCookbookForDependency(depMatch),
          });
        } else if (needsImg2Img && uiModule.showToast.length >= 2) {
          uiModule.showToast(layerName + ' failed: ' + e.message, {
            duration: 9000,
            action: 'Open Cookbook',
            onAction: () => openCookbookForImg2img(),
          });
        } else {
          uiModule.showToast(layerName + ' failed: ' + e.message, 6000);
        }
      }
    } finally {
      btn.disabled = false;
      btn.classList.remove('ge-btn-processing');
      try { btnSpinner?.destroy(); } catch {}
      btn.innerHTML = origHTML;
      btn.style.minWidth = '';
    }
  };
}
