<?php

namespace WeDevs\AppBridge\Core\License;

use WeDevs\AppBridge\Core\Contracts\LicenseBackend;

/**
 * EDD Software Licensing backed license transport.
 *
 * Talks directly to a self-hosted EDD Software Licensing store (e.g.
 * wperp.com) via its remote API, instead of routing through Appsero.
 * Used by products that are sold and licensed from our own EDD store.
 *
 * Remote API (POST to the store base URL):
 *   edd_action = activate_license | deactivate_license | check_license
 *   license    = <license key>
 *   item_id    = <download ID>
 *   url        = <activating site identity>
 *
 * `item_id` validation means a bundle parent key is rejected with
 * `invalid_item_id`; the child key (e.g. the bundled "ERP App" license)
 * is what activates here.
 */
class EddSlBackend implements LicenseBackend {

	/** @var string */
	private $store_url;

	/**
	 * Candidate EDD download IDs this license may belong to.
	 *
	 * A product with multiple billing plans (e.g. monthly vs. yearly) is a
	 * separate EDD download per plan, so a single key activates against one
	 * of several IDs. Activation tries each until one validates, so the
	 * correct plan is resolved server-side from the key alone — the client
	 * never sends an item ID.
	 *
	 * @var int[]
	 */
	private $item_ids;

	/** @var string */
	private $option_key;

	/** @var string */
	private $item_name;

	/**
	 * @param string    $store_url  EDD store base URL (no trailing query).
	 * @param int|int[] $item_id    EDD download ID(s) for the licensed product.
	 *                              Pass a list when the product has multiple
	 *                              plan-specific downloads (monthly/yearly).
	 * @param string    $option_key Option key used to persist license data.
	 * @param string    $item_name  Human-readable product name (for messages).
	 */
	public function __construct( string $store_url, $item_id, string $option_key, string $item_name = '' ) {
		$this->store_url  = untrailingslashit( $store_url );
		$this->item_ids   = array_values( array_filter( array_map( 'intval', (array) $item_id ) ) );
		$this->option_key = $option_key;
		$this->item_name  = $item_name;
	}

	/**
	 * @inheritDoc
	 */
	public function activate( string $license_key ): array {
		// Resolve the plan from the key alone: try each candidate download ID
		// until one validates. A key is only valid for its own plan's ID, so
		// at most one succeeds; the rest fail with `invalid_item_id`, which we
		// skip in favour of any real key-level error (expired/revoked/etc.).
		$mismatch_codes = [ 'invalid_item_id', 'item_name_mismatch', 'license_not_activable' ];
		$last_error     = '';
		$last_mismatch  = '';

		foreach ( $this->item_ids as $item_id ) {
			$response = $this->remote_request( 'activate_license', $license_key, $item_id );

			if ( is_wp_error( $response ) ) {
				return [
					'success' => false,
					'message' => $response->get_error_message(),
				];
			}

			// EDD SL reports the canonical state in the `license` field.
			if ( ! empty( $response['success'] ) && 'valid' === ( $response['license'] ?? '' ) ) {
				update_option( $this->option_key, $this->normalise( $license_key, $response, $item_id ), false );

				return [
					'success' => true,
					'message' => __( 'License activated successfully.', 'app-bridge' ),
				];
			}

			$error = $response['error'] ?? ( $response['license'] ?? '' );

			// A wrong-product code just means "not this plan" — keep trying the
			// other IDs. Remember a real key-level error to surface first; keep
			// the mismatch code as a fallback so a key that matches no plan
			// (e.g. a bundle parent key) still gets the targeted guidance
			// instead of a generic "unknown error".
			if ( in_array( $error, $mismatch_codes, true ) ) {
				$last_mismatch = $error;
			} else {
				$last_error = $error;
			}
		}

		return [
			'success' => false,
			'message' => $this->error_message( '' !== $last_error ? $last_error : $last_mismatch ),
		];
	}

	/**
	 * @inheritDoc
	 */
	public function deactivate( string $license_key ): array {
		// Deactivate against the ID the key was activated with (persisted at
		// activation). Fall back to the first candidate if none was stored.
		$stored  = $this->get_license();
		$item_id = ! empty( $stored['item_id'] ) ? (int) $stored['item_id'] : ( $this->item_ids[0] ?? 0 );

		$response = $this->remote_request( 'deactivate_license', $license_key, $item_id );

		// Always clear stored data on deactivation attempt.
		update_option(
			$this->option_key,
			[
				'key'    => '',
				'status' => 'deactivate',
			],
			false
		);

		if ( is_wp_error( $response ) ) {
			return [
				'success' => false,
				'message' => $response->get_error_message(),
			];
		}

		if ( empty( $response['success'] ) ) {
			return [
				'success' => false,
				'message' => $this->error_message( $response['license'] ?? '' ),
			];
		}

		return [
			'success' => true,
			'message' => __( 'License deactivated successfully.', 'app-bridge' ),
		];
	}

