/* AliothPress — WebMCP admin tools
 * ----------------------------------------------------------------------------
 * Agent tools for the admin panel. Loaded only when the owner enabled admin
 * agent access. Every tool runs in the signed-in user's session, so existing
 * @login_required / @admin_required checks and CSRF still apply — an agent can
 * never do more than the human could. Reads are free; writes are confirmed.
 *
 * Publishing policy: agents may create and edit DRAFTS but can never set
 * status to 'published'. Enforced twice — here (tools clamp/refuse) and
 * server-side (_agent_safe_status demotes 'published' for _format=json
 * callers). Publishing stays a human-only action in the admin UI.
 *
 * Partial updates (update_post / update_page / attach_image) mirror the human
 * flow: read the full record via the agent detail read, merge changes over it,
 * submit the complete form back to the same /edit route (which overwrites all
 * fields, exactly like the human's prefilled form).
 *
 * Writes post to the same routes the human UI uses, with _format=json, so they
 * get a clean { success, id, edit_url } contract back instead of an HTML
 * redirect — one source of truth for content creation, no parallel copy.
 *
 * Add a tool = append one object to TOOLS.
 * ----------------------------------------------------------------------------
 */
(function () {
  'use strict';
  if (!window.AliothMCP) return;
  var M = window.AliothMCP;

  function aiGenerate(payload) {
    return M.postJson('/admin/ai/generate', payload).then(function (r) { return r.json(); });
  }
  // Status-aware JSON reader. A bare r.json() on an nginx/Flask ERROR page
  // (413, 419-style CSRF, 500…) threw "SyntaxError: Unexpected token '<'",
  // which is what agents saw instead of the actual cause. Now non-2xx
  // responses become a readable error with the HTTP status and, when the
  // body is JSON, the server's own error text.
  function jsonOf(r) {
    if (r.ok) return r.json();
    var status = r.status;
    return r.text().then(function (body) {
      var msg = null;
      try {
        var j = JSON.parse(body);
        msg = (j && (j.error || j.message)) || null;
      } catch (e) { /* body was HTML or plain text */ }
      if (!msg) {
        if (status === 413) msg = 'payload_too_large: request body exceeded the server limit; retry with smaller content or fewer items per call';
        else if (status === 400 && /csrf/i.test(body)) msg = 'csrf_failed: the admin page session went stale; reload the page and retry';
        else if (status === 401 || status === 403) msg = 'not_authorized';
        else if (status === 404) msg = 'not_found';
        else if (status >= 500) msg = 'server_error';
        else msg = 'request_failed';
      }
      throw new Error(msg + ' (HTTP ' + status + ')');
    });
  }

  // --- Detail read (full editable record) -----------------------------------
  function getDetail(type, id) {
    return M.getJson('/admin/api/agent/list?type=' + encodeURIComponent(type) +
                     '&id=' + encodeURIComponent(id)).then(jsonOf);
  }

  // --- base64 / data-URL → Blob (for upload_image) --------------------------
  function b64ToBlob(data, filename) {
    var mime = '';
    var m = /^data:([^;]+);base64,/.exec(data);
    if (m) { mime = m[1]; data = data.slice(m[0].length); }
    var bin = atob(String(data).replace(/\s+/g, ''));
    var bytes = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
    if (!mime) {
      var ext = (String(filename).split('.').pop() || '').toLowerCase();
      mime = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
               webp: 'image/webp', svg: 'image/svg+xml', avif: 'image/avif' }[ext] || 'application/octet-stream';
    }
    return new Blob([bytes], { type: mime });
  }

  // --- Merge-save through the HUMAN edit route ------------------------------
  // /admin/posts|pages/<id>/edit overwrites every field from the submitted
  // form — exactly like the human's prefilled form. So the agent flow mirrors
  // the human flow: read the full record (agent detail read), merge the
  // changed fields over it, and submit the complete form back. The XHR header
  // makes the route answer with the same inline-save JSON the human editor
  // gets. Publishing is blocked here (and drafts stay drafts unless the
  // record was already published, in which case its status is preserved).
  var EDIT_FIELDS = ['title', 'content', 'excerpt', 'language', 'meta_title',
    'meta_description', 'meta_keywords', 'schema_type', 'schema_author',
    'schema_publisher', 'og_title', 'og_description', 'og_image',
    'twitter_title', 'twitter_description', 'twitter_image', 'featured_image',
    'featured_image_alt', 'featured_image_caption', 'tags', 'category',
    'faq_json', 'slug'];
  var PAGE_EDIT_FIELDS = ['template'];

  function mergedSave(type, id, changes, confirmMsg) {
    changes = changes || {};
    if (changes.status === 'published') {
      return Promise.resolve(M.fail(M.t('publish_blocked')));
    }
    return getDetail(type, id).then(function (res) {
      if (!res || !res.success || !res.item) return M.fail(res || { error: 'not_found' });
      var cur = res.item;
      if (type === 'page' && cur.use_builder && changes.content !== undefined) {
        return M.fail(M.t('builder_content_locked'));
      }
      var names = EDIT_FIELDS.concat(type === 'page' ? PAGE_EDIT_FIELDS : []);
      var fields = {};
      names.forEach(function (k) {
        fields[k] = (changes[k] !== undefined && changes[k] !== null) ? changes[k] : (cur[k] || '');
      });
      // Never escalate: 'draft' may be set explicitly, anything else keeps
      // the record's current status.
      fields.status = (changes.status === 'draft') ? 'draft' : cur.status;
      // Checkboxes: present = on, absent = off — mirror current values so a
      // partial agent update never silently unticks them.
      if (cur.show_author) fields.show_author = 'on';
      if (cur.show_date) fields.show_date = 'on';
      if (type === 'page') {
        if (cur.use_builder) { fields.use_builder = 'on'; fields.content = cur.content; }
        if (cur.is_posts_page) fields.is_posts_page = 'on';
      }
      var url = '/admin/' + (type === 'page' ? 'pages' : 'posts') + '/' + id + '/edit';
      return M.confirm(confirmMsg(cur, fields)).then(function (go) {
        if (!go) return M.fail(M.t('declined'));
        return M.postForm(url, fields, { 'X-Requested-With': 'XMLHttpRequest' })
          .then(jsonOf)
          .then(function (r) {
            return M.ok({ success: !!(r && r.success), id: cur.id,
                          status: fields.status, edit_url: url });
          })
          .catch(M.fail);
      });
    }).catch(M.fail);
  }

  // --- Batch create (one approval dialog for many items) --------------------
  // Items are posted SEQUENTIALLY to the same create/translate routes the
  // single tools use — same server-side draft clamp, same JSON contract.
  // Unselected items are reported back as skipped so the agent knows exactly
  // what happened to each one.
  function batchCreate(kind, items, selected) {
    var plural = kind === 'page' ? 'pages' : 'posts';
    var results = [];
    var chain = Promise.resolve();
    items.forEach(function (it, idx) {
      chain = chain.then(function () {
        if (selected.indexOf(idx) === -1) {
          results.push({ index: idx, title: it.title, skipped: true });
          return;
        }
        var url = it.translation_of
          ? '/admin/' + plural + '/' + encodeURIComponent(it.translation_of) + '/translate'
          : '/admin/' + plural + '/new';
        // Builder pages in batch: an item with a blocks array is created as a
        // Page Builder page, then its layout is saved — both covered by the one
        // batch approval the user already gave (mirrors create_builder_page).
        var isBuilder = kind === 'page' && Array.isArray(it.blocks);
        var payload = {
          _format: 'json', title: it.title,
          content: isBuilder ? '' : it.content,
          excerpt: it.excerpt || '', language: it.language || 'en', status: 'draft',
          meta_title: it.meta_title || '', meta_description: it.meta_description || '',
          meta_keywords: it.meta_keywords || '',
          og_title: it.og_title || '', og_description: it.og_description || '',
          twitter_title: it.twitter_title || '', twitter_description: it.twitter_description || '',
          faq_json: it.faq_json || '',
          schema_type: it.schema_type || '',
          tags: it.tags || '', category: it.category || '', slug: it.slug || '',
          featured_image: it.featured_image || '',
          featured_image_alt: it.featured_image_alt || '',
          featured_image_caption: it.featured_image_caption || '',
          og_image: it.og_image || '', twitter_image: it.twitter_image || ''
        };
        // Send only when provided — translate routes inherit these from the
        // source item ONLY if the form key is absent entirely.
        if (it.schema_author !== undefined && it.schema_author !== null) payload.schema_author = it.schema_author;
        if (it.schema_publisher !== undefined && it.schema_publisher !== null) payload.schema_publisher = it.schema_publisher;
        if (isBuilder && !it.translation_of) payload.use_builder = 'on';
        return M.postForm(url, payload).then(jsonOf).then(function (r) {
          var row = { index: idx, title: it.title, success: !!(r && r.success),
                      id: r && r.id, edit_url: r && r.edit_url, error: r && r.error };
          if (row.success && isBuilder && row.id) {
            // Second half of the builder item: save its layout. The PUT route
            // sets use_builder and renders the flat HTML content server-side.
            return M.putJson('/api/pages/' + row.id + '/blocks',
                             { blocks: it.blocks, convert: true })
              .then(jsonOf).then(function (saved) {
                row.blocks_saved = !!(saved && saved.success);
                if (!row.blocks_saved) { row.success = false; row.error = (saved && saved.error) || 'blocks_save_failed'; }
                row.builder_url = '/admin/pages/' + row.id + '/builder';
                results.push(row);
              }).catch(function (e) {
                row.blocks_saved = false; row.success = false; row.error = String(e);
                results.push(row);
              });
          }
          results.push(row);
        }).catch(function (e) {
          results.push({ index: idx, title: it.title, success: false, error: String(e) });
        });
      });
    });
    return chain.then(function () { return results; });
  }

  var BATCH_ITEM_SCHEMA = {
    type: 'object',
    properties: {
      title: { type: 'string' }, content: { type: 'string', description: 'HTML body' },
      excerpt: { type: 'string' }, language: { type: 'string', description: 'Defaults to en' },
      slug: { type: 'string' }, tags: { type: 'string', description: 'Comma-separated' },
      category: { type: 'string' },
      meta_title: { type: 'string' }, meta_description: { type: 'string' },
      meta_keywords: { type: 'string' },
      og_title: { type: 'string' }, og_description: { type: 'string' },
      twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
      faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
      schema_type: { type: 'string', description: 'Schema.org type. Posts: BlogPosting|Article|NewsArticle|TechArticle|HowTo (default BlogPosting). Pages: WebPage|AboutPage|ContactPage|FAQPage|CollectionPage (default WebPage)' },
      schema_author: { type: 'string' }, schema_publisher: { type: 'string' },
      blocks: { type: 'array', description: 'PAGES ONLY: array of page-builder block objects (see describe_builder_blocks). When set, the page is created as a Page Builder page and these blocks become its layout — all inside the one batch approval.', items: { type: 'object' } },
      featured_image: { type: 'string' }, featured_image_alt: { type: 'string' },
      featured_image_caption: { type: 'string' },
      og_image: { type: 'string' }, twitter_image: { type: 'string' },
      translation_of: { type: 'integer', description: 'Source id when this item is a translation of an existing item' }
    },
    required: ['title', 'content']
  };

  function batchExecute(kind, confirmKey) {
    return function (a) {
      var items = a.items || [];
      if (!items.length) return Promise.resolve(M.fail({ error: 'items_required' }));
      var labels = items.map(function (it) {
        return it.title + (it.language ? ' [' + it.language + ']' : '');
      });
      return M.confirmBatch(M.t(confirmKey, { count: items.length }), labels)
        .then(function (selected) {
          if (!selected || !selected.length) return M.fail(M.t('declined'));
          return batchCreate(kind, items, selected).then(function (res) {
            var created = res.filter(function (r) { return r.success; }).length;
            return M.ok({ success: true, requested: items.length,
                          approved: selected.length, created: created, results: res });
          });
        }).catch(M.fail);
    };
  }

  // Static reference for describe_builder_blocks. Mirrors BLOCK_RENDERERS in
  // block_renderer.py and the AI builder_blocks prompt — every type the human
  // Page Builder offers, none omitted. Keep in sync when adding block types.
  var BLOCK_REFERENCE = {
    usage: 'Each block is { "id": "<short random string>", "type": "<type>", "data": {...}, ' +
           '"settings": {...} }. Compose an array of blocks and save it with ' +
           'create_builder_page or set_page_blocks. For any image url, first get a real ' +
           'URL via list_media or upload_image (empty url renders a placeholder).',
    settings: {
      alignment: 'left|center|right. Optional: blocks saved without it get the same ' +
                 'defaults the admin UI gives a human — content-flow blocks (heading, ' +
                 'text, quote, table, accordion, tabs, code, html, form) default to left ' +
                 '(right on RTL-language pages), visual blocks (image, button, media, ' +
                 'spacers, containers) to center. Set it explicitly to override. Blocks ' +
                 'already stored on a page are never re-defaulted on later saves.',
      content_alignment: 'left|center|right',
      margin_top: 'none|sm|md|lg|xl', margin_bottom: 'none|sm|md|lg|xl',
      css_class: 'optional CSS class', anchor_id: 'optional #anchor id'
    },
    types: {
      heading: { text: 'Heading text', level: '1-6 (number)' },
      text: { html: '<p>Rich HTML paragraph(s)</p>' },
      image: { url: 'image URL', alt: 'required alt text', caption: '', size: 'full' },
      button: { text: 'Label', url: '#', style: 'primary|outline|ghost', size: 'sm|md|lg', target: '_self|_blank' },
      divider: { style: 'solid|dashed|dotted|gradient' },
      spacer: { height: 'pixels as string, e.g. "48"' },
      columns: { layout: '1|1/2+1/2|1/3+1/3+1/3|2/3+1/3|1/3+2/3|1/4+1/4+1/4+1/4',
                 columns: [{ blocks: '[nested blocks — any type except columns/section/slideshow]' }] },
      section: { background_type: 'color|gradient|image|none', background_value: '#1a1a2e',
                 background_image: 'URL', background_overlay: 'rgba(0,0,0,0.5)',
                 background_type_dark: 'color|gradient|image|none', background_value_dark: '#0f172a',
                 background_overlay_dark: '', padding: 'sm|md|lg|xl', text_color: 'auto|light|dark',
                 min_height: 'auto', full_width: false, content_width: 'default|narrow|wide|full',
                 blocks: '[nested blocks — any type except section]' },
      slideshow: { mode: 'hero|image', slides: [{ image: 'URL', alt: '', blocks: '[hero-mode nested blocks: heading, text, button…]' }],
                   animation: 'slide|fade', autoplay: true, interval: 5, show_arrows: true,
                   show_dots: true, aspect_ratio: '16/9|4/3|21/9|1/1|auto', overlay: 'rgba(0,0,0,0.4)',
                   padding: 'lg', text_color: 'light|dark|auto', full_width: false,
                   content_width: 'default|narrow|wide|full' },
      video: { source: 'youtube|vimeo|self', url: '', embed_id: 'YouTube id or Vimeo numeric id', title: '', aspect_ratio: '16/9' },
      gallery: { images: [{ url: 'URL', alt: '', caption: '' }], layout: 'grid', columns: 3, gap: 'md', lightbox: true },
      video_gallery: { videos: [{ url: '', embed_id: '', source: 'youtube|vimeo', title: '' }], columns: 2, gap: 'md' },
      audio: { url: 'audio file URL', title: '' },
      ticker: { images: [{ url: 'logo URL', alt: 'Company name' }], speed: 'slow|medium|fast',
                pause_on_hover: true, grayscale: true, height: 'sm|md|lg|xl', direction: 'rtl|ltr' },
      quote: { text: 'Quote text', author: 'Author' },
      accordion: { items: [{ title: 'Question', content: '<p>Answer HTML</p>' }], allow_multiple: false },
      tabs: { tabs: [{ title: 'Tab 1', content: '<p>Content HTML</p>' }] },
      table: { headers: ['Col1', 'Col2'], rows: [['a', 'b']], striped: true, responsive: true,
               table_width: 'full|auto', column_alignments: ['left', ''], column_widths: ['30', ''] },
      code: { code: 'source code', language: 'javascript' },
      html: { code: '<!-- raw HTML -->' },
      form: { form_id: 'form id (integer, from list_content type=form)', show_title: true, show_description: true }
    }
  };

  var TOOLS = [
    // ── Generation (no save) ─────────────────────────────────────────────
    {
      name: 'ai_generate_content',
      description: 'Generate a complete post or page (title, HTML content, excerpt, ' +
                   'SEO meta, FAQ) from a description. Returns the draft as data — it is ' +
                   'NOT saved. Use create_post / create_page to save it.',
      inputSchema: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'What the content should be about' },
          content_type: { type: 'string', enum: ['post', 'page'], description: 'Defaults to page' },
          language: { type: 'string', description: 'Language to write in, e.g. "English", "Deutsch"' }
        },
        required: ['prompt']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return aiGenerate({ task: 'generate', prompt: a.prompt,
          content_type: a.content_type || 'page', language: a.language || 'English' })
          .then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'ai_translate_text',
      description: 'Translate HTML content from one language to another, preserving markup.',
      inputSchema: {
        type: 'object',
        properties: {
          content: { type: 'string', description: 'HTML or text to translate' },
          source_lang: { type: 'string', description: 'Source language code, e.g. "en"' },
          target_lang: { type: 'string', description: 'Target language code, e.g. "de"' }
        },
        required: ['content', 'target_lang']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return aiGenerate({ task: 'translate', content: a.content,
          source_lang: a.source_lang || 'en', target_lang: a.target_lang })
          .then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'ai_generate_page_blocks',
      description: 'Generate a visual page-builder layout as JSON blocks from a description. ' +
                   'Returns the blocks array to paste into the page builder.',
      inputSchema: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'Describe the page layout you want' },
          language: { type: 'string', description: 'Language for the text content' }
        },
        required: ['prompt']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return aiGenerate({ task: 'builder_blocks', prompt: a.prompt, language: a.language || 'English' })
          .then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'ai_optimize_seo',
      description: 'Generate SEO fields (meta title, description, keywords, Open Graph) for a ' +
                   'given title and content.',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' }, content: { type: 'string' },
          prompt: { type: 'string', description: 'Optional extra instructions' },
          language: { type: 'string' }
        },
        required: ['title']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return aiGenerate({ task: 'seo', title: a.title, content: a.content || '',
          prompt: a.prompt || '', language: a.language || 'English' })
          .then(M.ok).catch(M.fail);
      }
    },

    // ── Read / search (safe, no confirmation) ────────────────────────────
    {
      name: 'list_content',
      description: 'List posts, pages, or forms, with optional filters by status, language, ' +
                   'or title text. Read-only. Returns at most `limit` items (default 50, ' +
                   'max 200) plus honest pagination info: `total` = how many items match ' +
                   'in all, `returned` = how many are in this response, `truncated` = true ' +
                   'when total > returned. If truncated, raise `limit` or narrow the ' +
                   'filters — items beyond the limit exist, they just were not returned. ' +
                   '(Form submissions are not exposed — they hold visitor personal data.)',
      inputSchema: {
        type: 'object',
        properties: {
          type: { type: 'string', enum: ['post', 'page', 'form'], description: 'Defaults to post' },
          status: { type: 'string', description: 'e.g. draft, published, scheduled' },
          language: { type: 'string', description: 'Language code, e.g. en, de' },
          query: { type: 'string', description: 'Filter by words in the title' },
          limit: { type: 'integer', description: 'Max items to return (default 50, cap 200)' }
        }
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        var p = new URLSearchParams();
        p.set('type', a.type || 'post');
        if (a.status) p.set('status', a.status);
        if (a.language) p.set('language', a.language);
        if (a.query) p.set('q', a.query);
        if (a.limit) p.set('limit', a.limit);
        return M.getJson('/admin/api/agent/list?' + p.toString()).then(jsonOf).then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'search_content',
      description: 'Search posts, pages, or forms by title. Returns matches with ids and slugs.',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string' },
          content_type: { type: 'string', enum: ['post', 'page', 'form'] }
        },
        required: ['query']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        var url = '/admin/api/search-titles?q=' + encodeURIComponent(a.query) +
                  '&content_type=' + encodeURIComponent(a.content_type || 'post');
        return M.getJson(url).then(jsonOf).then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'check_slug',
      description: 'Check whether a URL slug is available for a post or page.',
      inputSchema: {
        type: 'object',
        properties: { slug: { type: 'string' }, content_type: { type: 'string', enum: ['post', 'page'] } },
        required: ['slug']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return M.postJson('/admin/api/check-slug', { slug: a.slug, content_type: a.content_type || 'post' })
          .then(jsonOf).then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'get_content',
      description: 'Read the FULL editable record of one post or page by id (from ' +
                   'list_content / search_content): title, content HTML, excerpt, status, ' +
                   'SEO fields, Open Graph and Twitter images, featured image, tags, ' +
                   'category, FAQ, slug — plus template/use_builder for pages. Read-only. ' +
                   'Use before update_post / update_page to see current values.',
      inputSchema: {
        type: 'object',
        properties: {
          content_type: { type: 'string', enum: ['post', 'page'] },
          id: { type: 'integer' }
        },
        required: ['content_type', 'id']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return getDetail(a.content_type, a.id).then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'list_media',
      description: 'List images in the media library with their public URLs, alt text and ' +
                   'dimensions. Use these URLs as featured_image / og_image / twitter_image ' +
                   'or inside page-builder image, gallery, slideshow and ticker blocks. ' +
                   'Optional query filters by filename, alt text, or title. Read-only.',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Filter by words in filename, alt text, or title' },
          limit: { type: 'integer', description: 'Max results, default 30' }
        }
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return M.getJson('/admin/media/api/list').then(jsonOf).then(function (items) {
          var q = (a.query || '').toLowerCase();
          var limit = a.limit && a.limit > 0 ? a.limit : 30;
          var filtered = (items || []).filter(function (img) {
            if (!q) return true;
            return ((img.filename || '') + ' ' + (img.alt_text || '') + ' ' + (img.title || ''))
              .toLowerCase().indexOf(q) !== -1;
          });
          return M.ok({ success: true, total: filtered.length,
                        items: filtered.slice(0, limit) });
        }).catch(M.fail);
      }
    },
    {
      name: 'get_page_blocks',
      description: 'Read the current page-builder blocks JSON of a page by id. Read-only. ' +
                   'Use before set_page_blocks to modify an existing layout instead of ' +
                   'overwriting it blindly. The response includes converts_on_save: true ' +
                   'means the page still holds regular-editor content (the returned blocks ' +
                   'are a non-persisted preview conversion) and set_page_blocks will refuse ' +
                   'without an explicit convert=true.',
      inputSchema: {
        type: 'object',
        properties: { page_id: { type: 'integer' } },
        required: ['page_id']
      },
      annotations: { readOnlyHint: true },
      execute: function (a) {
        return M.getJson('/api/pages/' + encodeURIComponent(a.page_id) + '/blocks')
          .then(jsonOf).then(M.ok).catch(M.fail);
      }
    },
    {
      name: 'describe_builder_blocks',
      description: 'Reference of ALL page-builder block types and their data shapes ' +
                   '(heading, text, image, button, divider, spacer, columns, section, ' +
                   'slideshow, video, gallery, video_gallery, audio, ticker, quote, ' +
                   'accordion, tabs, table, code, html, form) plus the shared settings ' +
                   'object. Read this before composing blocks for create_builder_page or ' +
                   'set_page_blocks. Read-only, static.',
      inputSchema: { type: 'object', properties: {} },
      annotations: { readOnlyHint: true },
      execute: function () {
        return Promise.resolve(M.ok(BLOCK_REFERENCE));
      }
    },

    // ── Write (confirmed, clean JSON contract) ───────────────────────────
    {
      name: 'create_post',
      description: 'Create a blog post. ALWAYS saved as a draft — agents cannot publish; ' +
                   'the site owner publishes from the admin UI. The user confirms before ' +
                   'it is created. Supports the full metadata set in one call: slug ' +
                   '(check_slug first), FAQ, OG/Twitter titles+descriptions+images, and ' +
                   'Schema.org type/author/publisher (image URLs from list_media or ' +
                   'upload_image). To create a LANGUAGE VERSION ' +
                   'of an existing post, pass translation_of (the source post id from ' +
                   'list_content) — the new post is then linked to the source via its ' +
                   'translation group, so hreflang and the language switcher work. ' +
                   'Creating a translation without translation_of leaves it unlinked.',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' }, content: { type: 'string', description: 'HTML body' },
          excerpt: { type: 'string' }, language: { type: 'string', description: 'Defaults to en' },
          slug: { type: 'string', description: 'URL slug. Sanitized server-side and made unique (a taken slug gets a numeric suffix — verify with check_slug first if the exact value matters). Omit to auto-generate from the title.' },
          meta_title: { type: 'string' }, meta_description: { type: 'string' },
          meta_keywords: { type: 'string' },
          tags: { type: 'string', description: 'Comma-separated' },
          category: { type: 'string' },
          og_title: { type: 'string' }, og_description: { type: 'string' },
          twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
          faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
          schema_type: { type: 'string', enum: ['BlogPosting', 'Article', 'NewsArticle', 'TechArticle', 'HowTo'], description: 'Schema.org type. Defaults to BlogPosting' },
          schema_author: { type: 'string' }, schema_publisher: { type: 'string' },
          featured_image: { type: 'string', description: 'Image URL for the featured image' },
          featured_image_alt: { type: 'string' },
          featured_image_caption: { type: 'string' },
          og_image: { type: 'string', description: 'Image URL for Open Graph sharing' },
          twitter_image: { type: 'string', description: 'Image URL for Twitter/X cards' },
          translation_of: { type: 'integer', description: 'Source post id when this post is a translation of an existing post. The server links it into the source\u2019s translation group.' }
        },
        required: ['title', 'content']
      },
      annotations: { destructiveHint: false },
      execute: function (a) {
        // Translations go through the dedicated translate route: the server
        // inherits the translation group from the source post (the agent never
        // handles the group id) and rejects duplicate languages with a JSON 409.
        var url = a.translation_of
          ? '/admin/posts/' + encodeURIComponent(a.translation_of) + '/translate'
          : '/admin/posts/new';
        return M.confirm(M.t('confirm_create_post', { title: a.title, status: 'draft' }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            var payload = {
              _format: 'json', title: a.title, content: a.content, excerpt: a.excerpt || '',
              language: a.language || 'en', status: 'draft', slug: a.slug || '',
              meta_title: a.meta_title || '', meta_description: a.meta_description || '',
              meta_keywords: a.meta_keywords || '',
              tags: a.tags || '', category: a.category || '',
              og_title: a.og_title || '', og_description: a.og_description || '',
              twitter_title: a.twitter_title || '', twitter_description: a.twitter_description || '',
              faq_json: a.faq_json || '',
              schema_type: a.schema_type || '',
              featured_image: a.featured_image || '',
              featured_image_alt: a.featured_image_alt || '',
              featured_image_caption: a.featured_image_caption || '',
              og_image: a.og_image || '', twitter_image: a.twitter_image || ''
            };
            // Send only when provided: the translate route inherits these from
            // the source item ONLY if the form key is absent entirely.
            if (a.schema_author !== undefined && a.schema_author !== null) payload.schema_author = a.schema_author;
            if (a.schema_publisher !== undefined && a.schema_publisher !== null) payload.schema_publisher = a.schema_publisher;
            return M.postForm(url, payload).then(jsonOf).then(M.ok).catch(M.fail);
          });
      }
    },
    {
      name: 'create_page',
      description: 'Create a page. ALWAYS saved as a draft — agents cannot publish; the ' +
                   'site owner publishes from the admin UI. The user confirms before it is ' +
                   'created. Supports the full metadata set in one call: slug (check_slug ' +
                   'first), FAQ, OG/Twitter titles+descriptions+images, and Schema.org ' +
                   'type/author/publisher (image URLs from list_media or upload_image). ' +
                   'To create a LANGUAGE VERSION of an ' +
                   'existing page, pass translation_of (the source page id from ' +
                   'list_content) — the new page is then linked to the source via its ' +
                   'translation group, so hreflang and the language switcher work.',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' }, content: { type: 'string', description: 'HTML body' },
          excerpt: { type: 'string' }, language: { type: 'string', description: 'Defaults to en' },
          slug: { type: 'string', description: 'URL slug. Sanitized server-side and made unique (a taken slug gets a numeric suffix — verify with check_slug first if the exact value matters). Omit to auto-generate from the title.' },
          meta_title: { type: 'string' }, meta_description: { type: 'string' },
          meta_keywords: { type: 'string' },
          tags: { type: 'string', description: 'Comma-separated' },
          category: { type: 'string' },
          og_title: { type: 'string' }, og_description: { type: 'string' },
          twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
          faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
          schema_type: { type: 'string', enum: ['WebPage', 'AboutPage', 'ContactPage', 'FAQPage', 'CollectionPage'], description: 'Schema.org type. Defaults to WebPage' },
          schema_author: { type: 'string' }, schema_publisher: { type: 'string' },
          featured_image: { type: 'string', description: 'Image URL for the featured image' },
          featured_image_alt: { type: 'string' },
          featured_image_caption: { type: 'string' },
          og_image: { type: 'string', description: 'Image URL for Open Graph sharing' },
          twitter_image: { type: 'string', description: 'Image URL for Twitter/X cards' },
          translation_of: { type: 'integer', description: 'Source page id when this page is a translation of an existing page. The server links it into the source\u2019s translation group.' }
        },
        required: ['title', 'content']
      },
      annotations: { destructiveHint: false },
      execute: function (a) {
        var url = a.translation_of
          ? '/admin/pages/' + encodeURIComponent(a.translation_of) + '/translate'
          : '/admin/pages/new';
        return M.confirm(M.t('confirm_create_page', { title: a.title, status: 'draft' }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            var payload = {
              _format: 'json', title: a.title, content: a.content, excerpt: a.excerpt || '',
              language: a.language || 'en', status: 'draft', slug: a.slug || '',
              meta_title: a.meta_title || '', meta_description: a.meta_description || '',
              meta_keywords: a.meta_keywords || '',
              tags: a.tags || '', category: a.category || '',
              og_title: a.og_title || '', og_description: a.og_description || '',
              twitter_title: a.twitter_title || '', twitter_description: a.twitter_description || '',
              faq_json: a.faq_json || '',
              schema_type: a.schema_type || '',
              featured_image: a.featured_image || '',
              featured_image_alt: a.featured_image_alt || '',
              featured_image_caption: a.featured_image_caption || '',
              og_image: a.og_image || '', twitter_image: a.twitter_image || ''
            };
            // Send only when provided: the translate route inherits these from
            // the source item ONLY if the form key is absent entirely.
            if (a.schema_author !== undefined && a.schema_author !== null) payload.schema_author = a.schema_author;
            if (a.schema_publisher !== undefined && a.schema_publisher !== null) payload.schema_publisher = a.schema_publisher;
            return M.postForm(url, payload).then(jsonOf).then(M.ok).catch(M.fail);
          });
      }
    },
    {
      name: 'create_posts_batch',
      description: 'Create MANY blog posts in one go — ideal when migrating content from ' +
                   'another CMS. Shows ONE approval dialog listing every title with ' +
                   'checkboxes: the user can select all or approve a subset; unselected ' +
                   'items are skipped and reported back. All items are saved as DRAFTS — ' +
                   'agents cannot publish. Each item supports the same fields as ' +
                   'create_post. Items are created sequentially in the given order. ' +
                   'SIZING: the whole batch travels inside this one tool call, and agent ' +
                   'runtimes cap tool-call size — with FULL-LENGTH content (e.g. complete ' +
                   'translations) send 1–3 items per call and repeat; the batch dialog is ' +
                   'only worth it for many SHORT items. CERTAINTY: nothing is created ' +
                   'before the approval dialog, so a call that fails to go through ' +
                   'created nothing; after approval, `results` reports every item ' +
                   'individually (created / failed with reason / skipped) — no need to ' +
                   're-list content to verify.',
      inputSchema: {
        type: 'object',
        properties: {
          items: { type: 'array', description: 'Posts to create', items: BATCH_ITEM_SCHEMA, minItems: 1 }
        },
        required: ['items']
      },
      annotations: { destructiveHint: false },
      execute: batchExecute('post', 'batch_title_posts')
    },
    {
      name: 'create_pages_batch',
      description: 'Create MANY pages in one go — ideal when migrating a site from ' +
                   'another CMS. Shows ONE approval dialog listing every title with ' +
                   'checkboxes: the user can select all or approve a subset; unselected ' +
                   'items are skipped and reported back. All items are saved as DRAFTS — ' +
                   'agents cannot publish. Each item supports the same fields as ' +
                   'create_page, PLUS an optional blocks array: items with blocks are ' +
                   'created as Page Builder pages with that layout — pages and their ' +
                   'blocks are all covered by the single batch approval (see ' +
                   'describe_builder_blocks for block shapes). SIZING: the whole batch ' +
                   'travels inside this one tool call, and agent runtimes cap tool-call ' +
                   'size — with FULL-LENGTH content or rich block layouts send 1–3 items ' +
                   'per call and repeat. CERTAINTY: nothing is created before the ' +
                   'approval dialog; after approval, `results` reports every item ' +
                   'individually (created / failed with reason / skipped).',
      inputSchema: {
        type: 'object',
        properties: {
          items: { type: 'array', description: 'Pages to create', items: BATCH_ITEM_SCHEMA, minItems: 1 }
        },
        required: ['items']
      },
      annotations: { destructiveHint: false },
      execute: batchExecute('page', 'batch_title_pages')
    },
    {
      name: 'create_builder_page',
      description: 'Create a visual page-builder page from blocks (e.g. from ' +
                   'ai_generate_page_blocks; see describe_builder_blocks for every block ' +
                   'type and its data shape). ALWAYS saved as a draft — agents cannot ' +
                   'publish. Pass blocks as the array of block objects. Optionally set ' +
                   'slug and SEO/Schema.org metadata in the same call; anything omitted ' +
                   'can be added later with update_page. To create a LANGUAGE VERSION of ' +
                   'an existing page, pass translation_of (the source page id) — the new ' +
                   'page joins the source\u2019s translation group and the given blocks ' +
                   'become its (translated) layout. Confirmed before creating.',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' },
          blocks: { type: 'array', description: 'Array of page-builder block objects', items: { type: 'object' } },
          language: { type: 'string', description: 'Defaults to en' },
          translation_of: { type: 'integer', description: 'Source page id when this builder page is a translation of an existing page' },
          slug: { type: 'string', description: 'URL slug. Sanitized server-side and made unique (a taken slug gets a numeric suffix — verify with check_slug first if the exact value matters). Omit to auto-generate from the title.' },
          meta_title: { type: 'string' }, meta_description: { type: 'string' },
          meta_keywords: { type: 'string' },
          og_title: { type: 'string' }, og_description: { type: 'string' },
          twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
          faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
          schema_type: { type: 'string', enum: ['WebPage', 'AboutPage', 'ContactPage', 'FAQPage', 'CollectionPage'], description: 'Schema.org type. Defaults to WebPage' },
          schema_author: { type: 'string' }, schema_publisher: { type: 'string' }
        },
        required: ['title', 'blocks']
      },
      annotations: { destructiveHint: false },
      execute: function (a) {
        var status = 'draft';
        var url = a.translation_of
          ? '/admin/pages/' + encodeURIComponent(a.translation_of) + '/translate'
          : '/admin/pages/new';
        return M.confirm(M.t('confirm_create_builder_page', { title: a.title, status: status }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            var payload = {
              _format: 'json', title: a.title, content: '',
              language: a.language || 'en', status: status, slug: a.slug || '',
              meta_title: a.meta_title || '', meta_description: a.meta_description || '',
              meta_keywords: a.meta_keywords || '',
              og_title: a.og_title || '', og_description: a.og_description || '',
              twitter_title: a.twitter_title || '', twitter_description: a.twitter_description || '',
              faq_json: a.faq_json || '',
              schema_type: a.schema_type || ''
            };
            // The /new route needs the builder flag; the translate route infers
            // it from the source page and the blocks PUT below sets it anyway.
            if (!a.translation_of) payload.use_builder = 'on';
            // Send only when provided: the translate route inherits these from
            // the source page ONLY if the form key is absent entirely.
            if (a.schema_author !== undefined && a.schema_author !== null) payload.schema_author = a.schema_author;
            if (a.schema_publisher !== undefined && a.schema_publisher !== null) payload.schema_publisher = a.schema_publisher;
            return M.postForm(url, payload).then(jsonOf).then(function (created) {
              if (!created || !created.success || !created.id) return M.fail(created || 'Create failed.');
              var pid = created.id;
              return M.putJson('/api/pages/' + pid + '/blocks',
                               { blocks: a.blocks, convert: true })
                .then(jsonOf).then(function (saved) {
                  return M.ok({
                    success: !!(saved && saved.success), id: pid, status: status,
                    edit_url: '/admin/pages/' + pid + '/edit',
                    builder_url: '/admin/pages/' + pid + '/builder'
                  });
                });
            }).catch(M.fail);
          });
      }
    },
    {
      name: 'set_page_blocks',
      description: 'Replace the page-builder blocks of an existing page by id. This OVERWRITES ' +
                   'the current layout. Pass blocks as the array of block objects. Confirmed ' +
                   'before saving. If the page currently holds regular-editor content (see ' +
                   'converts_on_save in get_page_blocks), saving blocks CONVERTS it to the ' +
                   'Page Builder — the regular editor becomes read-only for that page — and ' +
                   'the API refuses unless convert=true is passed explicitly. Never pass ' +
                   'convert=true unless the user clearly wants that conversion.',
      inputSchema: {
        type: 'object',
        properties: {
          page_id: { type: 'integer' },
          blocks: { type: 'array', description: 'Array of page-builder block objects', items: { type: 'object' } },
          convert: { type: 'boolean', description: 'Required (true) only when the page still holds regular-editor content: acknowledges that saving converts the page to the Page Builder and makes the regular editor read-only for it. Default false.' }
        },
        required: ['page_id', 'blocks']
      },
      annotations: { destructiveHint: true },
      execute: function (a) {
        var msgKey = a.convert ? 'confirm_convert_page_blocks' : 'confirm_set_page_blocks';
        return M.confirm(M.t(msgKey, { id: a.page_id }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            return M.putJson('/api/pages/' + a.page_id + '/blocks',
                             { blocks: a.blocks, convert: !!a.convert })
              .then(jsonOf).then(M.ok).catch(M.fail);
          });
      }
    },
    {
      name: 'upload_image',
      description: 'Upload an image to the media library through the same route as the ' +
                   'human upload form (optimization, WebP/AVIF variants, EXIF stripping and ' +
                   'SVG sanitizing all apply). Pass the file as a base64 data URL ' +
                   '("data:image/png;base64,...") or raw base64 plus a filename with the ' +
                   'right extension. alt_text is MANDATORY (accessibility policy of this ' +
                   'CMS). Returns the public URL to use as featured/OG/Twitter image or in ' +
                   'builder blocks. Confirmed before uploading.',
      inputSchema: {
        type: 'object',
        properties: {
          filename: { type: 'string', description: 'e.g. sunset.jpg' },
          data: { type: 'string', description: 'Base64 data URL or raw base64 of the image file' },
          alt_text: { type: 'string', description: 'Required alt text describing the image' },
          title: { type: 'string' }, caption: { type: 'string' }, description: { type: 'string' }
        },
        required: ['filename', 'data', 'alt_text']
      },
      annotations: { destructiveHint: false },
      execute: function (a) {
        return M.confirm(M.t('confirm_upload_image', { name: a.filename }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            var blob;
            try { blob = b64ToBlob(a.data, a.filename); }
            catch (e) { return M.fail({ error: 'invalid_base64' }); }
            return M.postFile('/admin/media/upload', blob, a.filename, {
              _format: 'json', alt_text: a.alt_text,
              title: a.title || '', caption: a.caption || '', description: a.description || ''
            }).then(jsonOf).then(M.ok).catch(M.fail);
          });
      }
    },
    {
      name: 'upload_images_batch',
      description: 'Upload MANY images to the media library in one go — ideal for ' +
                   'filling a gallery, slideshow, or ticker block, or preparing images ' +
                   'for a batch of posts/pages. Shows ONE approval dialog listing every ' +
                   'filename with checkboxes; unselected files are skipped and reported ' +
                   'back. Each item takes the same fields as upload_image (base64 data ' +
                   'URL or raw base64 + filename; alt_text is MANDATORY per image — ' +
                   'accessibility policy of this CMS). Files go through the same route ' +
                   'as the human upload form (optimization, WebP/AVIF variants, EXIF ' +
                   'stripping, SVG sanitizing). Returns the public URL of every uploaded ' +
                   'image, ready to use in gallery/slideshow blocks or as ' +
                   'featured/OG/Twitter images.',
      inputSchema: {
        type: 'object',
        properties: {
          items: {
            type: 'array', minItems: 1, description: 'Images to upload',
            items: {
              type: 'object',
              properties: {
                filename: { type: 'string', description: 'e.g. sunset.jpg' },
                data: { type: 'string', description: 'Base64 data URL or raw base64 of the image file' },
                alt_text: { type: 'string', description: 'Required alt text describing the image' },
                title: { type: 'string' }, caption: { type: 'string' }, description: { type: 'string' }
              },
              required: ['filename', 'data', 'alt_text']
            }
          }
        },
        required: ['items']
      },
      annotations: { destructiveHint: false },
      execute: function (a) {
        var items = a.items || [];
        if (!items.length) return Promise.resolve(M.fail({ error: 'items_required' }));
        var labels = items.map(function (it) { return it.filename; });
        return M.confirmBatch(M.t('batch_title_images', { count: items.length }), labels)
          .then(function (selected) {
            if (!selected || !selected.length) return M.fail(M.t('declined'));
            var results = [];
            var chain = Promise.resolve();
            items.forEach(function (it, idx) {
              chain = chain.then(function () {
                if (selected.indexOf(idx) === -1) {
                  results.push({ index: idx, filename: it.filename, skipped: true });
                  return;
                }
                var blob;
                try { blob = b64ToBlob(it.data, it.filename); }
                catch (e) {
                  results.push({ index: idx, filename: it.filename, success: false, error: 'invalid_base64' });
                  return;
                }
                return M.postFile('/admin/media/upload', blob, it.filename, {
                  _format: 'json', alt_text: it.alt_text,
                  title: it.title || '', caption: it.caption || '', description: it.description || ''
                }).then(jsonOf).then(function (r) {
                  results.push({ index: idx, filename: it.filename,
                                 success: !!(r && r.success), id: r && r.id,
                                 url: r && r.url, alt_text: r && r.alt_text,
                                 error: r && r.error });
                }).catch(function (e) {
                  results.push({ index: idx, filename: it.filename, success: false, error: String(e) });
                });
              });
            });
            return chain.then(function () {
              var uploaded = results.filter(function (r) { return r.success; }).length;
              return M.ok({ success: true, requested: items.length,
                            approved: selected.length, uploaded: uploaded, results: results });
            });
          }).catch(M.fail);
      }
    },
    {
      name: 'update_post',
      description: 'Update an existing post by id through the same edit route the human ' +
                   'editor uses. Only the fields you pass change — the current record is ' +
                   'read first and everything else is preserved. Can set the featured ' +
                   'image, Open Graph image and Twitter image (URLs from list_media / ' +
                   'upload_image). status accepts only "draft" (to unpublish); agents can ' +
                   'NEVER publish — a published post stays published, its edits go live. ' +
                   'Confirmed before saving.',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          title: { type: 'string' }, content: { type: 'string', description: 'HTML body' },
          excerpt: { type: 'string' }, language: { type: 'string' },
          status: { type: 'string', enum: ['draft'], description: 'Only "draft" is allowed; omit to keep current status' },
          meta_title: { type: 'string' }, meta_description: { type: 'string' },
          meta_keywords: { type: 'string' },
          schema_type: { type: 'string', enum: ['BlogPosting', 'Article', 'NewsArticle', 'TechArticle', 'HowTo'], description: 'Schema.org type' },
          schema_author: { type: 'string' }, schema_publisher: { type: 'string' },
          og_title: { type: 'string' }, og_description: { type: 'string' },
          og_image: { type: 'string', description: 'Image URL for Open Graph sharing' },
          twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
          twitter_image: { type: 'string', description: 'Image URL for Twitter/X cards' },
          featured_image: { type: 'string', description: 'Image URL for the featured image' },
          featured_image_alt: { type: 'string' }, featured_image_caption: { type: 'string' },
          tags: { type: 'string', description: 'Comma-separated' }, category: { type: 'string' },
          faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
          slug: { type: 'string' }
        },
        required: ['id']
      },
      annotations: { destructiveHint: true },
      execute: function (a) {
        var id = a.id, changes = {};
        Object.keys(a).forEach(function (k) { if (k !== 'id') changes[k] = a[k]; });
        return mergedSave('post', id, changes, function (cur, fields) {
          return M.t('confirm_update_post', { id: cur.id, title: cur.title, status: fields.status });
        });
      }
    },
    {
      name: 'update_page',
      description: 'Update an existing page by id through the same edit route the human ' +
                   'editor uses. Only the fields you pass change — the current record is ' +
                   'read first and everything else (including Page Builder blocks) is ' +
                   'preserved. Can set the featured image, Open Graph image and Twitter ' +
                   'image (URLs from list_media / upload_image). For pages that use the ' +
                   'Page Builder, change the layout with set_page_blocks, not the content ' +
                   'field. status accepts only "draft"; agents can NEVER publish. ' +
                   'Confirmed before saving.',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          title: { type: 'string' }, content: { type: 'string', description: 'HTML body (not for builder pages)' },
          excerpt: { type: 'string' }, language: { type: 'string' },
          template: { type: 'string' },
          status: { type: 'string', enum: ['draft'], description: 'Only "draft" is allowed; omit to keep current status' },
          meta_title: { type: 'string' }, meta_description: { type: 'string' },
          meta_keywords: { type: 'string' },
          schema_type: { type: 'string', enum: ['WebPage', 'AboutPage', 'ContactPage', 'FAQPage', 'CollectionPage'], description: 'Schema.org type' },
          schema_author: { type: 'string' }, schema_publisher: { type: 'string' },
          og_title: { type: 'string' }, og_description: { type: 'string' },
          og_image: { type: 'string', description: 'Image URL for Open Graph sharing' },
          twitter_title: { type: 'string' }, twitter_description: { type: 'string' },
          twitter_image: { type: 'string', description: 'Image URL for Twitter/X cards' },
          featured_image: { type: 'string', description: 'Image URL for the featured image' },
          featured_image_alt: { type: 'string' }, featured_image_caption: { type: 'string' },
          tags: { type: 'string', description: 'Comma-separated' }, category: { type: 'string' },
          faq_json: { type: 'string', description: 'JSON array of {question, answer}' },
          slug: { type: 'string' }
        },
        required: ['id']
      },
      annotations: { destructiveHint: true },
      execute: function (a) {
        var id = a.id, changes = {};
        Object.keys(a).forEach(function (k) { if (k !== 'id') changes[k] = a[k]; });
        return mergedSave('page', id, changes, function (cur, fields) {
          return M.t('confirm_update_page', { id: cur.id, title: cur.title, status: fields.status });
        });
      }
    },
    {
      name: 'attach_image',
      description: 'Attach a media-library image to a post or page in one step: as the ' +
                   'featured image, the Open Graph image, the Twitter card image, or all ' +
                   'three (slot "all"). Pass image_id (from list_media) or a direct ' +
                   'image_url. Only the image fields change; everything else is preserved. ' +
                   'Confirmed before saving.',
      inputSchema: {
        type: 'object',
        properties: {
          content_type: { type: 'string', enum: ['post', 'page'] },
          id: { type: 'integer', description: 'Post or page id' },
          slot: { type: 'string', enum: ['featured', 'og', 'twitter', 'all'] },
          image_id: { type: 'integer', description: 'Image id from list_media' },
          image_url: { type: 'string', description: 'Direct image URL (alternative to image_id)' },
          alt: { type: 'string', description: 'Alt text override for the featured image' },
          caption: { type: 'string', description: 'Caption for the featured image' }
        },
        required: ['content_type', 'id', 'slot']
      },
      annotations: { destructiveHint: true },
      execute: function (a) {
        function resolveImage() {
          if (a.image_url) return Promise.resolve({ url: a.image_url, alt_text: a.alt || '' });
          if (!a.image_id) return Promise.reject({ error: 'image_id_or_image_url_required' });
          return M.getJson('/admin/media/api/list').then(jsonOf).then(function (items) {
            var img = (items || []).filter(function (i) { return i.id === a.image_id; })[0];
            if (!img) throw { error: 'image_not_found', id: a.image_id };
            return img;
          });
        }
        return resolveImage().then(function (img) {
          var changes = {};
          if (a.slot === 'featured' || a.slot === 'all') {
            changes.featured_image = img.url;
            changes.featured_image_alt = a.alt || img.alt_text || '';
            if (a.caption !== undefined) changes.featured_image_caption = a.caption;
          }
          if (a.slot === 'og' || a.slot === 'all') changes.og_image = img.url;
          if (a.slot === 'twitter' || a.slot === 'all') changes.twitter_image = img.url;
          return mergedSave(a.content_type, a.id, changes, function (cur) {
            return M.t('confirm_attach_image', { slot: a.slot, type: a.content_type,
                                                 id: cur.id, title: cur.title });
          });
        }).catch(M.fail);
      }
    },
    {
      name: 'build_menu',
      description: 'Set the header or footer menu to a list of pages/posts, in order. REPLACES ' +
                   'the existing menu for that location. Each item is {type:"page"|"post", id}. ' +
                   'Optional parent_index nests an item under an earlier top-level item (0-based). ' +
                   'Confirmed before saving.',
      inputSchema: {
        type: 'object',
        properties: {
          location: { type: 'string', enum: ['header', 'footer'], description: 'Defaults to header' },
          items: {
            type: 'array', description: 'Ordered menu items',
            items: {
              type: 'object',
              properties: {
                type: { type: 'string', enum: ['page', 'post'] },
                id: { type: 'integer' },
                parent_index: { type: 'integer', description: 'Index of parent top-level item' }
              },
              required: ['type', 'id']
            }
          }
        },
        required: ['items']
      },
      annotations: { destructiveHint: true },
      execute: function (a) {
        var location = a.location === 'footer' ? 'footer' : 'header';
        var items = a.items || [];
        var structure = items.map(function (it, i) {
          return { type: it.type, id: String(it.id), temp_id: 't' + i,
                   parent: (typeof it.parent_index === 'number') ? ('t' + it.parent_index) : null };
        });
        return M.confirm(M.t('confirm_build_menu', { location: location, count: items.length }))
          .then(function (go) {
            if (!go) return M.fail(M.t('declined'));
            return M.postForm('/admin/menus', {
              _format: 'json', menu_location: location, menu_structure: JSON.stringify(structure)
            }).then(jsonOf).then(M.ok).catch(M.fail);
          });
      }
    },

    // ── Navigation ───────────────────────────────────────────────────────
    {
      name: 'open_admin_section',
      description: 'Navigate the admin panel to a section so its page-specific tools and UI ' +
                   'become available.',
      inputSchema: {
        type: 'object',
        properties: {
          section: {
            type: 'string',
            enum: ['dashboard', 'posts', 'pages', 'forms', 'menus', 'media', 'files',
                   'newsletter', 'ai', 'tools', 'themes', 'users', 'settings'],
            description: 'Which admin section to open'
          }
        },
        required: ['section']
      },
      annotations: { readOnlyHint: false },
      execute: function (a) {
        var map = {
          dashboard: '/admin', posts: '/admin/posts', pages: '/admin/pages', forms: '/admin/forms',
          menus: '/admin/menus', media: '/admin/media', files: '/admin/files',
          newsletter: '/admin/newsletter', ai: '/admin/ai', tools: '/admin/tools',
          themes: '/admin/themes', users: '/admin/users', settings: '/admin/settings'
        };
        var target = map[a.section];
        if (!target) return M.fail('Unknown section.');
        window.location.href = target;
        return M.ok({ navigating_to: target });
      }
    }
  ];

  M.ensureApi(function () {
    if (!M.supported()) return;
    // The four ai_* tools run through the CMS's own AI provider (server-side,
    // owner's API key). When no provider is configured they cannot work, so
    // they are not registered at all: every tool an agent sees is a tool
    // that works. Agents bring their own model and cover generation and
    // translation themselves via the regular create/update tools.
    var tools = TOOLS;
    if (M.config.aiTools === false) {
      tools = TOOLS.filter(function (t) { return t.name.indexOf('ai_') !== 0; });
    }
    M.defineAll(tools);
    M.activate();
    if (M.config.debug) console.info('[WebMCP] admin tools active:', tools.length);
  });
})();
