<?php
/**
 * Settings screen.
 *
 * @package Harbor_EU_EInvoicing_For_WooCommerce
 */

defined( 'ABSPATH' ) || exit;

class HEIN_Admin_Settings {

	private const REQUIRED_SELLER = [
		'name',
		'street',
		'city',
		'postcode',
		'country',
		'contact_name',
		'contact_phone',
		'contact_email',
	];

	public static function init(): void {
		add_action( 'admin_menu', [ __CLASS__, 'menu' ] );
		add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue' ] );

		register_setting(
			'hein_settings',
			'hein_settings',
			[
				'type'              => 'array',
				'sanitize_callback' => [ __CLASS__, 'sanitize' ],
			]
		);

		// wp-admin/options.php defaults this to manage_options and wp_die()s otherwise, so without
		// it every Shop Manager sees the page and is refused on Save
		add_filter(
			'option_page_capability_hein_settings',
			static function () {
				return 'manage_woocommerce';
			}
		);
	}

	/**
	 * The single source of the settings schema
	 */
	public static function defaults(): array {
		return [
			'profile'                  => 'peppol',
			'formats'                  => [ 'ubl' ],
			'store_files'              => 1,
			'delete_data_on_uninstall' => 0,
			'zero_rate_category'       => 'Z',
			'exemption_code'           => '',
			'exemption_reason'         => '',
			'seller'                   => [
				'name'             => '',
				'trading_name'     => '',
				'vat_id'           => '',
				'tax_registration' => '',
				'legal_id'         => '',
				'legal_id_scheme'  => '',
				'street'           => '',
				'street2'          => '',
				'city'             => '',
				'postcode'         => '',
				'subentity'        => '',
				'country'          => '',
				'contact_name'     => '',
				'contact_phone'    => '',
				'contact_email'    => '',
				'endpoint_id'      => '',
				'endpoint_scheme'  => '',
			],
			'payment'                  => [
				'iban'         => '',
				'bic'          => '',
				'account_name' => '',
				'terms_note'   => '',
				'due_days'     => 0,
			],
			'number'                   => [
				'prefix'  => '',
				'padding' => 6,
				'reset'   => 'yearly',
			],
		];
	}

	public static function all(): array {
		$defaults = self::defaults();
		$stored   = get_option( 'hein_settings', [] );
		$all      = wp_parse_args( is_array( $stored ) ? $stored : [], $defaults );

		foreach ( $defaults as $key => $value ) {
			if ( is_array( $value ) && array_values( $value ) !== $value ) {
				$all[ $key ] = wp_parse_args( is_array( $all[ $key ] ?? null ) ? $all[ $key ] : [], $value );
			}
		}

		$all['formats'] = array_values( array_filter( array_map( 'strval', (array) $all['formats'] ) ) );

		return $all;
	}

	public static function get( string $key, $default = '' ) {
		$all = self::all();

		return array_key_exists( $key, $all ) ? $all[ $key ] : $default;
	}

