<?php
/**
 * Custom Fields post-type meta boxes.
 *
 * @package CustomFields
 * @since 1.0.0
 */

namespace CustomFields;

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

/**
 * Handles post-type meta boxes for custom field groups.
 */
final class MetaBoxes {

	private const NONCE_ACTION = 'wpps_cf_save_meta';
	private const NONCE_NAME   = 'wpps_cf_meta_nonce';

	private static bool $nonce_printed = false;

	private Locations $locations;

	private Fields $fields;

	private Relationships $relationships;

	public function __construct( Locations $locations, Fields $fields ) {
		$this->locations     = $locations;
		$this->fields        = $fields;
		$this->relationships = new Relationships();
	}

	/**
	 * Register a meta box per applicable group on the current post.
	 *
	 * @param string        $post_type Current post type.
	 * @param \WP_Post|null $post      Post being edited.
	 */
	public function register( string $post_type, ?\WP_Post $post = null ): void {
		$post_id = $post instanceof \WP_Post ? (int) $post->ID : 0;
		$groups  = $this->locations->groupsForPost( $post_id, $post_type );
		$screen  = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		$screen_id = $screen instanceof \WP_Screen ? $screen->id : $post_type;

		foreach ( $groups as $key => $group ) {
			$fields = $this->fieldsForMetaBox( $group, $post_type );
			if ( $fields === [] ) {
				continue;
			}

			$position = (string) ( $group['position'] ?? 'normal' );
			$context  = Config::resolveMetaBoxContext( $position );
			$priority = Config::resolveMetaBoxPriority( $context, (string) ( $group['priority'] ?? 'default' ) );
			$box_id   = 'wpps-cf-' . sanitize_key( (string) $key );

			add_filter(
				'postbox_classes_' . $screen_id . '_' . $box_id,
				static function ( array $classes ) use ( $group ): array {
					$classes[] = 'cf-postbox';
					if ( (string) ( $group['style'] ?? 'default' ) === 'seamless' ) {
						$classes[] = 'cf-postbox--seamless';
					}

					return $classes;
				}
			);

			add_meta_box(
				$box_id,
				(string) ( $group['title'] ?? __( 'Custom Fields', 'wp-powersuite' ) ),
				function ( $inner_post ) use ( $group, $fields, $position ): void {
					$this->renderBox( $group, (int) $inner_post->ID, $fields, $position );
				},
				$post_type,
				$context,
				$priority
			);
		}
	}

	/**
	 * Fields that should render in the classic meta box for a group.
	 *
	 * @param array<string, mixed> $group     Group definition.
	 * @param string               $post_type Post type key.
	 * @return array<int, array<string, mixed>>
	 */
	private function fieldsForMetaBox( array $group, string $post_type ): array {
		return EditorSchema::fieldsForMetaBox( $group, $post_type );
	}

	/**
	 * @param array<string, mixed>             $group    Group definition.
	 * @param int                              $post_id  Post ID.
	 * @param array<int, array<string, mixed>> $fields   Fields to render.
	 * @param string                           $position Stored position key.
	 */
	private function renderBox( array $group, int $post_id, array $fields, string $position ): void {
		if ( ! self::$nonce_printed ) {
			wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
			self::$nonce_printed = true;
		}

		$group_key  = (string) ( $group['key'] ?? '' );
		$layout_mod = $position === 'side' ? 'side' : 'normal';
		$rows       = Config::packFieldRows( $fields );

		echo '<div class="cf-meta-box cf-meta-box--grid cf-meta-box--' . esc_attr( $layout_mod ) . '">';

		foreach ( $rows as $row ) {
			echo '<div class="cf-meta-box__row">';
			foreach ( $row as $field ) {
				$type     = (string) ( $field['type'] ?? 'text' );
				$meta_key = MetaKeys::resolve( $group_key, $field );
				$field_id = (string) ( $field['key'] ?? $meta_key );
				$id_attr  = 'wpps-cf-' . sanitize_key( $group_key ) . '-' . sanitize_key( $field_id );
				$width_cls = Config::fieldWidthClass( $field );

				if ( Config::storesValue( $type ) ) {
					if ( $meta_key === '' ) {
						continue;
					}
					$value = $this->readMeta( $post_id, $meta_key, $field );
					$name  = 'wpps_cf[' . $meta_key . ']';
				} else {
					$value = '';
					$name  = '';
				}

				echo '<div class="cf-meta-box__cell ' . esc_attr( $width_cls ) . '">';
				$this->fields->render( $field, $name, $value, $id_attr, $group );
				echo '</div>';
			}
			echo '</div>';
		}

		echo '</div>';
	}

	/**
	 * @param int                  $post_id  Post ID.
	 * @param string               $meta_key Meta key.
	 * @param array<string, mixed> $field    Field definition.
	 * @return mixed
	 */
	private function readMeta( int $post_id, string $meta_key, array $field ): mixed {
		$type     = (string) ( $field['type'] ?? 'text' );
		$is_multi = $this->isMultiValueField( $type, $field );
		$stored   = get_post_meta( $post_id, $meta_key, true );

		if ( $stored === '' || $stored === null ) {
			if ( $type === 'link' ) {
				return [ 'url' => '', 'title' => '', 'target' => '' ];
			}
			if ( $is_multi ) {
				return [];
			}
			return $field['default'] ?? '';
		}

		if ( $type === 'gallery' && ! is_array( $stored ) ) {
			return array_filter( array_map( 'absint', explode( ',', (string) $stored ) ) );
		}

		return $stored;
	}

	/**
	 * @param string               $type  Field type.
	 * @param array<string, mixed> $field Field definition.
	 */
	private function isMultiValueField( string $type, array $field ): bool {
		if ( $type === 'checkbox' || $type === 'gallery' ) {
			return true;
		}

		return ! empty( $field['multiple'] ) && in_array( $type, [ 'select', 'post_object', 'term', 'user' ], true );
	}

