<?php

namespace Automattic\WCShipping\Packages;

class Package {

	const TYPE_BOX      = 'box';
	const TYPE_ENVELOPE = 'envelope';

	private string $id;
	private string $name;
	private string $dimensions;
	private float $length;
	private float $width;
	private float $height;
	private float $box_weight;
	private float $max_weight;
	private string $type;
	private bool $is_user_defined = true;

	/**
	 * Creates an instance of the class from package data provided as an array.
	 *
	 * @throws PackageValidationException
	 */
	public static function from_array( array $data ): Package {
		$data = self::map_wcst_keys_to_wcshipping_keys( $data );
		self::validate_from_array_input( $data );

		return new Package(
			$data['id'] ?? null,
			$data['name'],
			$data['type'] ?? self::TYPE_BOX,
			$data['dimensions'],
			$data['boxWeight'],
			$data['maxWeight'] ?? 0.0
		);
	}

	/**
	 * @throws PackageValidationException
	 */
	public function __construct( ?string $id, string $name, string $type, string $dimensions, float $box_weight, ?float $max_weight = 0.0 ) {
		$this->name       = $name;
		$this->type       = $type;
		$this->dimensions = $dimensions;
		$this->box_weight = $box_weight;
		$this->max_weight = $max_weight;
		$this->set_or_generate_id( $id );
		$this->validate();
		$this->set_length_width_height_from_dimensions( $dimensions );
	}

	/**
	 * @throws \InvalidArgumentException
	 */
	public function __get( $property ) {
		if ( property_exists( $this, $property ) ) {
			return $this->$property;
		}
		throw new \InvalidArgumentException( sprintf( 'Property %s does not exist.', esc_html( $property ) ) );
	}

	/**
	 * @return array{
	 *     id: string,
	 *     name: string,
	 *     dimensions: string,
	 *     boxWeight: float,
	 *     maxWeight: float,
	 *     type: string,
	 *     is_user_defined: true
	 * }
	 */
	public function to_array(): array {
		return array(
			'id'              => $this->id,
			'name'            => $this->name,
			'dimensions'      => $this->dimensions,
			'boxWeight'       => $this->box_weight,
			'maxWeight'       => $this->max_weight,
			'type'            => $this->type,
			'is_user_defined' => $this->is_user_defined,
		);
	}

	/**
	 * @return array{
	 *     id: string,
	 *     name: string,
	 *     inner_dimensions: string,
	 *     box_weight: float,
	 *     max_weight: float,
	 *     is_letter: bool,
	 *     is_user_defined: bool
	 * }
	 */
	public function to_wcst_array(): array {
		return array(
			'id'               => $this->id,
			'name'             => $this->name,
			'inner_dimensions' => $this->dimensions,
			'box_weight'       => $this->box_weight,
			'max_weight'       => $this->max_weight,
			'is_letter'        => self::TYPE_ENVELOPE === $this->type,
			'is_user_defined'  => $this->is_user_defined,
		);
	}

	public function to_api_array(): array {
		return array(
			'id'              => $this->id,
			'name'            => $this->name,
			'dimensions'      => $this->dimensions,
			'length'          => $this->length,
			'width'           => $this->width,
			'height'          => $this->height,
			'box_weight'      => $this->box_weight,
			'is_letter'       => self::TYPE_ENVELOPE === $this->type,
			'is_user_defined' => $this->is_user_defined,
			'type'            => $this->type,
		);
	}

	/**
	 * Generate an ID if the package does not have one yet.
	 *
	 * We need a unique ID for each package to be able to identify them as a box_id
	 * to prepopulate the saved templates form, and other types of situations where
	 * uniqueness is required.
	 *
	 * This introduces backwards support for migrated WCS&T packages that do not have an ID
	 * and older version of WC Shipping that had a bug not setting an ID.
	 *
	 * @since 1.2.2 Moved this method here from `WC_Connect_Service_Settings_Store`.
	 * @since 1.1.2 This logic was introduced in `WC_Connect_Service_Settings_Store`.
	 *
	 * @return void
	 */
	private function set_or_generate_id( $id ) {
		if ( ! empty( $id ) && 'custom_box' !== $id ) {
			$this->id = $id;
		} else {
			$this->id = md5( $this->name );
		}
	}

