<?php

namespace ModernFields\FieldGroups\Locations;

use ModernFields\FieldGroups\Entities\FieldGroup;
use ModernFields\FieldGroups\Entities\Post;
use ModernFields\FieldGroups\Locations\UseCases\GroupAssetsLoaderUseCase;
use ModernFields\FieldGroups\Repositories\FieldGroupRepository;
use ModernFields\FieldGroups\Repositories\PostRepository;
use ModernFields\FieldGroups\Repositories\UserRepository;
use ModernFields\WordPress\Security\SecurityHelper;

defined('ABSPATH') || exit;

/**
 * AbstractPostLocation Class
 *
 * Shared base for the two post edit-screen strategies:
 * - PostLocation: post types using the block editor (meta saved through the
 *   core/editor store and the REST save flow).
 * - ClassicPostLocation: post types using the classic editor (meta submitted as
 *   regular form inputs and saved on save_post, like users / media / terms).
 *
 * Holds everything both strategies share: detecting the post edit screen,
 * resolving the edited post, discovering its field groups, rendering the
 * metabox root and enqueuing the React assets. Subclasses only decide which
 * post types they own, which metabox context each group goes to, and which
 * JavaScript bundle boots.
 *
 * @since 1.1.0
 */
abstract class AbstractPostLocation extends Location
{
  /**
   * Register the hooks shared by both post location strategies.
   *
   * @since 1.1.0
   * @return void
   */
  public function registerHooks(): void
  {
    add_action('add_meta_boxes', [$this, 'registerPostMetaboxes'], 10, 2);
    add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
  }

  /**
   * Decide whether this strategy owns the given post type.
   *
   * @since 1.1.0
   * @param string $post_type The post type being edited.
   * @return bool True when this strategy should handle the post type.
   */
  abstract protected function shouldHandlePostType(string $post_type): bool;

  /**
   * Resolve the metabox context a field group should be registered into.
   *
   * Returning null skips the group (e.g. block-editor inspector groups, which
   * are rendered through registerPlugin in JavaScript instead).
   *
   * @since 1.1.0
   * @param FieldGroup $field_group The field group to place.
   * @return string|null One of 'normal' / 'side' / 'advanced', or null to skip.
   */
  abstract protected function getMetaboxContext(FieldGroup $field_group): ?string;

  /**
   * The build bundle name to enqueue for this strategy.
   *
   * @since 1.1.0
   * @return string
   */
  abstract protected function getBundleName(): string;

  /**
   * Post types that never receive Modern Fields post metaboxes.
   *
   * Media has its own location and the Modern Fields CPT manages its own UI.
   *
   * @since 1.1.0
   * @return array<string>
   */
  protected function getExcludedPostTypes(): array
  {
    return ['attachment', MODERN_FIELDS_CPT_SLUG];
  }

  /**
   * Resolve the post type for the current post edit screen.
   *
   * @since 1.1.0
   * @return string|null The post type, or null when it can't be determined.
   */
  protected function resolvePostType(): ?string
  {
    $current_admin_page = SecurityHelper::getCurrentAdminPage();

    if ($current_admin_page === 'post-new.php') {
      return isset($_GET['post_type']) ? sanitize_key(wp_unslash($_GET['post_type'])) : 'post';
    }

    if ($current_admin_page === 'post.php') {
      return get_post_type($this->resolvePostId()) ?: null;
    }

    return null;
  }

  /**
   * Resolve the post ID for the current post edit screen.
   *
   * Returns 0 on post-new.php, where WordPress exposes the auto-draft through
   * the global $post that get_post() falls back to.
   *
   * @since 1.1.0
   * @return int
   */
  protected function resolvePostId(): int
  {
    return isset($_GET['post']) ? absint($_GET['post']) : 0;
  }

  /**
   * Whether the current admin screen is a post edit screen owned by this strategy.
   *
   * @since 1.1.0
   * @return bool
   */
  protected function isHandledPostEditScreen(): bool
  {
    if (!SecurityHelper::isCurrentAdminPage(['post.php', 'post-new.php'])) {
      return false;
    }

    $post_type = $this->resolvePostType();

    if (!$post_type || in_array($post_type, $this->getExcludedPostTypes(), true)) {
      return false;
    }

    return $this->shouldHandlePostType($post_type);
  }

  /**
   * Register every applicable field group as a metabox for the current post.
   *
   * @since 1.1.0
   * @param string  $post_type The current post type being edited.
   * @param \WP_Post $post      The post object currently being edited.
   * @return void
   */
  public function registerPostMetaboxes($post_type, $post): void
  {
    if (!$this->isHandledPostEditScreen()) {
      return;
    }

    $post = PostRepository::getPostById($post->ID);
    $field_groups = FieldGroupRepository::getPostsFieldGroups($post);

    foreach ($field_groups as $field_group) {
      $context = $this->getMetaboxContext($field_group);

      if ($context === null) {
        continue;
      }

      add_meta_box(
        'mf-post-fields-metabox-' . $field_group->getId(),
        $field_group->getTitle(),
        [$this, 'renderMetabox'],
        $field_group->getPostType(),
        $context,
        'high',
        ["group_id" => $field_group->getId()]
      );
    }
  }

  /**
   * Render the metabox root the React application mounts onto.
   *
   * @since 1.1.0
   * @param \WP_Post $post    The post being edited.
   * @param array    $metabox The metabox arguments.
   * @return void
   */
  public function renderMetabox($post, $metabox): void
  {
    echo '<div class="mf-post-fields-root" data-group-id="' . esc_attr($metabox['args']['group_id']) . '"></div>';
  }

  /**
   * Enqueue the React assets for the current post edit screen.
   *
   * @since 1.1.0
   * @return void
   */
  public function enqueueAssets(): void
  {
    if (!$this->isHandledPostEditScreen()) {
      return;
    }

    $post = PostRepository::getPostById($this->resolvePostId());
    $field_groups = FieldGroupRepository::getPostsFieldGroups($post);

    $groupAssetsLoaderUseCase = new GroupAssetsLoaderUseCase(
      $this->getBundleName(),
      $field_groups,
      $this->buildContext($post)
    );
    $groupAssetsLoaderUseCase->execute();
  }

  /**
   * Build the JavaScript context shared by both strategies.
   *
   * Exposes the options and user field groups so the field value block can
   * resolve them. Subclasses may extend this (e.g. classic posts inject the
   * post meta so the React state can seed itself without the editor store).
   *
   * @since 1.1.0
   * @param Post $post The post being edited.
   * @return array<string, mixed>
   */
  protected function buildContext(Post $post): array
  {
    $author = UserRepository::getUserById($post->getAuthorId());

    # Options pages and dashboard widgets both store their values as site
    # options: the field value block offers them under the same source.
    $options_groups = FieldGroupRepository::getOptionsBackedFieldGroups();

    # The author can be legitimately missing (post_author = 0 on WP-CLI/API
    # creations, or a deleted user whose content was not reassigned). Fall
    # back to the generic user groups so the field value block picker keeps
    # the field schema — mirroring FieldValueSourcesAssets::resolveUserGroups(),
    # whose payload must stay interchangeable with this one.
    $user_groups = $author
      ? FieldGroupRepository::getUserFieldGroups($author)
      : FieldGroupRepository::getNewUserFieldGroups();

    return [
      "optionsGroups" => array_map(fn(FieldGroup $group) => $group->toArray(), $options_groups),
      "userGroups" => array_map(fn(FieldGroup $group) => $group->toArray(), $user_groups),
    ];
  }
}
