<?php
/**
 * Built-in form scoped CSS generator.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

/**
 * Generates safe form-specific CSS from Design Studio settings.
 */
class ScopedCssGenerator {
	/**
	 * Generate scoped CSS for one form.
	 *
	 * @param int                                           $form_id Form id.
	 * @param array<string,array<string,mixed>>             $design Design Studio settings.
	 * @param array<int,array<string,array<string,mixed>>> $field_styles Per-field style settings.
	 * @return string
	 */
	public function generate( $form_id, array $design, array $field_styles = array() ) {
		$form_id = absint( $form_id );
		if ( $form_id <= 0 ) {
			return '';
		}

		$scope = '.subin-form-' . $form_id;
		$css   = array();

		foreach ( $this->selector_map( $scope ) as $part => $selector ) {
			$settings = is_array( $design[ $part ] ?? null ) ? $design[ $part ] : array();
			if ( empty( $settings ) ) {
				continue;
			}

			$declarations = $this->declarations_for_part( $part, $settings );
			if ( ! empty( $declarations ) ) {
				$css[] = $selector . '{' . implode( '', $declarations ) . '}';
			}

			$hover = $this->hover_declarations( $part, $settings );
			if ( ! empty( $hover ) ) {
				$css[] = $selector . ':hover{' . implode( '', $hover ) . '}';
			}

			$focus = $this->focus_declarations( $part, $settings );
			if ( ! empty( $focus ) ) {
				$css[] = $selector . ':focus{' . implode( '', $focus ) . '}';
			}

			$error = $this->error_declarations( $part, $settings );
			if ( ! empty( $error ) ) {
				$css[] = $this->error_selector( $selector, $part ) . '{' . implode( '', $error ) . '}';
			}

			$placeholder = $this->placeholder_declarations( $part, $settings );
			if ( ! empty( $placeholder ) ) {
				$css[] = $selector . '::placeholder{' . implode( '', $placeholder ) . '}';
			}
		}

		foreach ( $this->field_selector_map( $scope, $field_styles ) as $part => $field_selectors ) {
			foreach ( $field_selectors as $field_id => $selector ) {
				$settings = is_array( $field_styles[ $field_id ][ $part ] ?? null ) ? $field_styles[ $field_id ][ $part ] : array();
				if ( empty( $settings ) ) {
					continue;
				}

				$declarations = $this->declarations_for_part( $part, $settings );
				if ( ! empty( $declarations ) ) {
					$css[] = $selector . '{' . implode( '', $declarations ) . '}';
				}

				$focus = $this->focus_declarations( $part, $settings );
				if ( ! empty( $focus ) ) {
					$css[] = $selector . ':focus{' . implode( '', $focus ) . '}';
				}

				$error = $this->error_declarations( $part, $settings );
				if ( ! empty( $error ) ) {
					$css[] = $this->error_selector( $selector, $part ) . '{' . implode( '', $error ) . '}';
				}

				$placeholder = $this->placeholder_declarations( $part, $settings );
				if ( ! empty( $placeholder ) ) {
					$css[] = $selector . '::placeholder{' . implode( '', $placeholder ) . '}';
				}
			}
		}

		return implode( "\n", $css );
	}

	/**
	 * Get scoped selectors for supported parts.
	 *
	 * @param string $scope Form scope.
	 * @return array<string,string>
	 */
	private function selector_map( $scope ) {
		$root = $scope . '.subin-form-wrap';

		return array(
			'form_wrapper'       => $root,
			'form'               => $root . ' [data-subin-part="form"]',
			'layout_container'   => $root . ' [data-subin-part="layout_container"]',
			'layout_row'         => $root . ' [data-subin-part="layout_row"]',
			'layout_column'      => $root . ' [data-subin-part="layout_column"]',
			'header'             => $root . ' [data-subin-part="header"]',
			'header_title'       => $root . ' [data-subin-part="header_title"]',
			'header_description' => $root . ' [data-subin-part="header_description"]',
			'field_group'        => $root . ' [data-subin-part="field_group"]',
			'field_label'        => $root . ' [data-subin-part="field_label"]',
			'field_input'        => $root . ' [data-subin-part="field_input"]',
			'field_help'         => $root . ' [data-subin-part="field_help"]',
			'design_block'       => $root . ' [data-subin-part="design_block"]',
			'submit_button'      => $root . ' [data-subin-part="submit_button"]',
			'footer'             => $root . ' [data-subin-part="footer"]',
			'privacy_text'       => $root . ' [data-subin-part="privacy_text"]',
			'success_message'    => $root . ' [data-subin-part="success_message"]',
			'error_message'      => $root . ' [data-subin-part="error_message"]',
		);
	}