	/**
	 * Throws if the package instance has invalid data.
	 *
	 * @return void
	 * @throws PackageValidationException
	 */
	private function validate() {
		if ( ! in_array( $this->type, array( self::TYPE_ENVELOPE, self::TYPE_BOX ), true ) ) {
			throw new PackageValidationException( 'Invalid package type' );
		}

		if ( 1 !== preg_match( '/^(.+) x (.+) x (.+)$/', $this->dimensions ) ) {
			throw new PackageValidationException( 'Invalid package dimensions' );
		}

		if ( $this->box_weight < 0 ) {
			throw new PackageValidationException( 'Invalid package weight' );
		}

		if ( ! empty( $this->max_weight ) && $this->max_weight < 0 ) {
			throw new PackageValidationException( 'Invalid package max weight' );
		}
	}

	/**
	 * @param array $data Package data as array.
	 *
	 * @return array Package data as array with WCS&T keys mapped to WCShip keys.
	 */
	private static function map_wcst_keys_to_wcshipping_keys( array $data ): array {
		if ( isset( $data['max_weight'] ) && ! isset( $data['maxWeight'] ) ) {
			$data['maxWeight'] = $data['max_weight'];
			unset( $data['max_weight'] );
		}

		if ( isset( $data['box_weight'] ) && ! isset( $data['boxWeight'] ) ) {
			$data['boxWeight'] = $data['box_weight'];
			unset( $data['box_weight'] );
		}

		if ( isset( $data['inner_dimensions'] ) && ! isset( $data['dimensions'] ) ) {
			$data['dimensions'] = $data['inner_dimensions'];
			unset( $data['inner_dimensions'] );
		}

		if ( isset( $data['is_letter'] ) && ! isset( $data['type'] ) ) {
			$data['type'] = $data['is_letter'] ? self::TYPE_ENVELOPE : self::TYPE_BOX;
			unset( $data['is_letter'] );
		}

		return $data;
	}

	/**
	 * Throws if the array of package data to create a Package from is invalid.
	 *
	 * @return void
	 * @throws PackageValidationException
	 */
	private static function validate_from_array_input( array $data ) {
		$required_keys   = array( 'name', 'dimensions', 'boxWeight' );
		$stringable_keys = array( 'id', 'name', 'type', 'dimensions' );
		$numeric_keys    = array( 'boxWeight', 'maxWeight' );

		foreach ( $required_keys as $required_key ) {
			if ( ! isset( $data[ $required_key ] ) ) {
				throw new PackageValidationException( sprintf( 'Missing required key: %s', esc_html( $required_key ) ) );
			}
		}

		foreach ( $stringable_keys as $stringable_key ) {
			if ( isset( $data[ $stringable_key ] ) && ! is_scalar( $data[ $stringable_key ] ) && ! is_null( $data[ $stringable_key ] ) ) {
				throw new PackageValidationException( sprintf( 'Key %s must be a string', esc_html( $stringable_key ) ) );
			}
		}

		foreach ( $numeric_keys as $numeric_key ) {
			if ( isset( $data[ $numeric_key ] ) && ! is_numeric( $data[ $numeric_key ] ) ) {
				throw new PackageValidationException( sprintf( 'Key %s must be a number', esc_html( $numeric_key ) ) );
			}
		}
	}

	/**
	 * Convert dimensions from "LxWxH" to "length", "width", "height".
	 *
	 * @param string $dimensions The dimensions string in the format "LxWxH".
	 *
	 * @return array{length: string, width: string, height: string}
	 */
	private function dimensions_to_length_width_height( string $dimensions ): array {
		list( $length, $width, $height ) = explode( 'x', $dimensions );
		return array(
			'length' => trim( $length ),
			'width'  => trim( $width ),
			'height' => trim( $height ),
		);
	}

	/**
	 * Set the length, width, and height from the dimensions.
	 *
	 * @param string $dimensions The dimensions string in the format "LxWxH".
	 *
	 * @return void
	 */
	private function set_length_width_height_from_dimensions( string $dimensions ) {
		$dimensions_array = $this->dimensions_to_length_width_height( $dimensions );
		$this->length     = (float) $dimensions_array['length'];
		$this->width      = (float) $dimensions_array['width'];
		$this->height     = (float) $dimensions_array['height'];
	}
}