<?php
/**
 * Brevo contact attributes resource.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Api;

use RewindBrevoFormBridge\Support\Errors;

defined( 'ABSPATH' ) || exit;

/**
 * Reads (cached) and creates contact attributes, plus the pure slugify()
 * used to turn form-field labels into legal Brevo attribute names.
 *
 * @since 0.2.0
 */
class Attributes {

	/**
	 * Attribute categories Brevo accepts on the create endpoint.
	 *
	 * @var string[]
	 */
	const CATEGORIES = array( 'normal', 'transactional', 'category', 'calculated', 'global' );

	/**
	 * Value types valid for normal/transactional attributes.
	 * (Brevo's UI calls `float` "Number".)
	 *
	 * @var string[]
	 */
	const SIMPLE_TYPES = array( 'text', 'date', 'float', 'boolean', 'id' );

	/**
	 * API client.
	 *
	 * @var Client
	 */
	private Client $client;

	/**
	 * Cache.
	 *
	 * @var Cache
	 */
	private Cache $cache;

	/**
	 * Constructor.
	 *
	 * @since 0.2.0
	 * @param Client $client API client.
	 * @param Cache  $cache  Transient cache.
	 */
	public function __construct( Client $client, Cache $cache ) {
		$this->client = $client;
		$this->cache  = $cache;
	}

	/**
	 * All attributes, normalised to
	 * ['name', 'type', 'category', 'enumeration', 'multi_options'].
	 *
	 * enumeration is [ ['value' => int, 'label' => string], ... ] for
	 * category attributes; multi_options is a string[] for multiple-choice.
	 *
	 * @since 0.2.0
	 * @param bool $force Bypass the cache (explicit refresh only).
	 * @return array|\WP_Error Attribute rows, or WP_Error on API failure.
	 */
	/**
	 * Cache-only read — never calls the API. For admin page renders, which
	 * must not trigger requests on their own (guideline 7).
	 *
	 * @since 0.3.0
	 * @return array|null Null when nothing is cached.
	 */
	public function cached(): ?array {
		$cached = $this->cache->get( 'attributes' );

		return is_array( $cached ) ? $cached : null;
	}