	/**
	 * Get per-field scoped selectors.
	 *
	 * @param string                                      $scope Form scope.
	 * @param array<int,array<string,array<string,mixed>>> $field_styles Per-field style settings.
	 * @return array<string,array<int,string>>
	 */
	private function field_selector_map( $scope, array $field_styles ) {
		$root = $scope . '.subin-form-wrap';
		$map  = array();

		foreach ( $field_styles as $field_id => $part_settings ) {
			$field_id = absint( $field_id );
			if ( $field_id <= 0 || ! is_array( $part_settings ) ) {
				continue;
			}

			foreach ( array( 'field_group', 'field_label', 'field_input', 'field_help' ) as $part ) {
				if ( empty( $part_settings[ $part ] ) || ! is_array( $part_settings[ $part ] ) ) {
					continue;
				}
				$map[ $part ][ $field_id ] = $root . ' [data-subin-part="' . $part . '"][data-subin-field-id="' . $field_id . '"]';
			}
		}

		return $map;
	}

	/**
	 * Build base declarations.
	 *
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,string>
	 */
	private function declarations_for_part( $part, array $settings ) {
		$declarations = array();

		$this->add_color( $declarations, 'background_color', 'background-color', $settings );
		$this->add_gradient( $declarations, $settings );
		$this->add_background_image( $declarations, $settings );
		$this->add_color( $declarations, 'text_color', 'color', $settings );
		$this->add_color( $declarations, 'border_color', 'border-color', $settings );
		$this->add_px( $declarations, 'border_width', 'border-width', $settings );
		if ( ! empty( $settings['border_width'] ) ) {
			$declarations[] = 'border-style:solid;';
		}
		$this->add_px( $declarations, 'border_radius', 'border-radius', $settings );
		$this->add_px( $declarations, 'padding', 'padding', $settings );
		$this->add_px( $declarations, 'padding_x', 'padding-left', $settings );
		$this->add_px( $declarations, 'padding_x', 'padding-right', $settings );
		$this->add_px( $declarations, 'padding_y', 'padding-top', $settings );
		$this->add_px( $declarations, 'padding_y', 'padding-bottom', $settings );
		$this->add_px( $declarations, 'margin_bottom', 'margin-bottom', $settings );
		$this->add_px( $declarations, 'font_size', 'font-size', $settings );
		$this->add_raw_choice( $declarations, 'font_weight', 'font-weight', $settings, array( '300', '400', '500', '600', '700', '800' ) );
		$this->add_numeric_range( $declarations, 'line_height', 'line-height', $settings, 1.0, 2.2 );
		$this->add_raw_choice( $declarations, 'text_align', 'text-align', $settings, array( 'left', 'center', 'right' ) );
		$this->add_box_shadow( $declarations, $settings );

		if ( ! empty( $settings['width'] ) ) {
			if ( 'full' === $settings['width'] ) {
				$declarations[] = 'display:block;';
				$declarations[] = 'width:100%;';
				$declarations[] = 'max-width:100%;';
			} elseif ( 'auto' === $settings['width'] ) {
				$declarations[] = 'width:auto;';
			} elseif ( in_array( (string) $settings['width'], array( '25', '33', '50', '66', '75', '100' ), true ) ) {
				$declarations[] = 'display:block;';
				$declarations[] = 'width:' . (string) $settings['width'] . '%;';
				$declarations[] = 'max-width:100%;';
			}
		}

		if ( 'submit_button' === $part && ! empty( $settings['button_width'] ) && 'full' === $settings['button_width'] ) {
			$declarations[] = 'display:block;';
			$declarations[] = 'width:100%;';
		}

		$this->add_px( $declarations, 'max_width', 'max-width', $settings );

		return $declarations;
	}

	/**
	 * Build hover declarations.
	 *
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,string>
	 */
	private function hover_declarations( $part, array $settings ) {
		if ( ! in_array( $part, array( 'submit_button', 'field_input' ), true ) ) {
			return array();
		}

		$declarations = array();
		$this->add_color( $declarations, 'hover_background_color', 'background-color', $settings );
		$this->add_color( $declarations, 'hover_text_color', 'color', $settings );
		$this->add_color( $declarations, 'hover_border_color', 'border-color', $settings );
		$this->add_shadow_choice( $declarations, 'hover_box_shadow', $settings );

		return $declarations;
	}

	/**
	 * Build focus declarations.
	 *
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,string>
	 */
	private function focus_declarations( $part, array $settings ) {
		if ( ! in_array( $part, array( 'field_input', 'submit_button' ), true ) ) {
			return array();
		}

		$declarations = array();
		$this->add_color( $declarations, 'focus_background_color', 'background-color', $settings );
		$this->add_color( $declarations, 'focus_text_color', 'color', $settings );
		$this->add_color( $declarations, 'focus_border_color', 'border-color', $settings );
		$this->add_shadow_choice( $declarations, 'focus_box_shadow', $settings );

		return $declarations;
	}

