<?php
/**
 * Immutable value object representing a configured PayPal Business account.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Encapsulates a single PayPal account configuration record.
 *
 * Properties are readonly to enforce immutability; use the static factory
 * {@see PayPal_Account::from_database_row()} to construct from a DB result.
 *
 * @since 1.0.0
 */
class PayPal_Account {

	/**
	 * Construct a PayPal_Account value object.
	 *
	 * @since 1.0.0
	 *
	 * @param int|null            $id                  Database row ID; null for unsaved records.
	 * @param string              $account_label        Human-readable label (e.g. "US Entity").
	 * @param string              $merchant_id          PayPal merchant ID (non-empty).
	 * @param string              $account_country      ISO 3166-1 alpha-2 country code (e.g. "US").
	 * @param string              $legal_entity_name    Registered legal name.
	 * @param string              $legal_entity_tax_id  EIN, BN, or equivalent (non-empty).
	 * @param string              $legal_entity_address Full mailing address.
	 * @param array<string,mixed> $credentials          Decrypted credential key-value pairs.
	 * @param string|null         $webhook_id           PayPal webhook ID, or null if not yet registered.
	 * @param bool                $is_active            Whether this account is available for routing.
	 * @param \DateTimeImmutable  $created_at           Record creation timestamp.
	 * @param \DateTimeImmutable  $updated_at           Record last-update timestamp.
	 * @param string              $mode                 PayPal environment: 'live' or 'sandbox'. Default 'live'.
	 *
	 * @throws \InvalidArgumentException  When account_country, merchant_id, legal_entity_tax_id,
	 *                                    or mode fail validation.
	 */
	public function __construct(
		public readonly int|null $id,
		public readonly string $account_label,
		public readonly string $merchant_id,
		public readonly string $account_country,
		public readonly string $legal_entity_name,
		public readonly string $legal_entity_tax_id,
		public readonly string $legal_entity_address,
		private readonly array $credentials,
		public readonly string|null $webhook_id,
		public readonly bool $is_active,
		public readonly \DateTimeImmutable $created_at,
		public readonly \DateTimeImmutable $updated_at,
		public readonly string $mode = 'live',
	) {
		if ( 1 !== preg_match( '/^[A-Z]{2}$/', $this->account_country ) ) {
			throw new \InvalidArgumentException(
				'account_country must be an ISO 3166-1 alpha-2 code (two uppercase letters).'
			);
		}

		if ( '' === $this->merchant_id ) {
			throw new \InvalidArgumentException( 'merchant_id must not be empty.' );
		}

		if ( '' === $this->legal_entity_tax_id ) {
			throw new \InvalidArgumentException( 'legal_entity_tax_id must not be empty.' );
		}

		if ( ! in_array( $this->mode, array( 'live', 'sandbox' ), true ) ) {
			throw new \InvalidArgumentException( "mode must be 'live' or 'sandbox'." );
		}
	}

	/**
	 * Construct from a raw database row and pre-decrypted credentials.
	 *
	 * The repository layer is responsible for decrypting the stored
	 * encrypted_credentials blob before calling this factory.
	 *
	 * @since 1.0.0
	 *
	 * @param  array<string,mixed> $row                   Associative row from wpdb.
	 * @param  string              $decrypted_credentials JSON string of credential key-value pairs.
	 *
	 * @return self
	 */
	public static function from_database_row( array $row, string $decrypted_credentials ): self {
		$credentials_raw = json_decode( $decrypted_credentials, true );
		$credentials     = is_array( $credentials_raw ) ? $credentials_raw : array();

		$created_at = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', (string) $row['created_at'] );
		$updated_at = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', (string) $row['updated_at'] );

		return new self(
			id: isset( $row['id'] ) ? (int) $row['id'] : null,
			account_label: (string) $row['account_label'],
			merchant_id: (string) $row['merchant_id'],
			account_country: (string) $row['account_country'],
			legal_entity_name: (string) $row['legal_entity_name'],
			legal_entity_tax_id: (string) $row['legal_entity_tax_id'],
			legal_entity_address: (string) $row['legal_entity_address'],
			credentials: $credentials,
			webhook_id: isset( $row['webhook_id'] ) ? (string) $row['webhook_id'] : null,
			is_active: (bool) $row['is_active'],
			created_at: $created_at instanceof \DateTimeImmutable ? $created_at : new \DateTimeImmutable(),
			updated_at: $updated_at instanceof \DateTimeImmutable ? $updated_at : new \DateTimeImmutable(),
			mode: isset( $row['mode'] ) && in_array( (string) $row['mode'], array( 'live', 'sandbox' ), true ) ? (string) $row['mode'] : 'live',
		);
	}

	/**
	 * Return the decrypted credential array.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string,mixed>
	 */
	public function get_credentials(): array {
		return $this->credentials;
	}

	/**
	 * Serialise to an array, excluding sensitive credential data.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string,mixed>
	 */
	public function to_array(): array {
		return array(
			'id'                   => $this->id,
			'account_label'        => $this->account_label,
			'merchant_id'          => $this->merchant_id,
			'account_country'      => $this->account_country,
			'legal_entity_name'    => $this->legal_entity_name,
			'legal_entity_tax_id'  => $this->legal_entity_tax_id,
			'legal_entity_address' => $this->legal_entity_address,
			'webhook_id'           => $this->webhook_id,
			'is_active'            => $this->is_active,
			'created_at'           => $this->created_at->format( 'Y-m-d H:i:s' ),
			'updated_at'           => $this->updated_at->format( 'Y-m-d H:i:s' ),
		);
	}

	/**
	 * Build a GiveWP MerchantDetail from this account's stored credentials.
	 *
	 * Returns null when the account is missing clientId or clientSecret —
	 * callers should fall back to GiveWP's default credentials in that case.
	 *
	 * @since 1.0.0
	 *
	 * @return \Give\PaymentGateways\PayPalCommerce\Models\MerchantDetail|null
	 */
	public function to_merchant_detail(): ?\Give\PaymentGateways\PayPalCommerce\Models\MerchantDetail {
		$creds = $this->credentials;

		if ( empty( $creds['clientId'] ) || empty( $creds['clientSecret'] ) ) {
			return null;
		}

		return \Give\PaymentGateways\PayPalCommerce\Models\MerchantDetail::fromArray(
			array(
				'merchantId'             => $this->merchant_id,
				'merchantIdInPayPal'     => $creds['merchantIdInPayPal'] ?? $this->merchant_id,
				'clientId'               => (string) $creds['clientId'],
				'clientSecret'           => (string) $creds['clientSecret'],
				'token'                  => $creds['token'] ?? array(
					'accessToken' => '',
					'expiresIn'   => 0,
					'tokenType'   => 'Bearer',
				),
				'accountIsReady'         => $creds['accountIsReady'] ?? true,
				'supportsCustomPayments' => $creds['supportsCustomPayments'] ?? true,
				'accountCountry'         => $this->account_country,
			)
		);
	}
}
