<?php
/**
 * Optional Addon Loader.
 *
 * Safely loads and boots optional built-in addons without allowing a missing
 * or broken addon file to take down the entire suite.
 *
 * @package Polanger_Dashboard_Tools
 */

if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

class PDT_Optional_Addon_Loader {

  /**
   * Failure option key.
   */
  const FAILURE_OPTION = 'pdt_optional_addon_failures';

  /**
   * Site active addons option key.
   */
  const ACTIVE_OPTION = 'pdt_active_addons';

  /**
   * Network active addons option key.
   */
  const NETWORK_ACTIVE_OPTION = 'pdt_network_active_addons';

  /**
   * Optional addon definitions.
   *
   * @var array<string,array<string,mixed>>
   */
  private $definitions = array();

  /**
   * Constructor.
   *
   * @param array<string,array<string,mixed>> $definitions Optional addon definitions.
   */
  public function __construct( $definitions ) {
    $this->definitions = $this->normalize_definitions( $definitions );

    add_action( 'admin_notices', array( $this, 'render_failure_notices' ) );
    add_filter( 'pdt_addon_availability', array( $this, 'filter_faulted_addon_availability' ), 20, 3 );

    $this->probe_optional_addons();
  }

  /**
   * Safely boot an optional addon instance.
   *
   * @param string $slug Optional addon slug.
   * @return object|null
   */
  public function boot_addon( $slug ) {
    $definition = $this->get_definition( $slug );
    if ( empty( $definition ) ) {
      return null;
    }

    $slug = $definition['slug'];

    if ( ! function_exists( 'pdt_is_addon_active' ) || ! pdt_is_addon_active( $slug ) ) {
      return null;
    }

    if ( ! $this->load_addon_file( $slug ) ) {
      return null;
    }

    $class_name = $definition['class'];
    if ( empty( $class_name ) || ! class_exists( $class_name, false ) ) {
      $this->record_failure( $slug, 'class_missing', 'boot', $definition['file'], true );
      return null;
    }

    try {
      $instance = new $class_name();
    } catch ( Throwable $exception ) {
      $this->record_failure( $slug, 'boot_failed', 'boot', $definition['file'], true, $exception->getMessage() );
      return null;
    }

    $this->clear_failure( $slug );

    return $instance;
  }

  /**
   * Render admin notices for auto-disabled optional addons.
   *
   * @return void
   */
  public function render_failure_notices() {
    if ( ! current_user_can( 'manage_options' ) ) {
      return;
    }

    $failures = $this->get_failures();
    if ( empty( $failures ) ) {
      return;
    }

    foreach ( $failures as $failure ) {
      $title = isset( $failure['title'] ) ? $failure['title'] : $failure['slug'];
      ?>
      <div class="notice notice-warning">
        <p>
          <strong><?php echo esc_html( $title ); ?>:</strong>
          <?php echo esc_html( $this->get_failure_message( $failure ) ); ?>
        </p>
      </div>
      <?php
    }
  }

  /**
   * Mark faulted optional addons as temporarily unavailable on the Addons page.
   *
   * @param array  $availability Existing availability data.
   * @param string $addon_key    Addon slug.
   * @param array  $addon        Addon config.
   * @return array
   */
  public function filter_faulted_addon_availability( $availability, $addon_key, $addon ) {
    unset( $addon );

    $definition = $this->get_definition( $addon_key );
    if ( empty( $definition ) ) {
      return $availability;
    }

    $failure = $this->get_failure( $addon_key );
    if ( empty( $failure ) ) {
      return $availability;
    }

    return array_merge(
      is_array( $availability ) ? $availability : array(),
      array(
        'available'   => false,
        'reason'      => $this->get_failure_message( $failure ),
        'status_text' => __( 'Temporarily disabled', 'polanger-admin-menu-manager' ),
        'button_text' => __( 'Fix addon first', 'polanger-admin-menu-manager' ),
        'fact_text'   => __( 'Auto-disabled after a load failure', 'polanger-admin-menu-manager' ),
      )
    );
  }

  /**
   * Probe optional addons that must be loaded for this request.
   *
   * @return void
   */
  private function probe_optional_addons() {
    foreach ( array_keys( $this->definitions ) as $slug ) {
      if ( ! $this->should_probe_addon( $slug ) ) {
        continue;
      }

      $this->load_addon_file( $slug );
    }
  }

  /**
   * Whether the addon file should be probed on this request.
   *
   * @param string $slug Optional addon slug.
   * @return bool
   */
  private function should_probe_addon( $slug ) {
    $definition = $this->get_definition( $slug );
    if ( empty( $definition ) ) {
      return false;
    }

    if ( ! empty( $definition['load_always'] ) ) {
      return true;
    }

    if ( $this->is_addon_marked_active( $slug ) ) {
      return true;
    }

    return $this->has_failure( $slug );
  }

