<?php
/**
 * Plugin Name: Maya Grafix Blog MCP
 * Description: Read Blog posts and create or update Blog drafts through the WordPress MCP Adapter.
 * Version: 1.0.0
 * Author: Maya Grafix
 * Requires at least: 6.9
 * Requires PHP: 7.4
 * License: GPL-2.0-or-later
 * Text Domain: maya-grafix-blog-mcp
 */

defined( 'ABSPATH' ) || exit;

final class Maya_Grafix_Blog_MCP {
 const SEO = array( 'title' => 'rank_math_title', 'description' => 'rank_math_description', 'focus_keyword' => 'rank_math_focus_keyword' );

 public static function boot() {
  add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'category' ) );
  add_action( 'wp_abilities_api_init', array( __CLASS__, 'register' ) );
  add_action( 'admin_notices', array( __CLASS__, 'notice' ) );
 }

 public static function notice() {
  if ( ! current_user_can( 'manage_options' ) ) { return; }
  if ( ! function_exists( 'wp_register_ability' ) || ! post_type_exists( 'blog' ) ) {
   echo '<div class="notice notice-warning"><p>Maya Grafix Blog MCP requires the WordPress Abilities API and an existing post type named blog. Keep MCP Adapter active to expose these tools. This plugin does not register or change your Blog post type.</p></div>';
  }
 }

 public static function category() {
  wp_register_ability_category( 'maya-grafix-blog', array( 'label' => 'Maya Grafix Blog', 'description' => 'Read Blog content and manage drafts only.' ) );
 }

 private static function object_schema( $properties, $required = array() ) {
  return array( 'type' => 'object', 'properties' => $properties, 'required' => $required, 'additionalProperties' => false );
 }

 public static function register() {
  $id = array( 'type' => 'integer', 'minimum' => 1 );
  $text = array( 'type' => 'string', 'maxLength' => 500 );
  $fields = array(
   'title' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 300 ),
   'content' => array( 'type' => 'string', 'maxLength' => 300000, 'description' => 'WordPress editor HTML, optionally with Gutenberg block comments. No Markdown. Follow an existing Blog post; do not include SEO notes or social posts in the article body.' ),
   'excerpt' => array( 'type' => 'string', 'maxLength' => 5000 ),
   'slug' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200, 'pattern' => '^[a-z0-9]+(?:-[a-z0-9]+)*$' ),
   'seo' => self::object_schema( array( 'title' => $text, 'description' => $text, 'focus_keyword' => $text ) ),
  );
  $definitions = array(
   'list-blogs' => array( 'List Blog posts', 'List editable published posts and drafts in the blog post type. Read a relevant post before copying its formatting.', self::object_schema( array( 'search' => array( 'type' => 'string', 'maxLength' => 200 ), 'page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 10000 ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 20 ), 'status' => array( 'type' => 'string', 'enum' => array( 'draft', 'publish', 'both' ) ) ) ), true ),
   'get-blog' => array( 'Read a Blog post', 'Read raw editor content, excerpt, SEO fields and a version token. Restricted to editable Blog drafts and published Blog posts.', self::object_schema( array( 'post_id' => $id ), array( 'post_id' ) ), true ),
   'create-blog-draft' => array( 'Create a Blog draft', 'Create a draft only. Supply a unique request_id and reuse the same input on retry. Returns saved content and a version token. Cannot publish, delete or change site styles.', self::object_schema( array_merge( $fields, array( 'request_id' => array( 'type' => 'string', 'minLength' => 16, 'maxLength' => 80, 'pattern' => '^[A-Za-z0-9_-]+$' ) ) ), array( 'title', 'content', 'slug', 'request_id' ) ), false ),
   'update-blog-draft' => array( 'Update a Blog draft', 'Update supplied fields on an existing draft only. First read get-blog and pass its version as expected_version. Omitted fields stay unchanged. Refuses published, scheduled, private, pending and trashed posts.', self::object_schema( array_merge( $fields, array( 'post_id' => $id, 'expected_version' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ) ) ), array( 'post_id', 'expected_version' ) ), false ),
  );
  foreach ( $definitions as $name => $def ) {
   wp_register_ability( 'maya-grafix/' . $name, array(
    'label' => $def[0], 'description' => $def[1], 'category' => 'maya-grafix-blog',
    'input_schema' => $def[2], 'output_schema' => array( 'type' => 'object', 'additionalProperties' => true ),
    'permission_callback' => static function ( $input ) use ( $name ) { return self::permission( $name, $input ); },
    'execute_callback' => static function ( $input ) use ( $name ) { return self::execute( $name, $input ); },
    'meta' => array( 'show_in_rest' => false, 'mcp' => array( 'public' => true, 'type' => 'tool' ), 'annotations' => array( 'readonly' => $def[3], 'destructive' => false, 'idempotent' => $def[3] ) ),
   ) );
  }
 }

 private static function error( $code, $message, $status = 400 ) {
  return new WP_Error( 'mg_blog_' . $code, $message, array( 'status' => $status ) );
 }

 public static function permission( $name, $input ) {
  $type = get_post_type_object( 'blog' );
  if ( ! $type || ! is_user_logged_in() || ! current_user_can( $type->cap->edit_posts ) ) {
   return self::error( 'forbidden', 'An authenticated user with Blog editing permissions is required.', 403 );
  }
  if ( 'create-blog-draft' === $name && ! current_user_can( $type->cap->create_posts ) ) {
   return self::error( 'forbidden', 'You cannot create Blog drafts.', 403 );
  }
  if ( in_array( $name, array( 'get-blog', 'update-blog-draft' ), true ) ) {
   $post = get_post( isset( $input['post_id'] ) ? (int) $input['post_id'] : 0 );
   if ( ! $post || 'blog' !== $post->post_type || ! current_user_can( 'edit_post', $post->ID ) ) {
    return self::error( 'forbidden', 'That Blog post is unavailable or not editable by this user.', 403 );
   }
   $statuses = 'get-blog' === $name ? array( 'draft', 'publish' ) : array( 'draft' );
   if ( ! in_array( $post->post_status, $statuses, true ) ) {
    return self::error( 'status', 'This action does not allow the current post status.', 409 );
   }
  }
  return true;
 }

 private static function read( $id, $full = true ) {
  $p = get_post( $id );
  $seo = array();
  foreach ( self::SEO as $key => $meta ) { $seo[$key] = (string) get_post_meta( $id, $meta, true ); }
  $data = array( 'post_id' => $p->ID, 'post_type' => $p->post_type, 'status' => $p->post_status, 'title' => $p->post_title, 'slug' => $p->post_name, 'content' => $p->post_content, 'excerpt' => $p->post_excerpt, 'seo' => $seo, 'modified_gmt' => $p->post_modified_gmt );
  $data['version'] = hash( 'sha256', wp_json_encode( $data ) );
  $data['edit_url'] = admin_url( 'post.php?post=' . $id . '&action=edit' );
  $data['url'] = get_permalink( $id );
  $data['rank_math_active'] = defined( 'RANK_MATH_VERSION' );
  if ( ! $full ) { unset( $data['content'], $data['seo'] ); }
  return $data;
 }

 public static function execute( $name, $input ) {
  $allowed = self::permission( $name, $input );
  if ( true !== $allowed ) { return $allowed; }
  if ( 'get-blog' === $name ) { return self::read( (int) $input['post_id'] ); }
  if ( 'list-blogs' === $name ) {
   $type = get_post_type_object( 'blog' );
   $args = array( 'post_type' => 'blog', 'post_status' => array( 'draft', 'publish' ), 'posts_per_page' => isset( $input['per_page'] ) ? (int) $input['per_page'] : 10, 'paged' => isset( $input['page'] ) ? (int) $input['page'] : 1, 'orderby' => 'modified', 'order' => 'DESC', 's' => isset( $input['search'] ) ? sanitize_text_field( $input['search'] ) : '' );
   if ( ! empty( $input['status'] ) && 'both' !== $input['status'] ) { $args['post_status'] = $input['status']; }
   if ( ! current_user_can( $type->cap->edit_others_posts ) ) { $args['author'] = get_current_user_id(); }
   $q = new WP_Query( $args );
   $posts = array();
   foreach ( $q->posts as $p ) {
    if ( current_user_can( 'edit_post', $p->ID ) ) { $posts[] = self::read( $p->ID, false ); }
   }
   return array( 'posts' => $posts, 'page' => $args['paged'], 'has_more' => $args['paged'] < (int) $q->max_num_pages );
  }
  return self::write( $name, $input );
 }

 private static function fields( $input ) {
  $result = array();
  $map = array( 'title' => 'post_title', 'content' => 'post_content', 'excerpt' => 'post_excerpt', 'slug' => 'post_name' );
  foreach ( $map as $key => $field ) {
   if ( ! array_key_exists( $key, $input ) ) { continue; }
   $value = (string) $input[$key];
   if ( 'content' === $key || 'excerpt' === $key ) { $value = wp_kses_post( $value ); }
   elseif ( 'slug' === $key ) { $value = sanitize_title( $value ); }
   else { $value = sanitize_text_field( $value ); }
   if ( in_array( $key, array( 'title', 'slug' ), true ) && '' === trim( $value ) ) {
    return self::error( 'empty', 'Title and slug cannot be empty.' );
   }
   $result[$field] = $value;
  }
  return $result;
 }

 private static function write( $name, $input ) {
  $create = 'create-blog-draft' === $name;
  if ( ! $create && 'update-blog-draft' !== $name ) { return self::error( 'unknown', 'Unknown action.' ); }
  $fields = self::fields( $input );
  if ( is_wp_error( $fields ) ) { return $fields; }
  if ( ! $fields && empty( $input['seo'] ) ) { return self::error( 'empty', 'Provide at least one field to update.' ); }
  // Options use a unique database key to serialize this plugin's writes.
  // Locks never expire automatically: a crashed write needs inspection, not a blind retry.
  $request_key = $create ? 'mg_blog_request_' . hash( 'sha256', get_current_user_id() . ':' . $input['request_id'] ) : '';
  $lock = $create ? $request_key . '_lock' : 'mg_blog_post_lock_' . (int) $input['post_id'];
  if ( ! add_option( $lock, time(), '', false ) ) { return self::error( 'busy', 'A write is running or needs recovery. Inspect the existing draft before retrying. Do not use a new request ID.', 409 ); }
  try {
   if ( $create ) {
    $fingerprint_input = $input;
    ksort( $fingerprint_input );
    if ( isset( $fingerprint_input['seo'] ) ) { ksort( $fingerprint_input['seo'] ); }
    $fingerprint = hash( 'sha256', wp_json_encode( $fingerprint_input ) );
    $previous = get_option( $request_key );
    if ( $previous ) {
     if ( $previous['fingerprint'] !== $fingerprint ) { return self::error( 'request_conflict', 'This request_id was already used with different content.', 409 ); }
     if ( empty( $previous['post_id'] ) ) { return self::error( 'recovery', 'An earlier creation did not finish. Inspect Blog drafts; do not create another request.', 409 ); }
     $access = self::permission( 'get-blog', array( 'post_id' => $previous['post_id'] ) );
     if ( true !== $access ) { return $access; }
     return array( 'replayed' => true, 'post' => self::read( $previous['post_id'] ) );
    }
    $existing = get_posts( array( 'post_type' => 'blog', 'post_status' => array( 'draft', 'publish', 'pending', 'future', 'private', 'trash' ), 'name' => $fields['post_name'], 'numberposts' => 1, 'fields' => 'ids' ) );
    if ( $existing ) { return self::error( 'slug_exists', 'A Blog post already uses this slug. Find and read it before deciding what to do.', 409 ); }
    if ( ! add_option( $request_key, array( 'fingerprint' => $fingerprint, 'post_id' => 0 ), '', false ) ) { return self::error( 'journal', 'Could not reserve the request ID.', 409 ); }
    $fields['post_type'] = 'blog';
    $fields['post_status'] = 'draft';
    $fields['post_author'] = get_current_user_id();
    $id = wp_insert_post( wp_slash( $fields ), true );
    if ( is_wp_error( $id ) ) { return $id; }
    update_option( $request_key, array( 'fingerprint' => $fingerprint, 'post_id' => $id ), false );
   } else {
    $id = (int) $input['post_id'];
    clean_post_cache( $id );
    $access = self::permission( $name, $input );
    if ( true !== $access ) { return $access; }
    $before = self::read( $id );
    if ( ! hash_equals( $before['version'], $input['expected_version'] ) ) { return self::error( 'stale', 'The draft changed. Read it again and review the differences before updating.', 409 ); }
    if ( ! function_exists( 'wp_check_post_lock' ) ) { require_once ABSPATH . 'wp-admin/includes/post.php'; }
    if ( wp_check_post_lock( $id ) ) { return self::error( 'editor_lock', 'Another user is editing this draft.', 409 ); }
    wp_save_post_revision( $id );
    // Do not accept status/type/author input or force a draft status over an existing status.
    $fields['ID'] = $id;
    $updated = wp_update_post( wp_slash( $fields ), true );
    if ( is_wp_error( $updated ) ) { return $updated; }
   }
   clean_post_cache( $id );
   $p = get_post( $id );
   if ( ! $p || 'draft' !== $p->post_status || 'blog' !== $p->post_type ) {
    return self::error( 'unexpected_status', 'A concurrent edit or site hook changed the post status/type. Inspect the post before continuing; SEO was not changed.', 409 );
   }
   if ( isset( $input['seo'] ) ) {
    foreach ( self::SEO as $key => $meta ) {
     if ( array_key_exists( $key, $input['seo'] ) ) { update_post_meta( $id, $meta, wp_slash( sanitize_text_field( $input['seo'][$key] ) ) ); }
    }
   }
   $saved = self::read( $id );
   $warnings = array();
   foreach ( array( 'title' => 'post_title', 'content' => 'post_content', 'excerpt' => 'post_excerpt', 'slug' => 'post_name' ) as $key => $unused ) {
    if ( isset( $input[$key] ) && $input[$key] !== $saved[$key] ) { $warnings[] = $key . ' was normalized or sanitized; review the saved value.'; }
   }
   foreach ( isset( $input['seo'] ) ? $input['seo'] : array() as $key => $value ) {
    if ( $saved['seo'][$key] !== sanitize_text_field( $value ) ) { $warnings[] = 'SEO field ' . $key . ' did not save as requested.'; }
   }
   return array( 'replayed' => false, 'post' => $saved, 'warnings' => $warnings );
  } finally {
   delete_option( $lock );
  }
 }
}

Maya_Grafix_Blog_MCP::boot();