	/**
	 * Build error-state declarations.
	 *
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,string>
	 */
	private function error_declarations( $part, array $settings ) {
		if ( 'field_input' !== $part ) {
			return array();
		}

		$declarations = array();
		$this->add_color( $declarations, 'error_background_color', 'background-color', $settings );
		$this->add_color( $declarations, 'error_text_color', 'color', $settings );
		$this->add_color( $declarations, 'error_border_color', 'border-color', $settings );
		$this->add_shadow_choice( $declarations, 'error_box_shadow', $settings );

		return $declarations;
	}

	/**
	 * Get scoped selector for field error state.
	 *
	 * @param string $selector Base selector.
	 * @param string $part Part.
	 * @return string
	 */
	private function error_selector( $selector, $part ) {
		if ( 'field_input' === $part ) {
			$error_selector = preg_replace( '/ \[data-subin-part="field_input"/', ' .subin-field--error [data-subin-part="field_input"', $selector, 1 );
			return ( $error_selector ?: $selector ) . ', ' . $selector . '[aria-invalid="true"]';
		}

		return $selector;
	}

	/**
	 * Build placeholder declarations.
	 *
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,string>
	 */
	private function placeholder_declarations( $part, array $settings ) {
		if ( 'field_input' !== $part ) {
			return array();
		}

		$declarations = array();
		$this->add_color( $declarations, 'placeholder_color', 'color', $settings );

		return $declarations;
	}

	/**
	 * Add a color declaration.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param string              $key Setting key.
	 * @param string              $property CSS property.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_color( array &$declarations, $key, $property, array $settings ) {
		$value = $settings[ $key ] ?? '';
		if ( is_string( $value ) && preg_match( '/^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/', $value ) ) {
			$declarations[] = $property . ':' . $value . ';';
		}
	}

	/**
	 * Add a structured gradient declaration.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_gradient( array &$declarations, array $settings ) {
		$gradient = is_array( $settings['background_gradient'] ?? null ) ? $settings['background_gradient'] : array();
		if ( empty( $gradient ) ) {
			return;
		}

		$type      = sanitize_key( (string) ( $gradient['type'] ?? '' ) );
		$direction = sanitize_key( (string) ( $gradient['direction'] ?? '' ) );
		$from      = (string) ( $gradient['from'] ?? '' );
		$to        = (string) ( $gradient['to'] ?? '' );

		if ( 'linear' !== $type || ! in_array( $direction, array( '90deg', '120deg', '135deg', '160deg', '180deg' ), true ) ) {
			return;
		}

		if ( ! preg_match( '/^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/', $from ) || ! preg_match( '/^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/', $to ) ) {
			return;
		}

		$declarations[] = 'background:linear-gradient(' . $direction . ',' . $from . ',' . $to . ');';
	}

	/**
	 * Add safe media background-image declarations.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_background_image( array &$declarations, array $settings ) {
		$url = (string) ( $settings['background_image_url'] ?? '' );
		if ( '' === $url || preg_match( '/[\r\n\'"<>]/', $url ) || preg_match( '/^(?:javascript|data):/i', $url ) ) {
			return;
		}

		$url = esc_url_raw( $url, array( 'http', 'https' ) );
		if ( '' === $url ) {
			return;
		}

		$overlay = $this->rgba_from_settings( $settings, 'overlay_color', 'overlay_opacity' );
		$image   = 'url("' . $url . '")';
		if ( '' !== $overlay ) {
			$image = 'linear-gradient(' . $overlay . ',' . $overlay . '),' . $image;
		}

		$declarations[] = 'background-image:' . $image . ';';
		$this->add_raw_choice( $declarations, 'background_size', 'background-size', $settings, array( 'cover', 'contain', 'auto' ) );
		$positions = array(
			'center_center' => 'center center',
			'center_top'    => 'center top',
			'center_bottom' => 'center bottom',
			'left_center'   => 'left center',
			'right_center'  => 'right center',
		);
		$position = sanitize_key( (string) ( $settings['background_position'] ?? '' ) );
		if ( isset( $positions[ $position ] ) ) {
			$declarations[] = 'background-position:' . $positions[ $position ] . ';';
		}
		$this->add_raw_choice( $declarations, 'background_repeat', 'background-repeat', $settings, array( 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' ) );
	}

	/**
	 * Add a pixel declaration.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param string              $key Setting key.
	 * @param string              $property CSS property.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_px( array &$declarations, $key, $property, array $settings ) {
		if ( isset( $settings[ $key ] ) && is_numeric( $settings[ $key ] ) ) {
			$declarations[] = $property . ':' . absint( $settings[ $key ] ) . 'px;';
		}
	}

	/**
	 * Add an allowlisted raw CSS value.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param string              $key Setting key.
	 * @param string              $property CSS property.
	 * @param array<string,mixed> $settings Settings.
	 * @param array<int,string>   $allowed Allowed values.
	 * @return void
	 */
	private function add_raw_choice( array &$declarations, $key, $property, array $settings, array $allowed ) {
		$value = (string) ( $settings[ $key ] ?? '' );
		if ( in_array( $value, $allowed, true ) ) {
			$declarations[] = $property . ':' . $value . ';';
		}
	}