  /**
   * Safely include an addon bootstrap file.
   *
   * @param string $slug Optional addon slug.
   * @return bool
   */
  private function load_addon_file( $slug ) {
    $definition = $this->get_definition( $slug );
    if ( empty( $definition ) ) {
      return false;
    }

    if ( ! empty( $definition['loaded'] ) ) {
      return true;
    }

    $file      = $definition['file'];
    $is_active = $this->is_addon_marked_active( $slug );

    if ( ! file_exists( $file ) ) {
      $this->record_failure( $slug, 'missing_file', 'load', $file, $is_active );
      return false;
    }

    if ( ! is_readable( $file ) ) {
      $this->record_failure( $slug, 'unreadable_file', 'load', $file, $is_active );
      return false;
    }

    try {
      include_once $file;
    } catch ( Throwable $exception ) {
      $this->record_failure( $slug, 'include_failed', 'load', $file, $is_active, $exception->getMessage() );
      return false;
    }

    $class_name = $definition['class'];
    if ( empty( $class_name ) || ! class_exists( $class_name, false ) ) {
      $this->record_failure( $slug, 'class_missing', 'load', $file, $is_active );
      return false;
    }

    $this->definitions[ $slug ]['loaded'] = true;
    $this->clear_failure( $slug );

    return true;
  }

  /**
   * Record an addon failure and auto-disable the addon when needed.
   *
   * @param string $slug      Optional addon slug.
   * @param string $reason    Failure reason code.
   * @param string $stage     Failure stage.
   * @param string $file      Bootstrap file path.
   * @param bool   $is_active Whether the addon was active when the failure happened.
   * @param string $message   Optional raw exception message.
   * @return void
   */
  private function record_failure( $slug, $reason, $stage, $file, $is_active, $message = '' ) {
    $definition = $this->get_definition( $slug );
    if ( empty( $definition ) ) {
      return;
    }

    if ( $is_active ) {
      $this->deactivate_addon( $slug );
    }

    $failures = $this->get_failures();
    $failures[ $slug ] = array(
      'slug'    => $slug,
      'title'   => $definition['title'],
      'reason'  => sanitize_key( $reason ),
      'stage'   => sanitize_key( $stage ),
      'file'    => $this->to_relative_path( $file ),
      'message' => $this->sanitize_failure_message( $message ),
      'time'    => time(),
    );

    update_option( self::FAILURE_OPTION, $failures, false );
  }

  /**
   * Clear a stored addon failure record.
   *
   * @param string $slug Optional addon slug.
   * @return void
   */
  private function clear_failure( $slug ) {
    $failures = $this->get_failures();
    if ( ! isset( $failures[ $slug ] ) ) {
      return;
    }

    unset( $failures[ $slug ] );
    update_option( self::FAILURE_OPTION, $failures, false );
  }

  /**
   * Return all stored failures.
   *
   * @return array<string,array<string,mixed>>
   */
  private function get_failures() {
    $failures = get_option( self::FAILURE_OPTION, array() );

    if ( ! is_array( $failures ) ) {
      return array();
    }

    $normalized = array();

    foreach ( $failures as $slug => $failure ) {
      if ( ! is_array( $failure ) ) {
        continue;
      }

      $normalized_slug = sanitize_key( $slug );
      if ( '' === $normalized_slug ) {
        continue;
      }

      $normalized[ $normalized_slug ] = array(
        'slug'    => $normalized_slug,
        'title'   => isset( $failure['title'] ) ? sanitize_text_field( $failure['title'] ) : $normalized_slug,
        'reason'  => isset( $failure['reason'] ) ? sanitize_key( $failure['reason'] ) : 'boot_failed',
        'stage'   => isset( $failure['stage'] ) ? sanitize_key( $failure['stage'] ) : 'load',
        'file'    => isset( $failure['file'] ) ? sanitize_text_field( $failure['file'] ) : '',
        'message' => isset( $failure['message'] ) ? sanitize_text_field( $failure['message'] ) : '',
        'time'    => isset( $failure['time'] ) ? absint( $failure['time'] ) : 0,
      );
    }

    return $normalized;
  }

  /**
   * Return one failure entry.
   *
   * @param string $slug Optional addon slug.
   * @return array<string,mixed>
   */
  private function get_failure( $slug ) {
    $failures = $this->get_failures();
    return isset( $failures[ $slug ] ) ? $failures[ $slug ] : array();
  }

  /**
   * Whether a failure entry exists for the addon.
   *
   * @param string $slug Optional addon slug.
   * @return bool
   */
  private function has_failure( $slug ) {
    $failure = $this->get_failure( $slug );
    return ! empty( $failure );
  }

  /**
   * Auto-disable an optional addon by removing it from active option lists.
   *
   * @param string $slug Optional addon slug.
   * @return void
   */
  private function deactivate_addon( $slug ) {
    $site_active = get_option( self::ACTIVE_OPTION, array() );
    if ( is_array( $site_active ) && in_array( $slug, $site_active, true ) ) {
      $site_active = array_values( array_diff( array_map( 'sanitize_key', $site_active ), array( $slug ) ) );
      update_option( self::ACTIVE_OPTION, $site_active );
    }

    if ( is_multisite() ) {
      $network_active = get_site_option( self::NETWORK_ACTIVE_OPTION, array() );
      if ( is_array( $network_active ) && in_array( $slug, $network_active, true ) ) {
        $network_active = array_values( array_diff( array_map( 'sanitize_key', $network_active ), array( $slug ) ) );
        update_site_option( self::NETWORK_ACTIVE_OPTION, $network_active );
      }
    }
  }

