<?php

// phpcs:disable

if ( !class_exists( 'MeowKitPro_WPMC_Renewal' ) ) {

  /**
  * Tells a Pro install whose license is not active that a newer version exists, and how to get it.
  *
  * The updater already fetches the latest version without a valid license: the store answers with
  * an empty package, so WordPress shows "Automatic update is unavailable for this plugin", which
  * customers read as a broken plugin ("WordPress shows no updates"). This adds the missing why and
  * how. It also feeds NekoHeader's version line. The latest version comes from the updater's
  * update_plugins entry or from meowapps.com/versions.json, fetched at most once a week for every
  * Meow plugin on the site together, because WordPress's own check often misses our updates.
  */
  class MeowKitPro_WPMC_Renewal {
    private $licenser;
    private $basename;
    private $slug;

    // Store answers that mean "renew this license". Anything else (disabled after a refund or a
    // chargeback, revoked, a key for another product, a network failure) must not offer a renewal.
    private $renewable_issues = [ 'expired', 'inactive', 'site_inactive' ];

    public function __construct( $licenser ) {
      $this->licenser = $licenser;
      $this->basename = plugin_basename( $licenser->mainfile );
      $this->slug = dirname( $this->basename );
      add_action( 'in_plugin_update_message-' . $this->basename, [ $this, 'update_message' ], 10, 2 );
      // For the plugin's own license card: apply_filters( '<prefix>_meowapps_renewal', null ).
      add_filter( $licenser->prefix . '_meowapps_renewal', [ $this, 'get_info' ] );
      // For the header's version line: apply_filters( '<prefix>_meowapps_version_info', null ).
      add_filter( $licenser->prefix . '_meowapps_version_info', [ $this, 'get_version_info' ] );
    }

    /**
    * Installed vs latest version, with the one action that fixes it, for NekoHeader's version line.
    *
    * @return array|null [ 'current', 'latest', 'behind', 'action' => [ 'label', 'url', 'external' ] | null ]
    */
    public function get_version_info() {
      $current = (string) $this->licenser->version;
      $latest = $this->get_latest_version();
      if ( !$latest ) {
        return null;
      }
      $behind = version_compare( $current, $latest, '<' );
      $info = [ 'current' => $current, 'latest' => $latest, 'behind' => $behind, 'action' => null ];
      if ( !$behind ) {
        return $info;
      }
      $domain = $this->licenser->domain;
      if ( $this->licenser->is_registered() ) {
        // WordPress's own check is what misses our updates, so send them to a forced re-check.
        $info['action'] = [ 'label' => __( 'Update', $domain ), 'url' => admin_url( 'update-core.php?force-check=1' ), 'external' => false ];
      }
      else {
        $state = $this->get_state();
        if ( $state ) {
          $label = $state === 'renew' ? __( 'Renew', $domain ) : __( 'Get a license', $domain );
          $info['action'] = [ 'label' => $label, 'url' => $this->get_url( $state ), 'external' => true ];
        }
      }
      return $info;
    }

    /**
    * What to tell the customer, or null when there is nothing to say.
    *
    * @return array|null [ 'state' => 'renew'|'activate', 'current_version', 'new_version', 'url' ]
    */
    public function get_info() {
      $state = $this->get_state();
      if ( !$state ) {
        return null;
      }
      $new_version = $this->get_new_version();
      if ( !$new_version ) {
        return null;
      }
      return [
        'state' => $state,
        'current_version' => $this->licenser->version,
        'new_version' => $new_version,
        'url' => $this->get_url( $state ),
      ];
    }

    /**
    * Completes the line WordPress prints under the plugin when an update exists.
    *
    * @param array $plugin_data
    * @param object|array $response The update entry (the multisite path passes plugin data).
    */
    public function update_message( $plugin_data, $response ) {
      if ( is_object( $response ) && !empty( $response->package ) ) {
        return;
      }
      $info = $this->get_info();
      if ( !$info ) {
        return;
      }
      $domain = $this->licenser->domain;
      if ( $info['state'] === 'renew' ) {
        /* translators: %s: the new version number. */
        $text = sprintf( __( 'Your license is not active. Renew it to get version %s.', $domain ), $info['new_version'] );
        $link = __( 'Renew your license', $domain );
      }
      else {
        /* translators: %s: the new version number. */
        $text = sprintf( __( 'Activate your license to get version %s.', $domain ), $info['new_version'] );
        $link = __( 'Get a license', $domain );
      }
      printf(
        ' <strong>%s</strong> <a href="%s" target="_blank" rel="noopener">%s</a>',
        esc_html( $text ),
        esc_url( $info['url'] ),
        esc_html( $link )
      );
    }

    /**
    * 'renew' for a license that lapsed, 'activate' when no key was ever entered, null otherwise.
    *
    * @return string|null
    */
    private function get_state() {
      // The licenser already ran is_registered() in its constructor, so this reads the cached
      // answer and never triggers a license request.
      if ( $this->licenser->is_registered() ) {
        return null;
      }
      $license = $this->licenser->license;
      if ( empty( $license ) || empty( $license['key'] ) ) {
        return 'activate';
      }
      $issue = isset( $license['issue'] ) ? $license['issue'] : null;
      // No issue but not registered: the expiry date passed and the grace window closed.
      if ( empty( $issue ) || in_array( $issue, $this->renewable_issues, true ) ) {
        return 'renew';
      }
      return null;
    }

    /**
    * The newer version, if any: the highest of WordPress's update entry and versions.json.
    *
    * @return string|null
    */
    private function get_new_version() {
      $latest = $this->get_latest_version();
      if ( !$latest || version_compare( (string) $this->licenser->version, $latest, '>=' ) ) {
        return null;
      }
      return $latest;
    }

    /**
    * The highest version known: WordPress's update_plugins entry, or meowapps.com/versions.json.
    * versions.json covers the cases where WordPress's own check comes back blind.
    *
    * @return string|null
    */
    private function get_latest_version() {
      $versions = [];
      $updates = get_site_transient( 'update_plugins' );
      if ( is_object( $updates ) && !empty( $updates->response[ $this->basename ] ) ) {
        $update = $updates->response[ $this->basename ];
        if ( is_object( $update ) && !empty( $update->new_version ) ) {
          $versions[] = (string) $update->new_version;
        }
      }
      $store = $this->get_store_versions();
      if ( !empty( $store[ $this->slug ]['version'] ) ) {
        $versions[] = (string) $store[ $this->slug ]['version'];
      }
      if ( empty( $versions ) ) {
        return null;
      }
      usort( $versions, 'version_compare' );
      return end( $versions );
    }

    /**
    * meowapps.com/versions.json, fetched at most once a week and shared by every Meow plugin on the
    * site (one site transient, not one per plugin). Fails silent: a failure is remembered for 12 hours
    * so a server that cannot get out does not retry on every admin page.
    *
    * @return array Keyed by plugin folder: [ 'id', 'name', 'version', 'released' ].
    */
    private function get_store_versions() {
      static $memo = null;
      if ( $memo !== null ) {
        return $memo;
      }
      $key = 'meowapps_versions_v1';
      $cached = get_site_transient( $key );
      if ( is_array( $cached ) ) {
        return $memo = $cached;
      }
      // The CDN caches .json for a year: the date parameter makes each day's first request fresh.
      $url = 'https://meowapps.com/versions.json?d=' . gmdate( 'Ymd' );
      $response = wp_remote_get( $url, [ 'timeout' => 5, 'user-agent' => 'MeowApps' ] );
      $body = !is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200
        ? json_decode( wp_remote_retrieve_body( $response ), true ) : null;
      if ( is_array( $body ) && isset( $body['plugins'] ) && is_array( $body['plugins'] ) ) {
        $memo = $body['plugins'];
        set_site_transient( $key, $memo, 7 * DAY_IN_SECONDS );
      }
      else {
        $memo = [];
        set_site_transient( $key, $memo, 12 * HOUR_IN_SECONDS );
      }
      return $memo;
    }

    /**
    * Renewal checkout for a known key (EDD puts the right tier in the cart), product page otherwise.
    *
    * @param string $state
    * @return string
    */
    private function get_url( $state ) {
      $utm = [ 'utm_source' => $this->slug, 'utm_medium' => 'update-renewal' ];
      $license = $this->licenser->license;
      $key = is_array( $license ) && !empty( $license['key'] ) ? $license['key'] : '';
      if ( $state === 'renew' && $key && $this->licenser->item_id ) {
        return add_query_arg(
          array_merge( [ 'edd_license_key' => $key, 'download_id' => $this->licenser->item_id ], $utm ),
          'https://meowapps.com/checkout/'
        );
      }
      $page = preg_replace( '/-pro$/', '', $this->slug );
      return add_query_arg( $utm, 'https://meowapps.com/' . $page . '/' );
    }
  }
}
