<?php
/**
 * The accessibility statement data model + pure logic.
 *
 * Everything here is side-effect free and unit-testable: sanitizing input,
 * validating it, and conservatively deriving the conformance status.
 */

defined( 'ABSPATH' ) || exit;

class TSFFE_Data {

	const CONFORMANCE_FULL    = 'full';
	const CONFORMANCE_PARTIAL = 'partial';
	const CONFORMANCE_NONE    = 'none';

	/**
	 * Default, empty statement model.
	 *
	 * @return array<string, mixed>
	 */
	public static function defaults(): array {
		return array(
			'org_name'                 => '',
			'site_name'                => '',
			'site_url'                 => '',
			'scope'                    => '',
			'contact_email'            => '',
			'feedback_method'          => '',
			'enforcement_country'      => '',
			'wcag_version'             => '2.1',
			'prepared_date'            => '',
			'last_reviewed_date'       => '',
			'assessment_method'        => 'self', // self | third-party.
			'technologies'             => '',
			'conformance'              => self::CONFORMANCE_PARTIAL,
			'non_compliance'          => array(),
			'disproportionate_burden' => array(),
			'out_of_scope'            => array(),
			// Yes/no self-assessment answers, kept so the wizard can re-derive.
			'answers'                  => array(),
			'confirm_full'             => false,
		);
	}

	/**
	 * Conservatively derive the conformance status from yes/no answers.
	 *
	 * Rules (honest by construction):
	 *  - If no testing/assessment was done at all          => 'none'.
	 *  - If every accessibility question is explicitly yes
	 *    AND the owner deliberately confirms full conformance => 'full'.
	 *  - Anything else (the conservative default)            => 'partial'.
	 *
	 * Recognised answer keys (each expected to be the string 'yes' or 'no'):
	 *   tested            — has any accessibility testing been performed?
	 *   keyboard          — fully operable by keyboard?
	 *   contrast          — text meets contrast requirements?
	 *   alt_text          — images have text alternatives?
	 *   labels            — forms have labels/instructions?
	 *   headings          — proper heading structure / landmarks?
	 *   confirm_full      — deliberate confirmation of full conformance?
	 *
	 * @param array<string, mixed> $answers
	 */
	public static function derive_conformance( array $answers ): string {
		$yes = static function ( $v ): bool {
			return 'yes' === $v || true === $v || '1' === $v || 1 === $v;
		};

		// No testing at all => we cannot claim anything beyond non-conformant.
		if ( ! $yes( $answers['tested'] ?? null ) ) {
			return self::CONFORMANCE_NONE;
		}

		$required = array( 'keyboard', 'contrast', 'alt_text', 'labels', 'headings' );

		$all_yes = true;
		foreach ( $required as $key ) {
			if ( ! $yes( $answers[ $key ] ?? null ) ) {
				$all_yes = false;
				break;
			}
		}

		// "Full" is only reachable with every box ticked AND a deliberate confirmation.
		if ( $all_yes && $yes( $answers['confirm_full'] ?? null ) ) {
			return self::CONFORMANCE_FULL;
		}

		// Conservative default for a tested-but-imperfect (or unconfirmed) site.
		return self::CONFORMANCE_PARTIAL;
	}

	/**
	 * Sanitize raw input into a clean statement model.
	 *
	 * @param array<string, mixed> $in
	 * @return array<string, mixed>
	 */
	public static function sanitize( array $in ): array {
		$out = self::defaults();

		$out['org_name']      = isset( $in['org_name'] ) ? sanitize_text_field( $in['org_name'] ) : '';
		$out['site_name']     = isset( $in['site_name'] ) ? sanitize_text_field( $in['site_name'] ) : '';
		$out['site_url']      = isset( $in['site_url'] ) ? esc_url_raw( $in['site_url'] ) : '';
		$out['scope']         = isset( $in['scope'] ) ? sanitize_text_field( $in['scope'] ) : '';
		$out['contact_email'] = isset( $in['contact_email'] ) ? sanitize_email( $in['contact_email'] ) : '';
		$out['feedback_method'] = isset( $in['feedback_method'] ) ? sanitize_textarea_field( $in['feedback_method'] ) : '';
		$out['technologies']  = isset( $in['technologies'] ) ? sanitize_text_field( $in['technologies'] ) : '';

		$country = isset( $in['enforcement_country'] ) ? strtoupper( sanitize_text_field( $in['enforcement_country'] ) ) : '';
		$out['enforcement_country'] = ( null !== TSFFE_Bodies::get( $country ) ) ? $country : '';

		$wcag                 = isset( $in['wcag_version'] ) ? sanitize_text_field( $in['wcag_version'] ) : '2.1';
		$out['wcag_version']  = in_array( $wcag, array( '2.1', '2.2' ), true ) ? $wcag : '2.1';

		$out['prepared_date']      = self::sanitize_date( $in['prepared_date'] ?? '' );
		$out['last_reviewed_date'] = self::sanitize_date( $in['last_reviewed_date'] ?? '' );

		$method                  = isset( $in['assessment_method'] ) ? sanitize_text_field( $in['assessment_method'] ) : 'self';
		$out['assessment_method'] = in_array( $method, array( 'self', 'third-party' ), true ) ? $method : 'self';

		$out['non_compliance']          = self::sanitize_list( $in['non_compliance'] ?? array() );
		$out['disproportionate_burden'] = self::sanitize_list( $in['disproportionate_burden'] ?? array() );
		$out['out_of_scope']            = self::sanitize_list( $in['out_of_scope'] ?? array() );

		// Answers: each value coerced to 'yes' / 'no'.
		$answers = array();
		if ( isset( $in['answers'] ) && is_array( $in['answers'] ) ) {
			foreach ( $in['answers'] as $key => $val ) {
				$k = sanitize_key( $key );
				$answers[ $k ] = ( 'yes' === $val || '1' === $val || 1 === $val || true === $val ) ? 'yes' : 'no';
			}
		}
		$out['answers'] = $answers;

		$out['confirm_full'] = ! empty( $in['confirm_full'] ) && ( 'yes' === $in['confirm_full'] || '1' === $in['confirm_full'] || 1 === $in['confirm_full'] || true === $in['confirm_full'] );

		// Derive conformance from the answers (+confirmation), never trust a raw value.
		$answers_for_derive                 = $answers;
		$answers_for_derive['confirm_full'] = $out['confirm_full'] ? 'yes' : 'no';
		$out['conformance']                 = self::derive_conformance( $answers_for_derive );

		return $out;
	}