  /**
   * Whether the addon is currently marked active in site or network options.
   *
   * @param string $slug Optional addon slug.
   * @return bool
   */
  private function is_addon_marked_active( $slug ) {
    $site_active = get_option( self::ACTIVE_OPTION, array() );
    if ( is_array( $site_active ) && in_array( $slug, array_map( 'sanitize_key', $site_active ), true ) ) {
      return true;
    }

    if ( is_multisite() ) {
      $network_active = get_site_option( self::NETWORK_ACTIVE_OPTION, array() );
      if ( is_array( $network_active ) && in_array( $slug, array_map( 'sanitize_key', $network_active ), true ) ) {
        return true;
      }
    }

    return false;
  }

  /**
   * Normalize raw addon definitions.
   *
   * @param array<string,array<string,mixed>> $definitions Raw definitions.
   * @return array<string,array<string,mixed>>
   */
  private function normalize_definitions( $definitions ) {
    if ( ! is_array( $definitions ) ) {
      return array();
    }

    $normalized = array();

    foreach ( $definitions as $slug => $definition ) {
      if ( ! is_array( $definition ) ) {
        continue;
      }

      $normalized_slug = sanitize_key( $slug );
      if ( '' === $normalized_slug ) {
        continue;
      }

      $file = isset( $definition['file'] ) ? (string) $definition['file'] : '';
      if ( '' === $file ) {
        continue;
      }

      $normalized[ $normalized_slug ] = array(
        'slug'        => $normalized_slug,
        'title'       => isset( $definition['title'] ) ? sanitize_text_field( $definition['title'] ) : $normalized_slug,
        'file'        => $file,
        'class'       => isset( $definition['class'] ) ? sanitize_text_field( $definition['class'] ) : '',
        'load_always' => ! empty( $definition['load_always'] ),
        'loaded'      => false,
      );
    }

    return $normalized;
  }

  /**
   * Get one addon definition.
   *
   * @param string $slug Optional addon slug.
   * @return array<string,mixed>
   */
  private function get_definition( $slug ) {
    $slug = sanitize_key( $slug );
    return isset( $this->definitions[ $slug ] ) ? $this->definitions[ $slug ] : array();
  }

  /**
   * Convert an absolute addon path into a plugin-relative path.
   *
   * @param string $file Absolute file path.
   * @return string
   */
  private function to_relative_path( $file ) {
    if ( '' === $file ) {
      return '';
    }

    $normalized_file = wp_normalize_path( $file );
    $normalized_root = wp_normalize_path( PDT_PLUGIN_DIR );

    if ( 0 === strpos( $normalized_file, $normalized_root ) ) {
      return ltrim( substr( $normalized_file, strlen( $normalized_root ) ), '/' );
    }

    return basename( $normalized_file );
  }

  /**
   * Sanitize a raw exception/debug message for storage.
   *
   * @param string $message Raw message.
   * @return string
   */
  private function sanitize_failure_message( $message ) {
    if ( '' === $message ) {
      return '';
    }

    $clean = wp_strip_all_tags( (string) $message );
    $clean = trim( preg_replace( '/\s+/', ' ', $clean ) );

    return substr( $clean, 0, 220 );
  }

  /**
   * Build a human-readable failure message.
   *
   * @param array<string,mixed> $failure Failure record.
   * @return string
   */
  private function get_failure_message( $failure ) {
    $file = ! empty( $failure['file'] ) ? $failure['file'] : __( 'the addon bootstrap file', 'polanger-admin-menu-manager' );

    switch ( $failure['reason'] ) {
      case 'missing_file':
        return sprintf(
          /* translators: %s: Missing addon bootstrap file path. */
          __( 'This addon was automatically disabled because its bootstrap file is missing: %s. Restore the addon files and then activate it again.', 'polanger-admin-menu-manager' ),
          $file
        );

      case 'unreadable_file':
        return sprintf(
          /* translators: %s: Unreadable addon bootstrap file path. */
          __( 'This addon was automatically disabled because its bootstrap file is not readable: %s. Check file permissions and try again.', 'polanger-admin-menu-manager' ),
          $file
        );

      case 'include_failed':
        return __( 'This addon was automatically disabled because its files could not be loaded safely. Fix the addon code and then activate it again.', 'polanger-admin-menu-manager' );

      case 'class_missing':
        return __( 'This addon was automatically disabled because its main runtime class could not be found after loading the addon files. Reinstall or repair the addon and try again.', 'polanger-admin-menu-manager' );

      case 'boot_failed':
      default:
        return __( 'This addon was automatically disabled because it crashed during startup. Review the addon files, fix the issue, and activate it again when ready.', 'polanger-admin-menu-manager' );
    }
  }
}