	public function all( bool $force = false ) {
		if ( ! $force ) {
			$cached = $this->cache->get( 'attributes' );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$response = $this->client->get( '/contacts/attributes' );

		if ( ! $response->is_success() ) {
			return new \WP_Error( 'rbfb_api_error', Errors::translate( $response ) );
		}

		$body = $response->body();
		$raw  = ( isset( $body['attributes'] ) && is_array( $body['attributes'] ) ) ? $body['attributes'] : array();

		$attributes = array();
		foreach ( $raw as $row ) {
			$enumeration = array();
			if ( isset( $row['enumeration'] ) && is_array( $row['enumeration'] ) ) {
				foreach ( $row['enumeration'] as $option ) {
					$enumeration[] = array(
						'value' => isset( $option['value'] ) ? (int) $option['value'] : 0,
						'label' => isset( $option['label'] ) ? (string) $option['label'] : '',
					);
				}
			}

			$multi = array();
			if ( isset( $row['multiCategoryOptions'] ) && is_array( $row['multiCategoryOptions'] ) ) {
				foreach ( $row['multiCategoryOptions'] as $option ) {
					$multi[] = (string) $option;
				}
			}

			$attributes[] = array(
				'name'          => isset( $row['name'] ) ? (string) $row['name'] : '',
				// Calculated/global attributes come back without a `type`;
				// default to 'text' so the mapping UI always has something.
				'type'          => isset( $row['type'] ) ? (string) $row['type'] : 'text',
				'category'      => isset( $row['category'] ) ? (string) $row['category'] : 'normal',
				'enumeration'   => $enumeration,
				'multi_options' => $multi,
			);
		}

		$this->cache->set( 'attributes', $attributes );

		return $attributes;
	}

	/**
	 * Create an attribute at POST /contacts/attributes/{category}/{name}.
	 *
	 * The required body differs by category:
	 * - normal / transactional: `type` (text|date|float|boolean|id), or
	 *   type 'multiple-choice' with `multiCategoryOptions` in $extra.
	 * - category: `type` plus `enumeration` in $extra
	 *   ([ ['value' => int, 'label' => string], ... ]).
	 * - calculated / global: `value` (an expression string) in $extra.
	 *
	 * @since 0.2.0
	 * @param string $name     Attribute name — pass through slugify() first.
	 * @param string $category One of self::CATEGORIES.
	 * @param string $type     Value type; ignored for calculated/global.
	 * @param array  $extra    enumeration | multiCategoryOptions | value.
	 * @return true|\WP_Error True on 201, WP_Error otherwise.
	 */
	public function create( string $name, string $category, string $type, array $extra = array() ) {
		$name = self::slugify( $name );

		if ( '' === $name ) {
			return new \WP_Error( 'rbfb_invalid_attribute_name', __( 'The attribute name is empty after applying Brevo naming rules.', 'rewind-form-bridge-for-brevo' ) );
		}

		if ( ! in_array( $category, self::CATEGORIES, true ) ) {
			return new \WP_Error(
				'rbfb_invalid_category',
				sprintf(
					/* translators: 1: the rejected category, 2: the allowed categories. */
					__( 'Unsupported attribute category "%1$s". Allowed: %2$s.', 'rewind-form-bridge-for-brevo' ),
					$category,
					implode( ', ', self::CATEGORIES )
				)
			);
		}

		$body = $this->build_create_body( $category, $type, $extra );

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

		$response = $this->client->post( '/contacts/attributes/' . rawurlencode( $category ) . '/' . rawurlencode( $name ), $body );

		if ( ! $response->is_success() ) {
			return new \WP_Error( 'rbfb_api_error', Errors::translate( $response ) );
		}

		$this->cache->delete( 'attributes' );

		return true;
	}

	/**
	 * Validate and assemble the create-attribute request body per category.
	 *
	 * @since 0.2.0
	 * @param string $category Attribute category.
	 * @param string $type     Value type.
	 * @param array  $extra    Extra inputs (enumeration, options, value).
	 * @return array|\WP_Error Body array, or WP_Error for bad combinations.
	 */
	private function build_create_body( string $category, string $type, array $extra ) {
		if ( 'calculated' === $category || 'global' === $category ) {
			$value = isset( $extra['value'] ) ? trim( (string) $extra['value'] ) : '';

			if ( '' === $value ) {
				return new \WP_Error( 'rbfb_missing_value', __( 'Calculated and global attributes require a value expression.', 'rewind-form-bridge-for-brevo' ) );
			}

			return array( 'value' => $value );
		}

		if ( 'category' === $category ) {
			$enumeration = ( isset( $extra['enumeration'] ) && is_array( $extra['enumeration'] ) ) ? $extra['enumeration'] : array();

			if ( 0 === count( $enumeration ) ) {
				return new \WP_Error( 'rbfb_missing_enumeration', __( 'Category attributes require an enumeration of value/label pairs.', 'rewind-form-bridge-for-brevo' ) );
			}

			return array(
				'type'        => 'category',
				'enumeration' => $enumeration,
			);
		}

		// normal / transactional.
		if ( 'multiple-choice' === $type ) {
			$options = ( isset( $extra['multiCategoryOptions'] ) && is_array( $extra['multiCategoryOptions'] ) ) ? array_values( array_filter( array_map( 'strval', $extra['multiCategoryOptions'] ) ) ) : array();

			if ( 0 === count( $options ) ) {
				return new \WP_Error( 'rbfb_missing_options', __( 'Multiple-choice attributes require at least one option.', 'rewind-form-bridge-for-brevo' ) );
			}

			return array(
				'type'                 => 'multiple-choice',
				'multiCategoryOptions' => $options,
			);
		}

		if ( ! in_array( $type, self::SIMPLE_TYPES, true ) ) {
			return new \WP_Error(
				'rbfb_invalid_type',
				sprintf(
					/* translators: 1: the rejected type, 2: the allowed types. */
					__( 'Unsupported attribute type "%1$s". Allowed: %2$s or multiple-choice.', 'rewind-form-bridge-for-brevo' ),
					$type,
					implode( ', ', self::SIMPLE_TYPES )
				)
			);
		}

		return array( 'type' => $type );
	}

	/**
	 * Turn a human label into a legal Brevo attribute name.
	 *
	 * Rules: uppercase; [A-Z0-9_] only; spaces and hyphens become
	 * underscores; everything else stripped; repeated underscores collapsed;
	 * prefixed with A_ when starting with a digit; max 50 characters.
	 * Pure function — unit tested without WordPress.
	 *
	 * @since 0.2.0
	 * @param string $label Human label, e.g. "First name".
	 * @return string Legal attribute name, e.g. "FIRST_NAME". May be ''.
	 */
	public static function slugify( string $label ): string {
		$name = strtoupper( trim( $label ) );
		$name = str_replace( array( ' ', '-' ), '_', $name );
		$name = preg_replace( '/[^A-Z0-9_]/', '', $name );
		$name = preg_replace( '/_{2,}/', '_', $name );
		$name = trim( $name, '_' );

		if ( '' === $name ) {
			return '';
		}

		if ( preg_match( '/^[0-9]/', $name ) ) {
			$name = 'A_' . $name;
		}

		return substr( $name, 0, 50 );
	}
}