	/**
	 * @inheritDoc
	 */
	public function is_valid(): bool {
		$stored = $this->get_license();
		$key    = $stored['key'] ?? '';

		if ( empty( $key ) ) {
			return false;
		}

		// Optimistic local check first: if we hold an activated, unexpired
		// record, trust it. The manager layer handles cache invalidation on
		// status-change actions, so we avoid a remote round-trip per request.
		if ( ( $stored['status'] ?? '' ) === 'activate' ) {
			$expiry_days = $stored['expiry_days'] ?? null;
			if ( false === $expiry_days || null === $expiry_days || (int) $expiry_days > 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @inheritDoc
	 */
	public function get_license(): array {
		$stored = get_option( $this->option_key, [] );

		return is_array( $stored ) ? $stored : [];
	}

	// ── Internals ────────────────────────────────────────────────────────────

	/**
	 * Perform a remote EDD SL request.
	 *
	 * @return array|\WP_Error Decoded JSON body, or WP_Error on transport failure.
	 */
	private function remote_request( string $edd_action, string $license_key, int $item_id ) {
		$response = wp_remote_post(
			$this->store_url,
			[
				'timeout'   => 15,
				'sslverify' => true,
				'body'      => [
					'edd_action' => $edd_action,
					'license'    => $license_key,
					'item_id'    => $item_id,
					'item_name'  => rawurlencode( $this->item_name ),
					'url'        => home_url(),
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== (int) $code ) {
			return new \WP_Error(
				'edd_sl_http_error',
				sprintf(
					/* translators: %d: HTTP status code */
					__( 'License server returned an unexpected response (HTTP %d). Please try again.', 'app-bridge' ),
					(int) $code
				)
			);
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! is_array( $body ) ) {
			return new \WP_Error(
				'edd_sl_bad_response',
				__( 'Could not read the response from the license server.', 'app-bridge' )
			);
		}

		return $body;
	}

	/**
	 * Normalise an EDD SL activation/check response into the shared
	 * stored-license shape.
	 *
	 * @param array $response  Decoded EDD SL response.
	 * @param int   $item_id   Download ID the key activated against (for later deactivation).
	 * @return array
	 */
	private function normalise( string $license_key, array $response, int $item_id ): array {
		$activation_limit = isset( $response['license_limit'] ) ? (int) $response['license_limit'] : null;
		$site_count       = isset( $response['site_count'] ) ? (int) $response['site_count'] : null;

		$remaining = null;
		if ( null !== $activation_limit && $activation_limit > 0 && null !== $site_count ) {
			$remaining = max( 0, $activation_limit - $site_count );
		}

		return [
			'key'              => $license_key,
			'status'           => 'activate',
			'item_id'          => isset( $response['item_id'] ) ? (int) $response['item_id'] : $item_id,
			'remaining'        => $remaining,
			'activation_limit' => $activation_limit,
			'expiry_days'      => $this->expiry_days( $response['expires'] ?? null ),
			'title'            => $response['item_name'] ?? $this->item_name,
			'source_id'        => isset( $response['payment_id'] ) ? (string) $response['payment_id'] : '',
			'recurring'        => false, // EDD SL renewal handled store-side; not asserted here.
		];
	}

	/**
	 * Convert an EDD SL `expires` value into days-until-expiry.
	 *
	 * @param mixed $expires "Y-m-d H:i:s", "lifetime", or null.
	 * @return int|false|null Days remaining, false for perpetual, null if unknown.
	 */
	private function expiry_days( $expires ) {
		if ( empty( $expires ) ) {
			return null;
		}

		if ( 'lifetime' === $expires ) {
			return false; // perpetual — matches Appsero convention.
		}

		$expires_ts = is_numeric( $expires ) ? (int) $expires : strtotime( (string) $expires );

		if ( ! $expires_ts ) {
			return null;
		}

		$diff = $expires_ts - time();

		return (int) ceil( $diff / DAY_IN_SECONDS );
	}

	/**
	 * Map an EDD SL error/status code to a human-readable message.
	 */
	private function error_message( string $code ): string {
		switch ( $code ) {
			case 'invalid_item_id':
			case 'license_not_activable':
				return __( 'This key is not valid for the ERP App. If you purchased a bundle (e.g. Scale), use the ERP App license key, not the bundle key.', 'app-bridge' );
			case 'expired':
				return __( 'This license has expired. Please renew to continue.', 'app-bridge' );
			case 'no_activations_left':
				return __( 'No activations remaining for this license.', 'app-bridge' );
			case 'missing':
			case 'invalid':
				return __( 'Invalid license key.', 'app-bridge' );
			case 'disabled':
			case 'revoked':
				return __( 'This license has been disabled.', 'app-bridge' );
			case 'site_inactive':
				return __( 'License is not active for this site.', 'app-bridge' );
			case '':
				return __( 'Unknown error occurred.', 'app-bridge' );
			default:
				return $code;
		}
	}
}