	/**
	 * Add a numeric range declaration.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param string              $key Setting key.
	 * @param string              $property CSS property.
	 * @param array<string,mixed> $settings Settings.
	 * @param float               $min Minimum.
	 * @param float               $max Maximum.
	 * @return void
	 */
	private function add_numeric_range( array &$declarations, $key, $property, array $settings, $min, $max ) {
		if ( ! isset( $settings[ $key ] ) || ! is_numeric( $settings[ $key ] ) ) {
			return;
		}

		$value = (float) $settings[ $key ];
		if ( $value < $min || $value > $max ) {
			return;
		}

		$declarations[] = $property . ':' . rtrim( rtrim( (string) $value, '0' ), '.' ) . ';';
	}

	/**
	 * Add a box-shadow preset.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_box_shadow( array &$declarations, array $settings ) {
		if ( isset( $settings['box_shadow'] ) && 'custom' === sanitize_key( (string) $settings['box_shadow'] ) ) {
			$shadow = $this->custom_shadow_value( $settings );
			if ( '' !== $shadow ) {
				$declarations[] = 'box-shadow:' . $shadow . ';';
			}
			return;
		}

		$this->add_shadow_choice( $declarations, 'box_shadow', $settings );
	}

	/**
	 * Add a preset box shadow from a property.
	 *
	 * @param array<int,string>   $declarations Declarations.
	 * @param string              $key Property key.
	 * @param array<string,mixed> $settings Settings.
	 * @return void
	 */
	private function add_shadow_choice( array &$declarations, $key, array $settings ) {
		$shadows = array(
			'none'   => 'none',
			'soft'   => '0 8px 24px rgba(15,23,42,0.08)',
			'medium' => '0 12px 32px rgba(15,23,42,0.12)',
			'strong' => '0 16px 44px rgba(15,23,42,0.16)',
		);

		$value = sanitize_key( (string) ( $settings[ $key ] ?? '' ) );
		if ( isset( $shadows[ $value ] ) ) {
			$declarations[] = 'box-shadow:' . $shadows[ $value ] . ';';
		}
	}

	/**
	 * Build custom shadow value.
	 *
	 * @param array<string,mixed> $settings Settings.
	 * @return string
	 */
	private function custom_shadow_value( array $settings ) {
		$color = $this->rgba_from_settings( $settings, 'shadow_color', 'shadow_opacity' );
		if ( '' === $color ) {
			return '';
		}

		return (int) ( $settings['shadow_x'] ?? 0 ) . 'px ' . (int) ( $settings['shadow_y'] ?? 8 ) . 'px ' . absint( $settings['shadow_blur'] ?? 24 ) . 'px ' . (int) ( $settings['shadow_spread'] ?? 0 ) . 'px ' . $color;
	}

	/**
	 * Build rgba() from color and percent opacity settings.
	 *
	 * @param array<string,mixed> $settings Settings.
	 * @param string              $color_key Color key.
	 * @param string              $opacity_key Opacity key.
	 * @return string
	 */
	private function rgba_from_settings( array $settings, $color_key, $opacity_key ) {
		$color = (string) ( $settings[ $color_key ] ?? '' );
		if ( ! preg_match( '/^#(?:[a-f0-9]{3}|[a-f0-9]{6})$/', $color ) ) {
			return '';
		}

		if ( 4 === strlen( $color ) ) {
			$color = '#' . $color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3];
		}

		$opacity = isset( $settings[ $opacity_key ] ) && is_numeric( $settings[ $opacity_key ] ) ? absint( $settings[ $opacity_key ] ) : 100;
		if ( $opacity > 100 ) {
			$opacity = 100;
		}

		$alpha = rtrim( rtrim( (string) ( $opacity / 100 ), '0' ), '.' );
		if ( '' === $alpha ) {
			$alpha = '0';
		}

		return 'rgba(' . hexdec( substr( $color, 1, 2 ) ) . ',' . hexdec( substr( $color, 3, 2 ) ) . ',' . hexdec( substr( $color, 5, 2 ) ) . ',' . $alpha . ')';
	}
}
