/**
 * @package PublishPress
 * @author PublishPress
 *
 * PublishPress Taxonomy Filter for Gutenberg
 * This file manages checklist visibility based on taxonomy terms in the Gutenberg editor.
 */

/**
 * Class PPChTaxonomyFilter manages which checklists are shown or hidden based on taxonomy terms.
 * 
 * @class PPChTaxonomyFilter
 */
class PPChTaxonomyFilter {
  /**
   * DOM element selectors used throughout the class.
   * 
   * @static
   * @type {Object}
   */
  static SELECTORS = {
    checklistBox: '#pp-checklists-sidebar-req-box',
    sidebarContent: '#pp-checklists-sidebar-content',
    metaBox: '#pp-checklists-req-box',
    metaBoxContainer: '#pp_checklist_meta-meta-box',
    panelArea: '#publishpress-checklists-panel\\:checklists-sidebar',
    editorSidebar: '.interface-interface-skeleton__sidebar'
  };

  /**
   * Event names for communication with other components.
   * 
   * @static
   * @type {Object}
   */
  static EVENTS = {
    TIC: 'pp-checklists:tic',
    VALIDATE: 'pp-checklists:validate_requirements',
    UPDATE_STATE: 'pp-checklists:update_requirement_state',
    UPDATE_FAILED: 'pp-checklists.update-failed-requirements',
    REQUIREMENTS_UPDATED: 'pp-checklists.requirements-updated'
  };

  /**
   * Mapping of taxonomy slugs to their REST base names used in Gutenberg
   * WordPress uses different attribute names in the editor
   * 
   * @static
   * @type {Object}
   */
  static TAXONOMY_REST_MAP = {
    'category': 'categories',
    'post_tag': 'tags'
  };

  /**
   * Constructs the taxonomy filter instance.
   * 
   * @param {Object} wp - WordPress global object containing data, hooks, and domReady.
   * @param {Function} $ - jQuery instance for DOM manipulation.
   * @param {Object} data - Localized config data (exclusionOptions, enabledTaxonomies, taxonomyRestBases, postType).
   */
  constructor(wp, $, data) {
    this.wp = wp;
    this.$ = $;
    this.data = data || {};
    this.select = wp.data.select;
    this.dispatch = wp.data.dispatch;
    this.domReady = wp.domReady;

    // State tracking
    this.previousTerms = {};
    this.currentTerms = {};
    this.exclusionOptions = data.exclusionOptions || {};
    this.enabledTaxonomies = data.enabledTaxonomies || [];
    this.taxonomyRestBases = data.taxonomyRestBases || {};
    this.postType = data.postType || 'post';

    // Throttle timer for subscribe callback
    this._subscribeThrottleTimer = null;
  }

  /**
   * Initializes the component when DOM is ready.
   * 
   * @returns {void}
   */
  init() {
    this.domReady(() => {
      this._trackTaxonomyTerms();
      this._setupEventListeners();
      
      // Initial visibility update after a short delay
      setTimeout(() => {
        this.applyVisibilityRules();
      }, 500);
    });
  }

  /**
   * Sets up event listeners for the component.
   * 
   * @private
   * @returns {void}
   */
  _setupEventListeners() {
    // Listen for periodic ticks to update visibility
    this.$(document).on(PPChTaxonomyFilter.EVENTS.TIC, () => {
      this.applyVisibilityRules();
    });
  }

  /**
   * Gets the REST base attribute name for a taxonomy
   * WordPress Gutenberg uses REST base names for taxonomy attributes
   * 
   * @param {string} taxonomy - The taxonomy slug
   * @returns {string} The REST base name to use in getEditedPostAttribute
   */
  _getTaxonomyRestBase(taxonomy) {
    // First check if we have it from PHP
    if (this.taxonomyRestBases[taxonomy]) {
      return this.taxonomyRestBases[taxonomy];
    }
    // Check static mapping
    if (PPChTaxonomyFilter.TAXONOMY_REST_MAP[taxonomy]) {
      return PPChTaxonomyFilter.TAXONOMY_REST_MAP[taxonomy];
    }
    // Default: use the taxonomy slug itself
    return taxonomy;
  }