	/**
	 * @param mixed $list
	 * @return string[]
	 */
	private static function sanitize_list( $list ): array {
		if ( ! is_array( $list ) ) {
			return array();
		}

		$clean = array();
		foreach ( $list as $item ) {
			$item = sanitize_text_field( (string) $item );
			if ( '' !== trim( $item ) ) {
				$clean[] = $item;
			}
		}

		return array_values( $clean );
	}

	/**
	 * @param mixed $value
	 */
	private static function sanitize_date( $value ): string {
		$value = sanitize_text_field( (string) $value );

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

		// Accept YYYY-MM-DD only.
		if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			return $value;
		}

		return '';
	}

	/**
	 * Validate a (sanitized) statement model, returning a map of field => error.
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, string>
	 */
	public static function validate( array $data ): array {
		$errors = array();

		if ( empty( $data['org_name'] ) ) {
			$errors['org_name'] = __( 'The organisation or website owner name is required.', 'tendforge-statement-forge-for-eaa' );
		}

		if ( empty( $data['site_url'] ) ) {
			$errors['site_url'] = __( 'The website address is required.', 'tendforge-statement-forge-for-eaa' );
		}

		if ( empty( $data['contact_email'] ) || ! is_email( $data['contact_email'] ) ) {
			$errors['contact_email'] = __( 'A valid feedback contact email address is required.', 'tendforge-statement-forge-for-eaa' );
		}

		if ( empty( $data['enforcement_country'] ) || null === TSFFE_Bodies::get( $data['enforcement_country'] ) ) {
			$errors['enforcement_country'] = __( 'Please choose the EU country whose enforcement body applies to you.', 'tendforge-statement-forge-for-eaa' );
		}

		if ( ! in_array( $data['wcag_version'], array( '2.1', '2.2' ), true ) ) {
			$errors['wcag_version'] = __( 'Please choose a supported WCAG version.', 'tendforge-statement-forge-for-eaa' );
		}

		// Guardrail: "full" must be backed by a deliberate confirmation.
		if ( self::CONFORMANCE_FULL === ( $data['conformance'] ?? '' ) && empty( $data['confirm_full'] ) ) {
			$errors['conformance'] = __( 'Full conformance must be deliberately confirmed.', 'tendforge-statement-forge-for-eaa' );
		}

		return $errors;
	}

	/**
	 * Human-readable label for a conformance status, citing the EN/WCAG basis.
	 */
	public static function conformance_label( string $status, string $wcag_version ): string {
		switch ( $status ) {
			case self::CONFORMANCE_FULL:
				/* translators: %s: WCAG version, e.g. 2.1 */
				return sprintf( __( 'Fully conformant with WCAG %s level AA', 'tendforge-statement-forge-for-eaa' ), $wcag_version );
			case self::CONFORMANCE_NONE:
				/* translators: %s: WCAG version, e.g. 2.1 */
				return sprintf( __( 'Non-conformant with WCAG %s level AA', 'tendforge-statement-forge-for-eaa' ), $wcag_version );
			case self::CONFORMANCE_PARTIAL:
			default:
				/* translators: %s: WCAG version, e.g. 2.1 */
				return sprintf( __( 'Partially conformant with WCAG %s level AA', 'tendforge-statement-forge-for-eaa' ), $wcag_version );
		}
	}
}
