<?php

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

// Check Wallet Field
// ======================================================

/**
 * Local-only address format check for BTC and ETH.
 *
 * When no license key is set the plugin can't reach the Cryptoniq API
 * server, so all it can do is verify the address LOOKS valid by its
 * format. With a license, the API performs the real check (subscribes
 * the address with Tatum). This local regex still catches obvious typos
 * and random strings that merchants would otherwise commit to the DB.
 */
function cryptoniq_is_valid_address( $coin, $address ) {
	$address = trim( (string) $address );
	if ( $address === '' ) return false;

	switch ( strtoupper( $coin ) ) {
		case 'BTC':
			// P2PKH (starts 1) or P2SH (starts 3) — Base58, 25-34 chars after first
			if ( preg_match( '/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/', $address ) ) return true;
			// SegWit Bech32 (bc1q P2WPKH = 42 total / bc1q P2WSH = 62 total / bc1p Taproot = 62 total)
			// → 39 to 59 chars after the "bc1" prefix
			if ( preg_match( '/^bc1[ac-hj-np-z02-9]{39,59}$/', $address ) ) return true;
			return false;

		case 'ETH':
			// 0x prefix + 40 hex chars (no checksum enforcement, EIP-55 is
			// just for typo detection; both lowercased and mixed-case are valid)
			return (bool) preg_match( '/^0x[a-fA-F0-9]{40}$/', $address );

		default:
			// Other coins are locked when no license is active, so this
			// path shouldn't trigger under normal flow.
			return true;
	}
}

function cryptoniq_subscribe_wallet( $field, $value, $existing_value ) {
	// Field-level validator (requires 'validate_full' => true on the field).
	// $value is the FULL submitted address array for this coin; we send it
	// to /subscriptions/add as one request. The server diffs against its
	// stored state (add new, remove dropped) and validates each address
	// via Tatum. On any rejection we revert to the previous saved value
	// so the DB never contains an unverified address.

	$value = is_array( $value ) ? array_values( array_filter(
		array_map( 'trim', $value ),
		function ( $v ) { return is_string( $v ) && $v !== ''; }
	) ) : array();

	$existing = is_array( $existing_value ) ? array_values( array_filter(
		array_map( 'trim', $existing_value ),
		function ( $v ) { return is_string( $v ) && $v !== ''; }
	) ) : array();

	// No license stored — no API available, but we can still catch obvious
	// junk with a local regex check on BTC and ETH formats. Invalid rows
	// are dropped; valid ones are kept; the merchant sees an error listing
	// which entries were rejected.
	$license_key = cryptoniq_get_option( 'license_key' );
	if ( empty( $license_key ) ) {
		$coin    = strtoupper( str_replace( 'wallets_', '', $field['id'] ) );
		$kept    = array();
		$invalid = array();

		foreach ( $value as $addr ) {
			if ( cryptoniq_is_valid_address( $coin, $addr ) ) {
				$kept[] = $addr;
			} else {
				$invalid[] = $addr;
			}
		}

		if ( ! empty( $invalid ) ) {
			$field['msg'] = sprintf(
				/* translators: %1$s: coin label, %2$s: comma-separated invalid addresses */
				esc_html__( 'Invalid %1$s address format — dropped: %2$s', 'cryptoniq-pro' ),
				$coin,
				implode( ', ', $invalid )
			);
			return array( 'value' => $kept, 'error' => $field );
		}

		return array( 'value' => $kept, 'error' => false );
	}

	// IMPORTANT: we don't short-circuit on "both empty" here. The API runs an
	// orphan-cleanup pass on every successful /subscriptions/add call, and we
	// rely on that to catch ghost Tatum subscriptions left over from older
	// (buggy) runs. So even saving an empty wallet field is worth the round
	// trip — it lets the server tidy up Tatum if there's anything stale.
	//
	// Send the FULL current list every save. The API does its own diff and
	// only hits Tatum for actual changes (so unchanged saves are fast no-ops
	// on the Tatum side). We also don't short-circuit on "no change" because
	// pre-existing invalid addresses need a chance to be caught on every save.
	$coin     = strtoupper( str_replace( 'wallets_', '', $field['id'] ) );
	$response = wp_remote_post( CRYPTONIQ_API_URL . 'subscriptions/add', array(
		'headers' => array( 'Content-Type' => 'application/json' ),
		'body'    => wp_json_encode( array(
			'licenseKey' => $license_key,
			'coin'       => $coin,
			'address'    => $value,
		) ),
		'timeout' => 30,
	) );

	if ( is_wp_error( $response ) ) {
		// Network problem — keep what was there before, don't change anything.
		$field['msg'] = esc_html__( 'Network error — please try again.', 'cryptoniq-pro' );
		return array( 'value' => $existing, 'error' => $field );
	}

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

	if ( $code >= 400 ) {
		// New API contract: if the server can identify which addresses
		// were rejected, drop just those and keep the rest. The API is now
		// atomic, so on a 400 NONE of the requested adds are committed —
		// we're free to retry with a cleaned-up list on the next save.
		$invalid = ( is_array( $body ) && ! empty( $body['invalidAddresses'] ) && is_array( $body['invalidAddresses'] ) )
			? array_map( 'strval', $body['invalidAddresses'] )
			: array();

		if ( ! empty( $invalid ) ) {
			$invalid_lc = array_map( 'strtolower', $invalid );
			$kept       = array_values( array_filter( $value, function ( $a ) use ( $invalid_lc ) {
				return ! in_array( strtolower( trim( (string) $a ) ), $invalid_lc, true );
			} ) );

			$field['msg'] = sprintf(
				/* translators: %s: comma-separated invalid addresses */
				esc_html__( "Can't use address: %s", 'cryptoniq-pro' ),
				implode( ', ', $invalid )
			);
			return array( 'value' => $kept, 'error' => $field );
		}

		// Older API or generic error — fall back to the previous logic:
		//   value === existing → field is stuck with bad data, clear it
		//   value !== existing → revert so the user's prior good data isn't lost
		$api_msg = is_array( $body ) && ! empty( $body['error'] )
			? (string) $body['error']
			: 'Validation failed';
		$field['msg'] = sprintf(
			/* translators: %s: API error message */
			esc_html__( 'Invalid address rejected by the server: %s', 'cryptoniq-pro' ),
			$api_msg
		);

		$a = $value;    sort( $a );
		$b = $existing; sort( $b );
		$same = ( $a === $b );

		return array(
			'value' => $same ? array() : $existing,
			'error' => $field,
		);
	}

	// API accepted everything — save the new list.
	return array( 'value' => $value, 'error' => false );
}