  /**
   * Gets current taxonomy terms from the editor for all enabled taxonomies.
   * 
   * @returns {Object} Object with taxonomy slugs as keys and arrays of term IDs as values
   */
  getCurrentTerms() {
    const editorStore = this.select('core/editor');
    const terms = {};

    for (const taxonomy of this.enabledTaxonomies) {
      // Get the REST base name for this taxonomy
      const restBase = this._getTaxonomyRestBase(taxonomy);
      
      // Try to get edited terms using REST base name
      let editedTerms = editorStore.getEditedPostAttribute?.(restBase);
      
      // If not found with REST base, try with taxonomy slug directly
      if (!Array.isArray(editedTerms)) {
        editedTerms = editorStore.getEditedPostAttribute?.(taxonomy);
      }
      
      if (Array.isArray(editedTerms)) {
        terms[taxonomy] = editedTerms;
      } else {
        // Fallback to current post terms
        const currentPost = editorStore.getCurrentPost?.();
        if (currentPost) {
          // Try REST base first, then taxonomy slug
          if (Array.isArray(currentPost[restBase])) {
            terms[taxonomy] = currentPost[restBase];
          } else if (Array.isArray(currentPost[taxonomy])) {
            terms[taxonomy] = currentPost[taxonomy];
          } else {
            terms[taxonomy] = [];
          }
        } else {
          terms[taxonomy] = [];
        }
      }
    }

    return terms;
  }

  /**
   * Converts current terms to the format used in exclusion options (taxonomy:term_id)
   * 
   * @returns {Array} Array of term values in taxonomy:term_id format
   */
  getCurrentTermValues() {
    const termValues = [];
    
    for (const [taxonomy, termIds] of Object.entries(this.currentTerms)) {
      for (const termId of termIds) {
        termValues.push(`${taxonomy}:${termId}`);
      }
    }
    
    return termValues;
  }

  /**
   * Applies visibility rules to checklist items based on current taxonomy terms.
   * Shows or hides items according to configuration.
   * 
   * @returns {void}
   */
  applyVisibilityRules() {
    const $items = this.$('li.pp-checklists-req');
    if ($items.length === 0) return;

    const currentTermValues = this.getCurrentTermValues();
    
    // Process each checklist item
    const self = this;
    $items.each(function(_, element) {
      const $element = self.$(element);
      const id = $element.data('id');
      if (!id) return;
      
      const excludedTerms = self.exclusionOptions[id] || [];
      const shouldHideByTaxonomy = self._shouldHideItem(currentTermValues, excludedTerms);
      
      // Mark element with taxonomy filter state using data attribute
      $element.data('taxonomy-hidden', shouldHideByTaxonomy);
      
      if (shouldHideByTaxonomy) {
        $element.hide();
        // Store original classes and remove blocking classes so validation ignores this item
        if (!$element.data('taxonomy-original-classes')) {
          $element.data('taxonomy-original-classes', {
            hasBlock: $element.hasClass('pp-checklists-block'),
            hasWarning: $element.hasClass('pp-checklists-warning'),
            hasStatusNo: $element.hasClass('status-no')
          });
        }
        $element.removeClass('pp-checklists-block pp-checklists-warning status-no');
      } else {
        // Restore original classes if they were saved
        const originalClasses = $element.data('taxonomy-original-classes');
        if (originalClasses) {
          if (originalClasses.hasBlock) $element.addClass('pp-checklists-block');
          if (originalClasses.hasWarning) $element.addClass('pp-checklists-warning');
          if (originalClasses.hasStatusNo) $element.addClass('status-no');
          $element.removeData('taxonomy-original-classes');
        }
        // Only show if not hidden by other filters (check for status filter)
        const isHiddenByStatus = $element.data('status-hidden') === true;
        if (!isHiddenByStatus) {
          $element.show();
        }
      }
    });

    // Update UI based on visible items
    const noVisibleItems = this.$('li.pp-checklists-req:visible').length === 0;
    this.updateNoTasksMessage(noVisibleItems);
    this.updateWarningIcon();
    
    // Trigger re-validation to update publish lock state
    this._triggerValidation();
  }