	/**
	 * @param int $post_id Post ID being saved.
	 */
	public function save( int $post_id ): void {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( ! isset( $_POST[ self::NONCE_NAME ] ) ) {
			return;
		}
		$nonce = sanitize_text_field( wp_unslash( (string) $_POST[ self::NONCE_NAME ] ) );
		if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
			return;
		}

		$post_type = get_post_type( $post_id );
		if ( ! is_string( $post_type ) || $post_type === '' ) {
			return;
		}

		$post_type_object = get_post_type_object( $post_type );
		$capability       = $post_type_object->cap->edit_post ?? 'edit_post';
		if ( ! current_user_can( $capability, $post_id ) ) {
			return;
		}

		$groups = $this->locations->groupsForPost( $post_id );
		if ( $groups === [] ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above.
		$submitted = isset( $_POST['wpps_cf'] ) && is_array( $_POST['wpps_cf'] ) ? wp_unslash( $_POST['wpps_cf'] ) : [];

		$pending = $this->collectPendingValues( $groups, $submitted, $post_id, $post_type );
		if ( $pending['errors'] !== [] ) {
			set_transient(
				'wpps_cf_save_errors_' . get_current_user_id(),
				$pending['errors'],
				60
			);
			return;
		}

		foreach ( $pending['groups'] as $group_key => $group_data ) {
			$group = $group_data['group'];
			foreach ( $group_data['values'] as $meta_key => $value ) {
				if ( $this->fields->isEmpty( $value ) ) {
					delete_post_meta( $post_id, $meta_key );
				} else {
					update_post_meta( $post_id, $meta_key, $value );
				}
			}

			$this->relationships->syncPost( $post_id, $group, $group_data['relationships'] );
		}
	}

	/**
	 * Fields persisted through the classic meta box save handler.
	 *
	 * @param array<string, mixed> $group     Group definition.
	 * @param string               $post_type Post type key.
	 * @return array<int, array<string, mixed>>
	 */
	private function fieldsToSave( array $group, string $post_type ): array {
		return EditorSchema::fieldsForMetaBox( $group, $post_type );
	}

	/**
	 * Validate and prepare all submitted values before writing meta.
	 *
	 * @param array<string, array<string, mixed>> $groups     Matching groups.
	 * @param array<string, mixed>                $submitted  Submitted values.
	 * @param int                                 $post_id    Post ID.
	 * @param string                              $post_type  Post type key.
	 * @return array{errors: array<int, string>, groups: array<string, array<string, mixed>>}
	 */
	private function collectPendingValues( array $groups, array $submitted, int $post_id, string $post_type ): array {
		$errors = [];
		$out    = [];

		foreach ( $groups as $group_key => $group ) {
			$fields        = $this->fieldsToSave( $group, $post_type );
			$values        = [];
			$relationships = [];

			foreach ( $fields as $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}

				$type = (string) ( $field['type'] ?? 'text' );
				if ( ! Config::storesValue( $type ) ) {
					continue;
				}

				$meta_key = MetaKeys::resolve( (string) $group_key, $field );
				if ( $meta_key === '' ) {
					continue;
				}

				$old_raw = get_post_meta( $post_id, $meta_key, true );
				$raw     = $submitted[ $meta_key ] ?? ( $type === 'toggle' ? '' : null );

				if ( $this->fields->preservesValueWhenEmpty( $field ) && ( $raw === '' || $raw === null ) ) {
					if ( ! empty( $field['required'] ) && $this->fields->isEmpty( $old_raw ) ) {
						$errors[] = sprintf(
							/* translators: %s: field label. */
							__( '“%s” is required.', 'wp-powersuite' ),
							(string) ( $field['label'] ?? $meta_key )
						);
					}
					continue;
				}

				$value = $this->fields->sanitizeValue( $field, $raw, $group );

				if ( ! empty( $field['required'] ) && $this->fields->isEmpty( $value ) ) {
					$errors[] = sprintf(
						/* translators: %s: field label. */
						__( '“%s” is required.', 'wp-powersuite' ),
						(string) ( $field['label'] ?? $meta_key )
					);
					continue;
				}

				if ( ! $this->fields->isEmpty( $value ) && ! $this->fields->matchesPattern( $field, $value ) ) {
					$errors[] = sprintf(
						/* translators: %s: field label. */
						__( '“%s” has an invalid format.', 'wp-powersuite' ),
						(string) ( $field['label'] ?? $meta_key )
					);
					continue;
				}

				$values[ $meta_key ]                   = $value;
				$relationships[ $meta_key ] = [ 'old' => $old_raw, 'new' => $value ];
			}

			$out[ (string) $group_key ] = [
				'group'         => $group,
				'values'        => $values,
				'relationships' => $relationships,
			];
		}

		return [
			'errors' => $errors,
			'groups' => $out,
		];
	}

	/**
	 * Show validation errors from the last save attempt.
	 */
	public function showSaveErrors(): void {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen || $screen->base !== 'post' ) {
			return;
		}

		$errors = get_transient( 'wpps_cf_save_errors_' . get_current_user_id() );
		if ( ! is_array( $errors ) || $errors === [] ) {
			return;
		}

		delete_transient( 'wpps_cf_save_errors_' . get_current_user_id() );

		echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'Custom Fields could not save:', 'wp-powersuite' ) . '</strong></p><ul>';
		foreach ( $errors as $error ) {
			echo '<li>' . esc_html( (string) $error ) . '</li>';
		}
		echo '</ul><p>' . esc_html__( 'No field values were saved. Fix the errors above and try again.', 'wp-powersuite' ) . '</p></div>';
	}
}
