// FloorCanvas MCP bridge — talks to floorcanvas-mcp over a localhost WebSocket.
//
// Loaded by FloorCanvas.html after the React app. Exposes window.FCBridge.attach()
// which the App component calls inside a useEffect to hand over its state callbacks.
// The bridge opens the socket on first attach and reconnects with exponential
// backoff if it drops. Single connection — no multi-tab support in v1.
//
// Protocol: JSON-RPC 2.0. The MCP server sends requests; we respond.
// Methods handled: get_plan, set_plan, add_shape, update_shape, delete_shape,
// switch_floor, add_floor.

(function () {
  if (typeof window === 'undefined') return;

  var PORT = (window.FC_BRIDGE_PORT != null) ? window.FC_BRIDGE_PORT : 8765;
  var TOKEN = window.FC_BRIDGE_TOKEN || '';
  var URL_ = 'ws://127.0.0.1:' + PORT + '/fc' + (TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '');

  var slot = null;        // latest callbacks from App
  var ws = null;
  var backoff = 1000;
  var BACKOFF_MAX = 30000;

  function log() {
    if (window.FC_BRIDGE_QUIET) return;
    var args = ['[fc-bridge]'].concat(Array.prototype.slice.call(arguments));
    // eslint-disable-next-line no-console
    console.log.apply(console, args);
  }

  function connect() {
    try {
      ws = new WebSocket(URL_);
    } catch (e) {
      log('connect failed:', e && e.message);
      scheduleReconnect();
      return;
    }
    ws.onopen = function () {
      log('connected to', URL_);
      backoff = 1000;
    };
    ws.onclose = function (ev) {
      log('disconnected', ev && ev.code);
      ws = null;
      scheduleReconnect();
    };
    ws.onerror = function () {
      // onclose will fire too; nothing to do here.
    };
    ws.onmessage = onMessage;
  }

  function scheduleReconnect() {
    var delay = backoff;
    backoff = Math.min(backoff * 2, BACKOFF_MAX);
    setTimeout(connect, delay);
  }

  function send(obj) {
    if (!ws || ws.readyState !== 1 /* OPEN */) return;
    ws.send(JSON.stringify(obj));
  }

  function onMessage(ev) {
    var msg;
    try {
      msg = JSON.parse(ev.data);
    } catch (_) {
      return;
    }
    if (!msg || msg.jsonrpc !== '2.0' || msg.id == null) return;
    Promise.resolve().then(function () { return handle(msg.method, msg.params || {}); }).then(
      function (result) { send({ jsonrpc: '2.0', id: msg.id, result: result }); },
      function (err) {
        send({
          jsonrpc: '2.0',
          id: msg.id,
          error: { code: -32000, message: (err && err.message) || String(err) },
        });
      },
    );
  }

  function ensureSlot() {
    if (!slot) throw new Error('FloorCanvas bridge not attached yet — refresh the page after the app mounts.');
    return slot;
  }

  function uidShape() {
    return Math.random().toString(36).slice(2, 9);
  }

  function uidFloor() {
    return 'fl-' + Math.random().toString(36).slice(2, 7);
  }

  function shapeBounds(sh) {
    if (sh.type === 'wall' || sh.type === 'dimension') {
      return { minX: Math.min(sh.a.x, sh.b.x), minY: Math.min(sh.a.y, sh.b.y),
               maxX: Math.max(sh.a.x, sh.b.x), maxY: Math.max(sh.a.y, sh.b.y) };
    }
    var w = sh.w != null ? sh.w : (sh.width != null ? sh.width : 2);
    var h = sh.h != null ? sh.h : 2;
    return { minX: sh.x, minY: sh.y, maxX: sh.x + w, maxY: sh.y + h };
  }

  function unionBounds(shapes) {
    if (!shapes.length) return null;
    var b = shapeBounds(shapes[0]);
    for (var i = 1; i < shapes.length; i++) {
      var sb = shapeBounds(shapes[i]);
      if (sb.minX < b.minX) b.minX = sb.minX;
      if (sb.minY < b.minY) b.minY = sb.minY;
      if (sb.maxX > b.maxX) b.maxX = sb.maxX;
      if (sb.maxY > b.maxY) b.maxY = sb.maxY;
    }
    return b;
  }

  // Render the current SVG stage to a PNG data URL (or SVG string).
  function renderImage(format, bounds, multiplier) {
    var stage = document.querySelector('.stage svg');
    if (!stage) throw new Error('no .stage svg element on page');
    var clone = stage.cloneNode(true);
    clone.querySelectorAll('.ghost').forEach(function (g) { g.remove(); });

    var vb;
    if (bounds && Array.isArray(bounds) && bounds.length === 4) {
      vb = bounds;
    } else {
      var bbox = stage.getBBox();
      var pad = 6;
      vb = [bbox.x - pad, bbox.y - pad, bbox.width + pad * 2, bbox.height + pad * 2];
    }
    clone.setAttribute('viewBox', vb[0] + ' ' + vb[1] + ' ' + vb[2] + ' ' + vb[3]);
    clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    var cs = getComputedStyle(document.documentElement);
    var vars = ['--paper','--paper-2','--paper-3','--paper-4','--ink','--ink-4','--ink-5','--ink-6','--yellow','--red','--blue'];
    clone.setAttribute('style', vars.map(function (v) { return v + ': ' + cs.getPropertyValue(v).trim() + ';'; }).join(' '));
    var svgStr = new XMLSerializer().serializeToString(clone);

    if (format === 'svg') return Promise.resolve({ format: 'svg', svg: svgStr, viewBox: vb });

    var m = multiplier || 2;
    var widthPx = Math.round(vb[2] * 96 / 4 * m);
    var heightPx = Math.round(widthPx * vb[3] / vb[2]);
    var blob = new Blob([svgStr], { type: 'image/svg+xml' });
    var url = URL.createObjectURL(blob);
    return new Promise(function (resolve, reject) {
      var img = new Image();
      img.onload = function () {
        var canvas = document.createElement('canvas');
        canvas.width = widthPx; canvas.height = heightPx;
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = cs.getPropertyValue('--paper').trim() || '#f1ebe1';
        ctx.fillRect(0, 0, widthPx, heightPx);
        ctx.drawImage(img, 0, 0, widthPx, heightPx);
        URL.revokeObjectURL(url);
        var dataUrl = canvas.toDataURL('image/png');
        var base64 = dataUrl.split(',')[1] || '';
        resolve({ format: 'png', base64: base64, width: widthPx, height: heightPx, viewBox: vb });
      };
      img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('svg → png rasterise failed')); };
      img.src = url;
    });
  }

  function handle(method, params) {
    var s = ensureSlot();
    switch (method) {
      case 'get_plan': {
        var st = s.getState();
        return { name: st.name, floors: st.floors, activeId: st.activeId };
      }
      case 'set_plan': {
        var plan = params.plan;
        if (!plan || !plan.floors || !plan.floors.length) throw new Error('set_plan: plan.floors must be a non-empty array');
        var activeId = plan.activeId && plan.floors.some(function (f) { return f.id === plan.activeId; })
          ? plan.activeId
          : plan.floors[0].id;
        s.setHistory({ past: [], present: { floors: plan.floors, activeId: activeId }, future: [] });
        if (plan.name) s.setProjectName(plan.name);
        return { ok: true };
      }
      case 'add_shape': {
        var shape = params.shape;
        if (!shape || !shape.type) throw new Error('add_shape: shape.type is required');
        var newId = uidShape();
        var shapeWithId = Object.assign({}, shape, { id: newId });
        var targetFloorId = params.floor_id || null;
        s.commitState(function (state) {
          var fid = targetFloorId || state.activeId;
          if (!state.floors.some(function (f) { return f.id === fid; })) {
            throw new Error('add_shape: floor_id ' + JSON.stringify(fid) + ' not found');
          }
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return f.id === fid ? Object.assign({}, f, { shapes: f.shapes.concat([shapeWithId]) }) : f;
            }),
          });
        });
        return { id: newId };
      }
      case 'update_shape': {
        var uid = params.id;
        var patch = params.patch || {};
        if (!uid) throw new Error('update_shape: id is required');
        var stU = s.getState();
        var exists = stU.floors.some(function (f) { return f.shapes.some(function (sh) { return sh.id === uid; }); });
        if (!exists) throw new Error('update_shape: shape id ' + JSON.stringify(uid) + ' not found on any floor');
        s.commitState(function (state) {
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return Object.assign({}, f, {
                shapes: f.shapes.map(function (sh) {
                  if (sh.id !== uid) return sh;
                  return Object.assign({}, sh, patch);
                }),
              });
            }),
          });
        });
        return { ok: true };
      }
      case 'delete_shape': {
        var did = params.id;
        if (!did) throw new Error('delete_shape: id is required');
        var stD = s.getState();
        var hasD = stD.floors.some(function (f) { return f.shapes.some(function (sh) { return sh.id === did; }); });
        if (!hasD) throw new Error('delete_shape: shape id ' + JSON.stringify(did) + ' not found on any floor');
        s.commitState(function (state) {
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return Object.assign({}, f, {
                shapes: f.shapes.filter(function (sh) { return sh.id !== did; }),
              });
            }),
          });
        });
        return { ok: true };
      }
      case 'switch_floor': {
        var fid = params.floor_id;
        if (!fid) throw new Error('switch_floor: floor_id is required');
        var stateNow = s.getState();
        if (!stateNow.floors.some(function (f) { return f.id === fid; })) {
          throw new Error('switch_floor: floor_id ' + JSON.stringify(fid) + ' not found');
        }
        s.setActiveFloor(fid);
        return { ok: true };
      }
      case 'add_floor': {
        var name = params.name;
        if (!name) throw new Error('add_floor: name is required');
        var levelArg = (params.level == null) ? null : params.level;
        var newFloorId = uidFloor();
        s.commitState(function (state) {
          var lvl = (levelArg == null)
            ? (state.floors.reduce(function (mx, f) { return Math.max(mx, f.level); }, -Infinity) + 1)
            : levelArg;
          if (!isFinite(lvl)) lvl = 0;
          return Object.assign({}, state, {
            floors: state.floors.concat([{ id: newFloorId, name: name, level: lvl, shapes: [] }]),
          });
        });
        return { floor_id: newFloorId };
      }
      case 'screenshot': {
        var fmt = (params && params.format) === 'svg' ? 'svg' : 'png';
        var mult = (params && params.multiplier) || 2;
        var b = (params && params.bounds) || null;
        return renderImage(fmt, b, mult);
      }
      case 'pan_zoom': {
        if (!s.setView) throw new Error('view callbacks not attached');
        var cur = s.getView();
        var next = {
          tx: params && params.tx != null ? params.tx : cur.tx,
          ty: params && params.ty != null ? params.ty : cur.ty,
          scale: params && params.scale != null ? params.scale : cur.scale,
        };
        if (!isFinite(next.scale) || next.scale < 4 || next.scale > 200) {
          throw new Error('pan_zoom: scale must be between 4 and 200');
        }
        s.setView(next);
        return { ok: true, view: next };
      }
      case 'fit_to_content': {
        if (!s.setView) throw new Error('view callbacks not attached');
        var state = s.getState();
        var floor = state.floors.find(function (f) { return f.id === state.activeId; }) || state.floors[0];
        var shapes = floor.shapes;
        if (params && params.floor_id) {
          var f2 = state.floors.find(function (f) { return f.id === params.floor_id; });
          if (f2) shapes = f2.shapes;
        }
        if (params && Array.isArray(params.shape_ids) && params.shape_ids.length) {
          var idset = {};
          params.shape_ids.forEach(function (id) { idset[id] = true; });
          shapes = state.floors.reduce(function (acc, f) {
            return acc.concat(f.shapes.filter(function (sh) { return idset[sh.id]; }));
          }, []);
        }
        if (!shapes.length) throw new Error('fit_to_content: nothing to fit');
        var bb = unionBounds(shapes);
        var pad = (params && params.padding != null) ? params.padding : 4;
        var vp = document.querySelector('.stage');
        var rect = vp ? vp.getBoundingClientRect() : { width: 1200, height: 800 };
        var w = (bb.maxX - bb.minX) + pad * 2;
        var h = (bb.maxY - bb.minY) + pad * 2;
        var scale = Math.min(rect.width / w, rect.height / h);
        scale = Math.max(4, Math.min(120, scale));
        var tx = -((bb.minX - pad) * scale) + (rect.width - w * scale) / 2;
        var ty = -((bb.minY - pad) * scale) + (rect.height - h * scale) / 2;
        var v = { tx: tx, ty: ty, scale: scale };
        s.setView(v);
        return { ok: true, view: v, bounds: bb };
      }
      case 'select': {
        if (!s.setSelectedId) throw new Error('selection callbacks not attached');
        var id = (params && params.id) || null;
        if (id) {
          var st2 = s.getState();
          var hit = st2.floors.some(function (f) { return f.shapes.some(function (sh) { return sh.id === id; }); });
          if (!hit) throw new Error('select: shape id ' + JSON.stringify(id) + ' not found');
        }
        s.setSelectedId(id);
        return { ok: true, selected: id };
      }
      case 'undo': {
        if (!s.undo) throw new Error('undo callback not attached');
        s.undo();
        return { ok: true };
      }
      case 'redo': {
        if (!s.redo) throw new Error('redo callback not attached');
        s.redo();
        return { ok: true };
      }
      case 'rename_floor': {
        if (!params || !params.floor_id || !params.name) throw new Error('rename_floor: floor_id and name required');
        var rid = params.floor_id, rname = params.name;
        var stR = s.getState();
        if (!stR.floors.some(function (f) { return f.id === rid; })) {
          throw new Error('rename_floor: floor_id ' + JSON.stringify(rid) + ' not found');
        }
        s.commitState(function (state) {
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return f.id === rid ? Object.assign({}, f, { name: rname }) : f;
            }),
          });
        });
        return { ok: true };
      }
      case 'delete_floor': {
        if (!params || !params.floor_id) throw new Error('delete_floor: floor_id required');
        var did = params.floor_id;
        var stateNow = s.getState();
        if (stateNow.floors.length <= 1) throw new Error('delete_floor: at least one floor must remain');
        if (!stateNow.floors.some(function (f) { return f.id === did; })) {
          throw new Error('delete_floor: floor_id ' + JSON.stringify(did) + ' not found');
        }
        s.commitState(function (state) {
          var remaining = state.floors.filter(function (f) { return f.id !== did; });
          var nextActive = state.activeId === did ? remaining[0].id : state.activeId;
          return Object.assign({}, state, { floors: remaining, activeId: nextActive });
        });
        return { ok: true };
      }
      case 'set_style': {
        if (!s.setTweak) throw new Error('tweak callbacks not attached');
        var keys = ['vizStyle', 'paperStyle', 'accent', 'gridSize', 'showGrid', 'showDimensions', 'lineWeight', 'ghostMode', 'showGhost', 'presentation'];
        var applied = {};
        keys.forEach(function (k) {
          if (params && Object.prototype.hasOwnProperty.call(params, k)) {
            s.setTweak(k, params[k]);
            applied[k] = params[k];
          }
        });
        return { ok: true, applied: applied };
      }
      case 'export_png': {
        // Trigger the FC built-in PNG export (downloads in the browser).
        if (typeof window.exportPNG !== 'function') throw new Error('window.exportPNG not available');
        var filename = (params && params.filename) || 'floorcanvas';
        var multiplier2 = (params && params.multiplier) || 2;
        return Promise.resolve(window.exportPNG({ multiplier: multiplier2, filename: filename })).then(function () {
          return { ok: true, filename: filename + '.png' };
        });
      }
      case 'move_shapes': {
        if (!params || !Array.isArray(params.ids) || !params.ids.length) throw new Error('move_shapes: ids array required');
        var dx = (params.dx != null) ? params.dx : 0;
        var dy = (params.dy != null) ? params.dy : 0;
        var moved = 0;
        var idset2 = {};
        params.ids.forEach(function (id) { idset2[id] = true; });
        s.commitState(function (state) {
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return Object.assign({}, f, {
                shapes: f.shapes.map(function (sh) {
                  if (!idset2[sh.id]) return sh;
                  moved++;
                  if (sh.a && sh.b) {
                    return Object.assign({}, sh, {
                      a: { x: sh.a.x + dx, y: sh.a.y + dy },
                      b: { x: sh.b.x + dx, y: sh.b.y + dy },
                    });
                  }
                  return Object.assign({}, sh, {
                    x: (sh.x || 0) + dx,
                    y: (sh.y || 0) + dy,
                  });
                }),
              });
            }),
          });
        });
        return { ok: true, moved: moved };
      }
      case 'get_room_areas': {
        if (!window.FCRooms || typeof window.FCRooms.detectRooms !== 'function') {
          throw new Error('window.FCRooms.detectRooms not available');
        }
        var state3 = s.getState();
        var fid = (params && params.floor_id) || state3.activeId;
        var fl = state3.floors.find(function (f) { return f.id === fid; });
        if (!fl) throw new Error('get_room_areas: floor not found: ' + fid);
        var walls = fl.shapes.filter(function (sh) { return sh.type === 'wall'; });
        var data = window.FCRooms.detectRooms(walls);
        var rooms = (data && data.rooms) ? data.rooms : [];
        return { floor_id: fid, rooms: rooms.map(function (r) {
          return { centroid: r.centroid, area_sqft: r.area, ring: r.ring };
        }) };
      }
      case 'get_furniture_kinds': {
        var map = window.FURNITURE_MAP || {};
        var out = [];
        Object.keys(map).forEach(function (k) {
          var f = map[k];
          out.push({ kind: f.kind, label: f.label, category: f.category, default_w: f.w, default_h: f.h });
        });
        out.sort(function (a, b) { return (a.category || '').localeCompare(b.category || '') || a.kind.localeCompare(b.kind); });
        return { kinds: out };
      }
      case 'stamp_shapes': {
        // Insert a batch of shapes with an offset on the active or given floor.
        if (!params || !Array.isArray(params.shapes) || !params.shapes.length) {
          throw new Error('stamp_shapes: shapes array required');
        }
        var ox = (params.offset && params.offset.x) || 0;
        var oy = (params.offset && params.offset.y) || 0;
        var floor2 = params.floor_id || null;
        var newIds = [];
        s.commitState(function (state) {
          var fid = floor2 || state.activeId;
          if (!state.floors.some(function (f) { return f.id === fid; })) {
            throw new Error('stamp_shapes: floor_id not found: ' + fid);
          }
          var translated = params.shapes.map(function (sh) {
            var copy = JSON.parse(JSON.stringify(sh));
            copy.id = uidShape();
            newIds.push(copy.id);
            if (copy.a && copy.b) {
              copy.a = { x: copy.a.x + ox, y: copy.a.y + oy };
              copy.b = { x: copy.b.x + ox, y: copy.b.y + oy };
            } else {
              if (typeof copy.x === 'number') copy.x += ox;
              if (typeof copy.y === 'number') copy.y += oy;
            }
            return copy;
          });
          return Object.assign({}, state, {
            floors: state.floors.map(function (f) {
              return f.id === fid ? Object.assign({}, f, { shapes: f.shapes.concat(translated) }) : f;
            }),
          });
        });
        return { ok: true, ids: newIds };
      }
      default:
        throw new Error('Unknown method: ' + method);
    }
  }

  window.FCBridge = {
    attach: function (callbacks) {
      slot = callbacks;
      if (!ws) connect();
    },
    _status: function () {
      return {
        attached: !!slot,
        socket: ws && ws.readyState,
        url: URL_,
      };
    },
  };
})();