	/**
	 * The only method here that throws, and it is reached only from HEIN_Invoice_Data. The national
	 * rules (DE-R-002..007, IT-R-002/003/004, NL-R-002) are fatal and fire on the SELLER's country,
	 * so their union is one rule instead of a per-country matrix
	 *
	 * @throws HEIN_Invoice_Exception When the seller cannot appear on a compliant invoice.
	 */
	public static function seller(): array {
		$all     = self::all();
		$seller  = $all['seller'];
		$peppol  = 'en16931' !== ( $all['profile'] ?? 'peppol' );
		$country = strtoupper( trim( (string) $seller['country'] ) );

		foreach ( self::REQUIRED_SELLER as $key ) {
			if ( '' === trim( (string) $seller[ $key ] ) ) {
				throw new HEIN_Invoice_Exception(
					esc_html__(
						'The seller details are incomplete. Fill in the name, address and contact details under WooCommerce > E-Invoicing.',
						'harbor-eu-einvoicing-for-woocommerce'
					),
					'incomplete_seller'
				);
			}
		}

		// GR-R-001-1..7 demand a six-segment Greek invoice id that this plugin cannot produce
		if ( $peppol && in_array( $country, HEIN_Codes::unsupported_seller_countries(), true ) ) {
			throw new HEIN_Invoice_Exception(
				esc_html__(
					'Greek sellers are not supported by the Peppol profile in this version.',
					'harbor-eu-einvoicing-for-woocommerce'
				),
				'seller_country_unsupported'
			);
		}

		$typed = trim( (string) $seller['vat_id'] );
		$vat   = HEIN_VAT::is_valid_for( $typed, $country ) ? HEIN_VAT::canonical( $typed, $country ) : '';

		$endpoint_id     = trim( (string) $seller['endpoint_id'] );
		$endpoint_scheme = trim( (string) $seller['endpoint_scheme'] );

		if ( '' === $endpoint_id || '' === $endpoint_scheme ) {
			$derived         = '' !== $vat ? HEIN_Codes::endpoint_for_vat( $vat, $country ) : [];
			$endpoint_id     = (string) ( $derived['id'] ?? '' );
			$endpoint_scheme = (string) ( $derived['scheme'] ?? '' );
		}

		if ( $peppol && ( '' === $endpoint_id || '' === $endpoint_scheme ) ) {
			throw new HEIN_Invoice_Exception(
				esc_html__(
					'The Peppol profile requires a seller electronic address. Add a valid VAT number, or an explicit electronic address, under WooCommerce > E-Invoicing.',
					'harbor-eu-einvoicing-for-woocommerce'
				),
				'no_seller_endpoint'
			);
		}

		$scheme  = trim( (string) $seller['legal_id_scheme'] );
		$legal   = trim( (string) $seller['legal_id'] );
		$schemes = HEIN_Codes::legal_id_schemes( $country );

		// Where a country pins the scheme (NL-R-003, DK-R-014, SE-R-003/004/013 are fatal), an id
		// carrying the wrong one is worse than no id; elsewhere BT-30 is legal with no schemeID
		if ( $schemes && ! in_array( $scheme, $schemes, true ) ) {
			$legal  = '';
			$scheme = '';
		}

		if ( '' === $vat && '' === $legal ) {
			throw new HEIN_Invoice_Exception(
				esc_html__(
					'BR-CO-26 requires a seller VAT number or a legal registration number. Add one under WooCommerce > E-Invoicing.',
					'harbor-eu-einvoicing-for-woocommerce'
				),
				'no_seller_identifier'
			);
		}

		return [
			'name'             => trim( (string) $seller['name'] ),
			'trading_name'     => self::or_null( (string) $seller['trading_name'] ),
			'vat_id'           => '' !== $vat ? $vat : null,
			'tax_registration' => self::or_null( (string) $seller['tax_registration'] ),
			'legal_id'         => '' !== $legal ? $legal : null,
			'legal_id_scheme'  => '' !== $legal ? $scheme : null,
			'endpoint_id'      => $endpoint_id,
			'endpoint_scheme'  => $endpoint_scheme,
			'contact'          => [
				'name'  => trim( (string) $seller['contact_name'] ),
				'phone' => trim( (string) $seller['contact_phone'] ),
				'email' => trim( (string) $seller['contact_email'] ),
			],
			'address'          => [
				'street'    => trim( (string) $seller['street'] ),
				'street2'   => self::or_null( (string) $seller['street2'] ),
				'city'      => trim( (string) $seller['city'] ),
				'postcode'  => trim( (string) $seller['postcode'] ),
				'subentity' => self::or_null( (string) $seller['subentity'] ),
				'country'   => $country,
			],
		];
	}