  /**
   * Triggers the core checklists validation to update publish lock state.
   * This ensures hidden requirements don't block publishing.
   * 
   * @private
   * @returns {void}
   */
  _triggerValidation() {
    // Use the core PP_Checklists validation if available
    if (window.PP_Checklists && typeof window.PP_Checklists.validate_requirements === 'function') {
      // Small delay to ensure DOM updates are complete
      setTimeout(() => {
        window.PP_Checklists.validate_requirements();
      }, 50);
    }
  }

  /**
   * Determines if a checklist item should be hidden based on taxonomy rules.
   * 
   * @private
   * @param {Array} currentTermValues - Current post's term values
   * @param {Array} excludedTerms - Terms that should exclude this item
   * @returns {boolean} True if the item should be hidden
   */
  _shouldHideItem(currentTermValues, excludedTerms) {
    if (!Array.isArray(excludedTerms) || excludedTerms.length === 0) {
      return false;
    }

    // Check if any current term is in the excluded list
    for (const termValue of currentTermValues) {
      if (excludedTerms.includes(termValue)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Toggles the "no tasks" message in both Gutenberg sidebar and classic editor.
   * 
   * @param {boolean} show - Whether to show the message when no items remain
   * @returns {void}
   */
  updateNoTasksMessage(show) {
    // Update Gutenberg sidebar
    this._updateSidebarNoTasksMessage(show);
    
    // Update Gutenberg metabox
    this._updateMetaboxNoTasksMessage(show);
  }

  /**
   * Updates the "no tasks" message in the Gutenberg sidebar.
   * 
   * @private
   * @param {boolean} show - Whether to show the message
   * @returns {void}
   */
  _updateSidebarNoTasksMessage(show) {
    const $sidebarContentContainer = this.$(PPChTaxonomyFilter.SELECTORS.sidebarContent);
    if (!$sidebarContentContainer.length) return;

    // Remove any existing taxonomy filter message
    $sidebarContentContainer.find('.pp-no-tasks-message-taxonomy').remove();
    
    // Toggle visibility of "required" text
    $sidebarContentContainer.find('em').filter(function() {
      return this.textContent.toLowerCase().indexOf('required') !== -1;
    })[show ? 'hide' : 'show']();
    
    if (show && window.i18n?.noTaskLabel) {
      // Hide the checklist box
      this.$(PPChTaxonomyFilter.SELECTORS.checklistBox).hide();
      // Only add message if no other "no tasks" message exists (avoid duplicates with status filter)
      if ($sidebarContentContainer.find('.pp-no-tasks-message').length === 0) {
        $sidebarContentContainer.prepend(`<p class="pp-no-tasks-message-taxonomy"><em>${window.i18n.noTaskLabel}</em></p>`);
      }
    } else {
      // Show the checklist box
      this.$(PPChTaxonomyFilter.SELECTORS.checklistBox).show();
    }
  }

  /**
   * Updates the "no tasks" message in the Classic editor metabox.
   * 
   * @private
   * @param {boolean} show - Whether to show the message
   * @returns {void}
   */
  _updateMetaboxNoTasksMessage(show) {
    const $meta = this.$(PPChTaxonomyFilter.SELECTORS.metaBox);
    if (!$meta.length) return;

    const $box = this.$(PPChTaxonomyFilter.SELECTORS.metaBoxContainer);
    
    // Find the existing server-rendered "no tasks" message inside the metabox
    const $existingMessage = $meta.find('p').filter(function() {
      return this.textContent.toLowerCase().indexOf('checklist tasks') !== -1;
    });
    
    if (show) {
      // Hide requirement elements
      $meta.find('.required, .pp-checklists-required-legend').hide();
      $box.find('em').filter(function() {
        return this.textContent.toLowerCase().indexOf('required') !== -1;
      }).hide();
      
      // If server already rendered a message, just show it; otherwise add our own (if no duplicate exists)
      if ($existingMessage.length) {
        $existingMessage.show();
      } else if (!$meta.siblings('.pp-no-tasks-message-taxonomy').length && !$meta.siblings('.pp-no-tasks-message').length) {
        const message = window.ppChecklists?.noTaskLabel || "You don't have to complete any Checklist tasks.";
        $meta.before(`<p class="pp-no-tasks-message-taxonomy"><em>${message}</em></p>`);
      }
    } else {
      // Hide server-rendered message
      $existingMessage.hide();
      
      // Only show requirement elements if there are blocks
      if ($meta.find('.pp-checklists-block').length) {
        $meta.find('.required, .pp-checklists-required-legend').show();
        $box.find('em').filter(function() {
          return this.textContent.toLowerCase().indexOf('required') !== -1;
        }).show();
      }
      
      // Remove the JS-added message
      $meta.siblings('.pp-no-tasks-message-taxonomy').remove();
    }
  }

  /**
   * Updates the warning icon based on unfulfilled checklist requirements.
   * Triggers validation and updates the UI accordingly.
   * 
   * @returns {void}
   */
  updateWarningIcon() {
    // Try to validate requirements if core plugin available
    this._validateRequirements();

    // Count failed requirements not excluded for current terms
    const failedRequirementCount = this._countFailedRequirements();

    // Update body class for warning icon
    this.$('body')[failedRequirementCount > 0 ? 'addClass' : 'removeClass']('ppch-show-publishing-warning-icon');
    
    // Update warning component with failed requirements
    this.updatePublishPressWarningComponent();
  }

  /**
   * Validates requirements using the core checklists plugin.
   * 
   * @private
   * @returns {void}
   */
  _validateRequirements() {
    if (!window.PP_Checklists?.validate_requirements) return;
    
    try {
      // Temporarily set publishing state to false to avoid triggering
      // the publishing flow during validation
      const previousPublishingState = window.PP_Checklists.state.is_publishing;
      window.PP_Checklists.state.is_publishing = false;
      
      // Trigger validation
      this.$(document).trigger(PPChTaxonomyFilter.EVENTS.VALIDATE);
      
      // Restore previous state
      window.PP_Checklists.state.is_publishing = previousPublishingState;
    } catch (e) {
      console.error('Error during checklist validation for warning icon:', e);
    }
  }

  /**
   * Counts failed requirements that are visible for the current terms.
   * 
   * @private 
   * @returns {number} Count of failed requirements
   */
  _countFailedRequirements() {
    let failedCount = 0;
    const currentTermValues = this.getCurrentTermValues();

    // Count visible failed requirements
    this.$('li.pp-checklists-req.status-no').each((_, element) => {
      const $element = this.$(element);
      const id = $element.data('id');
      if (!id) return;

      // Check if this requirement is excluded for current terms
      const excludedTerms = this.exclusionOptions[id] || [];
      const isHidden = this._shouldHideItem(currentTermValues, excludedTerms);
      
      // Count if not hidden
      if (!isHidden) {
        failedCount++;
      }
    });

    return failedCount;
  }

  /**
   * Updates the PublishPress warning component with failed requirements.
   * Uses wp.hooks to communicate with other components.
   * 
   * @returns {void}
   */
  updatePublishPressWarningComponent() {
    if (!this.wp.hooks?.doAction) return;

    // Prepare containers for failed requirements
    const items = { block: [], warning: [] };
    const currentTermValues = this.getCurrentTermValues();

    // Collect visible failed requirements
    this._collectFailedRequirements(items, currentTermValues);
    
    // Notify other components about failed requirements
    this.wp.hooks.doAction(PPChTaxonomyFilter.EVENTS.UPDATE_FAILED, items);
  }

  /**
   * Collects failed requirements and categorizes them by type (block/warning).
   * Prioritizes sidebar items when available to prevent duplication.
   * 
   * @private
   * @param {Object} items - Container for failed requirements
   * @param {Array} currentTermValues - Current post's term values
   * @returns {void}
   */
  _collectFailedRequirements(items, currentTermValues) {
    // Prioritize sidebar items when available to prevent duplication
    const $sidebarItems = this.$('#pp-checklists-sidebar-req-box li.pp-checklists-req.status-no');
    const $items = $sidebarItems.length > 0 ? $sidebarItems : this.$('#pp-checklists-req-box li.pp-checklists-req.status-no');
    
    $items.each((_, element) => {
      const $element = this.$(element);
      const id = $element.data('id');
      if (!id) return;

      // Skip if the item is excluded for current terms
      const excludedTerms = this.exclusionOptions[id] || [];
      if (this._shouldHideItem(currentTermValues, excludedTerms)) return;

      // Find the label and add to appropriate category
      const $label = $element.find('.status-label');
      if ($label.length) {
        const labelText = $label.html().trim();
        
        if ($element.hasClass('pp-checklists-block')) {
          items.block.push(labelText);
        } else if ($element.hasClass('pp-checklists-warning')) {
          items.warning.push(labelText);
        }
      }
    });
  }

  /**
   * Updates the visibility of checklist items based on taxonomy terms.
   * 
   * @param {Object} newTerms - The updated taxonomy terms
   * @returns {void}
   */
  updateChecklistItemsVisibility(newTerms) {
    // Update current terms tracker
    this.currentTerms = newTerms;
    
    // Apply visibility rules
    this.applyVisibilityRules();
  }

  /**
   * Initializes taxonomy term tracking and updates visibility accordingly.
   * 
   * @private
   * @returns {void}
   */
  _trackTaxonomyTerms() {
    try {
      // Get initial taxonomy terms
      const initialTerms = this.getCurrentTerms();
      
      // Set terms and update visibility
      this.previousTerms = initialTerms;
      this.currentTerms = initialTerms;
      this.updateChecklistItemsVisibility(initialTerms);
      
      // Subscribe to term changes
      this._subscribeTermChanges();
    } catch (e) {
      console.error('PPChTaxonomyFilter init term tracking error:', e);
    }
  }

  /**
   * Subscribes to taxonomy term changes in the editor.
   * Throttled to avoid excessive updates on every store change.
   * 
   * @private
   * @returns {void}
   */
  _subscribeTermChanges() {
    const self = this;
    
    this.wp.data.subscribe(function() {
      // Throttle: only check terms every 200ms max
      if (self._subscribeThrottleTimer) return;
      
      self._subscribeThrottleTimer = setTimeout(function() {
        self._subscribeThrottleTimer = null;
        
        // Get current taxonomy terms
        const currentTerms = self.getCurrentTerms();
        
        // Check if terms have changed
        const hasChanged = self._termsHaveChanged(currentTerms);
        
        if (hasChanged) {
          self.previousTerms = JSON.parse(JSON.stringify(self.currentTerms));
          self.currentTerms = JSON.parse(JSON.stringify(currentTerms));
          self.applyVisibilityRules();
        }
      }, 200);
    });
  }

  /**
   * Checks if taxonomy terms have changed from previous state.
   * 
   * @private
   * @param {Object} newTerms - New terms to compare
   * @returns {boolean} True if terms have changed
   */
  _termsHaveChanged(newTerms) {
    for (const taxonomy of this.enabledTaxonomies) {
      const oldTerms = this.currentTerms[taxonomy] || [];
      const currentTerms = newTerms[taxonomy] || [];
      
      if (oldTerms.length !== currentTerms.length) {
        return true;
      }
      
      for (const termId of currentTerms) {
        if (!oldTerms.includes(termId)) {
          return true;
        }
      }
    }
    
    return false;
  }
}

// Initialize the component when DOM is ready
(function() {
  function initTaxonomyFilter() {
    if (typeof window.ppcTaxonomyFilterGutenbergData !== 'undefined' && window.wp && window.wp.data && window.jQuery) {
      var instance = new PPChTaxonomyFilter(window.wp, window.jQuery, window.ppcTaxonomyFilterGutenbergData);
      instance.init();
    } else {
      setTimeout(initTaxonomyFilter, 500);
    }
  }

  // Try multiple initialization methods
  if (window.wp && window.wp.domReady) {
    window.wp.domReady(function() {
      // Delay slightly to ensure all editor components are loaded
      setTimeout(initTaxonomyFilter, 1000);
    });
  } else {
    // Fallback: wait for document ready
    document.addEventListener('DOMContentLoaded', function() {
      setTimeout(initTaxonomyFilter, 1500);
    });
  }
})();