// Add License Validation Callback
// ======================================================

function cryptoniq_license_validate( $field, $value, $existing_value ) {
    // Empty submission is allowed — user is clearing the key (downgrading to free).
    if ( $value === '' ) {
        return array( 'value' => '', 'error' => false );
    }

    $api_url      = CRYPTONIQ_API_URL . 'license/check';
    $callback_url = site_url( '/wp-json/cryptoniq/v1/webhook-callback' );

    $response = wp_remote_post( $api_url, array(
        'headers' => array( 'Content-Type' => 'application/json' ),
        'body'    => wp_json_encode( array(
            'licenseKey'      => $value,
            'wp_callback_url' => $callback_url,
        ) ),
        'timeout' => 15,
    ) );

    // Two rejection modes:
    //   "invalid"  — API confirmed the key is bad. ALWAYS clear it, even if
    //                 it matches the existing saved value (catches keys that
    //                 were injected into the DB by bypassing the panel, or
    //                 keys whose validity changed since they were saved).
    //   "unknown"  — couldn't reach the API or got an unexpected response.
    //                 Keep the existing value so paying users aren't
    //                 dropped to an unlicensed state during a brief outage.
    $reject_invalid = function ( $msg ) use ( $field ) {
        $field['msg'] = $msg;
        return array( 'value' => '', 'error' => $field );
    };
    $reject_unknown = function ( $msg ) use ( $existing_value, $field ) {
        $field['msg'] = $msg;
        return array(
            'value' => $existing_value !== null ? $existing_value : '',
            'error' => $field,
        );
    };

    if ( is_wp_error( $response ) ) {
        return $reject_unknown( 'Network error. Please try again.' );
    }

    $code = wp_remote_retrieve_response_code( $response );
    $data = json_decode( wp_remote_retrieve_body( $response ), true );

    if ( ! is_array( $data ) ) {
        return $reject_unknown( 'Unexpected server response.' );
    }

    if ( $code === 403 && ! empty( $data['error'] ) && $data['error'] === 'invalid_license_key' ) {
        return $reject_invalid( 'Invalid license key.' );
    }

    if ( $code >= 400 ) {
        return $reject_unknown( 'License could not be verified. Try again.' );
    }

    // Valid key accepted. If the license is changing (new key, or coming
    // back after a period without one), force a server-side reconciliation
    // of every wallet list. Catches the case where the merchant removed an
    // address while no license was active — the API + Tatum still hold it
    // because no /subscriptions/add call ever fired for that edit.
    if ( $value !== $existing_value ) {
        cryptoniq_sync_wallets_to_api( $value );
    }

    return array( 'value' => $value, 'error' => false );
}



// Sync local wallet state up to the API
// ======================================================

/**
 * Push every saved wallet list to the API so it can reconcile its own state
 * (and Tatum) against what the merchant has saved locally.
 *
 * The API's /subscriptions/add endpoint diffs the submitted address array
 * against what it has stored for the shop+coin, then adds/removes Tatum
 * subscriptions accordingly. By re-pushing every list right after the
 * license is (re-)activated, any orphan subscriptions left over from edits
 * made without a license get cleaned up automatically — the merchant
 * doesn't have to manually re-edit each field.
 *
 * Best-effort: network errors are swallowed (the merchant shouldn't be
 * blocked from activating their license just because one coin's sync hit
 * a timeout — the next manual save on that field will catch up).
 */
function cryptoniq_sync_wallets_to_api( $license_key ) {
    if ( empty( $license_key ) ) return;
    if ( ! function_exists( 'cryptoniq_get_coins_list' ) ) return;

    $coins = array_keys( cryptoniq_get_coins_list() );

    foreach ( $coins as $coin ) {
        $raw = (array) cryptoniq_get_option( 'wallets_' . strtolower( $coin ) );

        // Normalize: trim, drop empties.
        $addresses = array_values( array_filter(
            array_map(
                function ( $a ) { return is_string( $a ) ? trim( $a ) : ''; },
                $raw
            ),
            function ( $a ) { return $a !== ''; }
        ) );

        // Send even an empty list — that's exactly how we tell the API to
        // drop any stored entries (and the matching Tatum subs) for this coin.
        wp_remote_post( CRYPTONIQ_API_URL . 'subscriptions/add', array(
            'headers' => array( 'Content-Type' => 'application/json' ),
            'body'    => wp_json_encode( array(
                'licenseKey' => $license_key,
                'coin'       => strtoupper( $coin ),
                'address'    => $addresses,
            ) ),
            'timeout' => 15,
            // Don't block the license save if a single coin's sync hangs.
            'blocking' => true,
        ) );
    }
}