	/**
	 * NEVER throws: an exception inside a Settings API sanitize callback is an uncaught fatal on
	 * options.php, which would brick the very page that fixes the misconfiguration it complained
	 * about. Problems are reported with add_settings_error()
	 */
	public static function sanitize( $input ): array {
		$input = is_array( $input ) ? $input : [];
		$clean = self::defaults();

		$clean['profile']                  = 'en16931' === ( $input['profile'] ?? '' ) ? 'en16931' : 'peppol';
		$clean['store_files']              = empty( $input['store_files'] ) ? 0 : 1;
		$clean['delete_data_on_uninstall'] = empty( $input['delete_data_on_uninstall'] ) ? 0 : 1;
		$clean['zero_rate_category']       = 'E' === ( $input['zero_rate_category'] ?? '' ) ? 'E' : 'Z';
		$clean['exemption_code']           = sanitize_text_field( (string) ( $input['exemption_code'] ?? '' ) );
		$clean['exemption_reason']         = sanitize_text_field( (string) ( $input['exemption_reason'] ?? '' ) );

		// Validated against the registry, not a hardcoded list, or no premium slug could ever persist
		$formats          = array_map( 'sanitize_key', (array) ( $input['formats'] ?? [] ) );
		$clean['formats'] = array_values( array_intersect( array_keys( HEIN_Generator_Registry::all() ), $formats ) );
		$seller           = is_array( $input['seller'] ?? null ) ? $input['seller'] : [];
		$payment          = is_array( $input['payment'] ?? null ) ? $input['payment'] : [];
		$number           = is_array( $input['number'] ?? null ) ? $input['number'] : [];

		foreach ( array_keys( $clean['seller'] ) as $key ) {
			$clean['seller'][ $key ] = sanitize_text_field( (string) ( $seller[ $key ] ?? '' ) );
		}

		$clean['seller']['country']       = strtoupper( substr( $clean['seller']['country'], 0, 2 ) );
		$clean['seller']['contact_email'] = sanitize_email( (string) ( $seller['contact_email'] ?? '' ) );
		$clean['seller']['vat_id']        = HEIN_VAT::normalize( $clean['seller']['vat_id'] );

		$clean['payment']['iban']         = strtoupper(
			(string) preg_replace( '/\s+/', '', sanitize_text_field( (string) ( $payment['iban'] ?? '' ) ) )
		);
		$clean['payment']['bic']          = strtoupper( sanitize_text_field( (string) ( $payment['bic'] ?? '' ) ) );
		$clean['payment']['account_name'] = sanitize_text_field( (string) ( $payment['account_name'] ?? '' ) );
		$clean['payment']['terms_note']   = sanitize_textarea_field( (string) ( $payment['terms_note'] ?? '' ) );
		$clean['payment']['due_days']     = min( 365, absint( $payment['due_days'] ?? 0 ) );

		$clean['number']['prefix']  = sanitize_text_field( (string) ( $number['prefix'] ?? '' ) );
		$clean['number']['padding'] = min( 20, max( 1, absint( $number['padding'] ?? 6 ) ) );
		$clean['number']['reset']   = in_array( $number['reset'] ?? '', [ 'none', 'yearly', 'monthly' ], true )
			? (string) $number['reset']
			: 'yearly';

		// The premium orchestrator hooks hein_sanitize_settings only while the licence is active, so a
		// free-side save (or one made after the licence lapses) would otherwise rebuild $clean from
		// defaults() alone and wipe the stored premium keys and endpoints. Carry the pro subtree forward
		$stored = get_option( 'hein_settings', [] );

		if ( is_array( $stored ) && isset( $stored['pro'] ) ) {
			$clean['pro'] = $stored['pro'];

			// Keep a premium format the current build can still name, even when its checkbox is absent
			$clean['formats'] = array_values(
				array_unique(
					array_merge(
						$clean['formats'],
						array_intersect( array_keys( HEIN_Generator_Registry::all() ), (array) ( $stored['formats'] ?? [] ) )
					)
				)
			);
		}

		self::report( $clean );

		// Without this the premium format checkboxes and API keys are stripped by the free
		// whitelist on every save, and the hein_format_options seam is decorative
		return (array) apply_filters( 'hein_sanitize_settings', $clean, $input );
	}

	public static function menu(): void {
		add_submenu_page(
			'woocommerce',
			__( 'E-Invoicing', 'harbor-eu-einvoicing-for-woocommerce' ),
			__( 'E-Invoicing', 'harbor-eu-einvoicing-for-woocommerce' ),
			'manage_woocommerce',
			'hein-settings',
			[ __CLASS__, 'render' ]
		);
	}

	public static function render(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}

		$hein_settings = self::all();
		$hein_formats  = HEIN_Generator_Registry::all();
		$hein_exposed  = HEIN_Invoice_Store::is_publicly_exposed();

		require HEIN_PATH . 'templates/settings-page.php';
	}

	public static function enqueue( string $hook_suffix ): void {
		$screen  = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		$screens = [ 'shop_order', 'woocommerce_page_hein-settings' ];

		if ( function_exists( 'wc_get_page_screen_id' ) ) {
			$screens[] = wc_get_page_screen_id( 'shop-order' );
		}

		if ( ! $screen || ! in_array( $screen->id, $screens, true ) ) {
			return;
		}

		wp_enqueue_style( 'hein-admin', HEIN_URL . 'assets/admin.css', [], HEIN_VERSION );
		wp_enqueue_script( 'hein-admin', HEIN_URL . 'assets/admin.js', [], HEIN_VERSION, true );

		wp_localize_script(
			'hein-admin',
			'heinAdmin',
			[
				'vatFormats' => self::vat_formats(),
				'i18n'       => [
					'valid'   => __( 'VAT number format looks valid.', 'harbor-eu-einvoicing-for-woocommerce' ),
					'invalid' => __( 'This does not match the VAT number format for its country. Include the country prefix, for example DE123456789.', 'harbor-eu-einvoicing-for-woocommerce' ),
					'unsaved' => __( 'The VAT number or purchase order reference has been edited but not saved. Leave this page and lose those edits?', 'harbor-eu-einvoicing-for-woocommerce' ),
					// The same field means different things on the two screens it appears on, and one
					// shared empty-state announced "the order" under the store's own company details
					'empty'   => [
						'order'    => __( 'No VAT number. The order will be invoiced as a domestic or consumer sale.', 'harbor-eu-einvoicing-for-woocommerce' ),
						'settings' => __( 'No VAT number. Invoices will identify your business by its legal registration number instead.', 'harbor-eu-einvoicing-for-woocommerce' ),
					],
				],
			]
		);
	}

	/**
	 * The browser has to apply the same per-country rule the server does, and the only way that stays
	 * true is to read HEIN_VAT's own patterns rather than restate them; a loose shared pattern can
	 * only ever paint an invalid number green
	 */
	private static function vat_formats(): array {
		$reflection = new ReflectionClass( 'HEIN_VAT' );
		$formats    = [];

		foreach ( (array) $reflection->getConstant( 'FORMATS' ) as $prefix => $pattern ) {
			// PCRE delimiters are not part of a JavaScript RegExp source
			$formats[ $prefix ] = substr( (string) $pattern, 1, -1 );
		}

		return $formats;
	}

	private static function report( array $clean ): void {
		$country = $clean['seller']['country'];
		$vat     = $clean['seller']['vat_id'];

		if ( '' !== $vat && ! HEIN_VAT::is_valid_for( $vat, $country ) ) {
			self::error(
				'hein_seller_vat',
				__(
					'The seller VAT number does not match the format for its country. Invoices will be issued without a VAT number until it is corrected.',
					'harbor-eu-einvoicing-for-woocommerce'
				)
			);
		}

		$schemes = HEIN_Codes::legal_id_schemes( $country );

		if ( '' !== $clean['seller']['legal_id_scheme'] && ! in_array( $clean['seller']['legal_id_scheme'], $schemes, true ) ) {
			self::error(
				'hein_legal_scheme',
				__( 'The legal registration scheme is not valid for the seller country and will not be used.', 'harbor-eu-einvoicing-for-woocommerce' )
			);
		}

		// DK-R-002 makes BT-30 mandatory for a Danish seller
		if ( 'DK' === $country && '' === trim( $clean['seller']['legal_id'] ) ) {
			self::error(
				'hein_dk_legal_id',
				__( 'Danish sellers must provide a CVR number with scheme 0184, or the invoice will be rejected.', 'harbor-eu-einvoicing-for-woocommerce' )
			);
		}

		foreach ( self::REQUIRED_SELLER as $key ) {
			if ( '' === trim( (string) $clean['seller'][ $key ] ) ) {
				self::error(
					'hein_seller_incomplete',
					__(
						'The seller name, street, city, postcode, country, contact name, phone and email are all required before an invoice can be generated.',
						'harbor-eu-einvoicing-for-woocommerce'
					)
				);
				break;
			}
		}

		// BR-E-10 is fatal unless a code or a reason is present on an 'E' document
		if ( 'E' === $clean['zero_rate_category'] && '' === $clean['exemption_code'] && '' === $clean['exemption_reason'] ) {
			self::error(
				'hein_exemption',
				__( 'The exempt category needs an exemption code or an exemption reason.', 'harbor-eu-einvoicing-for-woocommerce' )
			);
		}

		// BR-61 is fatal when the payment means code is 30, which bank transfer maps to only with an IBAN
		$bacs = get_option( 'woocommerce_bacs_settings', [] );

		if ( is_array( $bacs ) && 'yes' === ( $bacs['enabled'] ?? 'no' ) && '' === $clean['payment']['iban'] ) {
			self::error(
				'hein_iban',
				__(
					'Direct bank transfer is enabled but no IBAN is configured. Invoices will record the payment means as undefined rather than a credit transfer.',
					'harbor-eu-einvoicing-for-woocommerce'
				)
			);
		}
	}

	private static function error( string $code, string $message ): void {
		add_settings_error( 'hein_settings', $code, $message, 'warning' );
	}

	private static function or_null( string $value ): ?string {
		$value = trim( $value );

		return '' !== $value ? $value : null;
	}
}
