<?php
/**
 * Freefactu XSD Validator Class
 *
 * Validates VeriFactu XML documents against AEAT XSD schemas before submission.
 * Catches schema errors locally to avoid unnecessary AEAT API calls.
 *
 * @package Freefactu
 * @since 1.1.0
 */

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

class Freefactu_XSD_Validator {

	/**
	 * AEAT XSD schema URLs.
	 *
	 * @var array
	 */
	private static $schema_urls = array(
		'SuministroLR' => 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd',
		'SuministroInformacion' => 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd',
	);

	/**
	 * Local schema cache directory.
	 *
	 * @var string
	 */
	private static $cache_dir = '';

	/**
	 * Initialize cache directory.
	 *
	 * @return string Cache directory path.
	 */
	private static function get_cache_dir() {
		if ( empty( self::$cache_dir ) ) {
			$upload_dir = wp_upload_dir();
			self::$cache_dir = $upload_dir['basedir'] . '/freefactu/xsd/';
			
			// Create directory if it doesn't exist
			if ( ! file_exists( self::$cache_dir ) ) {
				wp_mkdir_p( self::$cache_dir );
			}
		}
		return self::$cache_dir;
	}

	/**
	 * Get XSD schema file path, downloading if necessary.
	 *
	 * @param string $schema_name Schema name (SuministroLR or SuministroInformacion).
	 * @return string|WP_Error Path to local schema file or WP_Error on failure.
	 */
	private static function get_schema_path( $schema_name ) {
		if ( ! isset( self::$schema_urls[ $schema_name ] ) ) {
			return new WP_Error( 'invalid_schema', __( 'Esquema no válido.', 'freefactu' ) );
		}

		$cache_dir = self::get_cache_dir();
		$local_path = $cache_dir . $schema_name . '.xsd';
		
		// Check if we have a cached version (cache for 7 days)
		if ( file_exists( $local_path ) ) {
			$file_age = time() - filemtime( $local_path );
			if ( $file_age < 7 * DAY_IN_SECONDS ) {
				return $local_path;
			}
		}

		// Download ALL schemas (main schema imports the other)
		$download_result = self::download_all_schemas();
		if ( is_wp_error( $download_result ) ) {
			// If we have a cached version, use it even if expired
			if ( file_exists( $local_path ) ) {
				return $local_path;
			}
			return $download_result;
		}

		return $local_path;
	}

	/**
	 * Download all required XSD schemas and fix import references.
	 *
	 * @return true|WP_Error True on success, WP_Error on failure.
	 */
	private static function download_all_schemas() {
		$cache_dir = self::get_cache_dir();

		// First download SuministroInformacion (it's referenced by SuministroLR)
		$info_response = wp_remote_get( self::$schema_urls['SuministroInformacion'], array(
			'timeout' => 30,
			'sslverify' => true,
		) );

		if ( is_wp_error( $info_response ) ) {
			return new WP_Error( 'download_failed', 
				/* translators: %s: Error message */
				sprintf( __( 'No se pudo descargar SuministroInformacion.xsd: %s', 'freefactu' ), $info_response->get_error_message() )
			);
		}

		$info_content = wp_remote_retrieve_body( $info_response );
		if ( empty( $info_content ) ) {
			return new WP_Error( 'empty_schema', __( 'El esquema SuministroInformacion.xsd está vacío.', 'freefactu' ) );
		}

		// Save SuministroInformacion
		$info_path = $cache_dir . 'SuministroInformacion.xsd';
		if ( false === file_put_contents( $info_path, $info_content ) ) {
			return new WP_Error( 'save_failed', __( 'No se pudo guardar SuministroInformacion.xsd.', 'freefactu' ) );
		}

		// Now download SuministroLR
		$lr_response = wp_remote_get( self::$schema_urls['SuministroLR'], array(
			'timeout' => 30,
			'sslverify' => true,
		) );

		if ( is_wp_error( $lr_response ) ) {
			return new WP_Error( 'download_failed', 
				/* translators: %s: Error message */
				sprintf( __( 'No se pudo descargar SuministroLR.xsd: %s', 'freefactu' ), $lr_response->get_error_message() )
			);
		}

		$lr_content = wp_remote_retrieve_body( $lr_response );
		if ( empty( $lr_content ) ) {
			return new WP_Error( 'empty_schema', __( 'El esquema SuministroLR.xsd está vacío.', 'freefactu' ) );
		}

		// Rewrite import path to use local file
		$lr_content = str_replace(
			'schemaLocation="SuministroInformacion.xsd"',
			'schemaLocation="' . $info_path . '"',
			$lr_content
		);
		
		// Also handle full URL imports
		$lr_content = str_replace(
			self::$schema_urls['SuministroInformacion'],
			$info_path,
			$lr_content
		);

		// Save SuministroLR with fixed paths
		$lr_path = $cache_dir . 'SuministroLR.xsd';
		if ( false === file_put_contents( $lr_path, $lr_content ) ) {
			return new WP_Error( 'save_failed', __( 'No se pudo guardar SuministroLR.xsd.', 'freefactu' ) );
		}

		return true;
	}

	/**
	 * Validate XML against VeriFactu XSD schemas.
	 *
	 * What this function does:
	 * - Loads the XML document
	 * - Downloads/caches the AEAT XSD schemas
	 * - Validates XML structure against schemas
	 * - Returns validation errors if any
	 *
	 * Where it's called from:
	 * - class-freefactu-client.php: Before sending to AEAT
	 *
	 * @param string $xml_content XML content to validate.
	 * @return true|WP_Error True if valid, WP_Error with validation errors.
	 */
	public static function validate( $xml_content ) {
		if ( empty( $xml_content ) ) {
			return new WP_Error( 'empty_xml', __( 'El contenido XML está vacío.', 'freefactu' ) );
		}

		// Enable user error handling
		libxml_use_internal_errors( true );
		libxml_clear_errors();

		// Load the XML
		$dom = new DOMDocument();
		$dom->preserveWhiteSpace = false;
		
		if ( ! $dom->loadXML( $xml_content ) ) {
			$errors = self::get_libxml_errors();
			return new WP_Error( 'xml_parse_error', 
				/* translators: %s: Error details */
				sprintf( __( 'Error al parsear XML: %s', 'freefactu' ), implode( '; ', $errors ) )
			);
		}

		// Clear any existing cache first (in case of corrupt files)
		self::clear_cache();

		// Get the main schema (SuministroLR is the root schema)
		$schema_path = self::get_schema_path( 'SuministroLR' );
		if ( is_wp_error( $schema_path ) ) {
			// If we can't get schema, fall back to basic validation
			Freefactu_Logger::log( 'XSD schema unavailable, using basic validation: ' . $schema_path->get_error_message(), 'warning' );
			libxml_clear_errors();
			libxml_use_internal_errors( false );
			return self::validate_basic( $xml_content );
		}

		// Validate against schema
		$valid = @$dom->schemaValidate( $schema_path );

		if ( ! $valid ) {
			$errors = self::get_libxml_errors();
			
			// Check if it's a schema loading error (import issues)
			$has_schema_error = false;
			foreach ( $errors as $error ) {
				if ( strpos( $error, 'failed to load') !== false || 
				     strpos( $error, 'Failed to locate') !== false ||
				     strpos( $error, 'does not resolve') !== false ) {
					$has_schema_error = true;
					break;
				}
			}
			
			// If schema loading failed, fall back to basic validation
			if ( $has_schema_error ) {
				Freefactu_Logger::log( 'XSD schema import error, using basic validation', 'warning' );
				libxml_clear_errors();
				libxml_use_internal_errors( false );
				return self::validate_basic( $xml_content );
			}
			
			// Log actual validation errors
			Freefactu_Logger::log( 'XSD validation failed: ' . implode( '; ', $errors ), 'error' );
			
			return new WP_Error( 'xsd_validation_error', 
				/* translators: %s: Error details */
				sprintf( __( 'Error de validación XSD: %s', 'freefactu' ), implode( '; ', $errors ) )
			);
		}

		libxml_clear_errors();
		libxml_use_internal_errors( false );

		return true;
	}

	/**
	 * Perform basic structural validation without full XSD.
	 *
	 * This is a fallback when XSD schemas can't be downloaded.
	 * Checks for required elements and basic structure.
	 *
	 * @param string $xml_content XML content to validate.
	 * @return true|WP_Error True if valid, WP_Error with errors.
	 */
	public static function validate_basic( $xml_content ) {
		if ( empty( $xml_content ) ) {
			return new WP_Error( 'empty_xml', __( 'El contenido XML está vacío.', 'freefactu' ) );
		}

		libxml_use_internal_errors( true );
		libxml_clear_errors();

		$dom = new DOMDocument();
		if ( ! $dom->loadXML( $xml_content ) ) {
			$errors = self::get_libxml_errors();
			return new WP_Error( 'xml_parse_error', 
				/* translators: %s: Error details */
				sprintf( __( 'Error al parsear XML: %s', 'freefactu' ), implode( '; ', $errors ) )
			);
		}

		$errors = array();

		// Create XPath for querying
		$xpath = new DOMXPath( $dom );
		$xpath->registerNamespace( 'sfLR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd' );
		$xpath->registerNamespace( 'sf', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd' );

		// Check required elements
		$required_elements = array(
			'//sfLR:Cabecera' => 'Cabecera',
			'//sf:ObligadoEmision' => 'ObligadoEmision',
			'//sf:NIF' => 'NIF',
			'//sf:NombreRazon' => 'NombreRazon',
			'//sfLR:RegistroFactura' => 'RegistroFactura',
			'//sf:RegistroAlta' => 'RegistroAlta',
			'//sf:IDFactura' => 'IDFactura',
			'//sf:NumSerieFactura' => 'NumSerieFactura',
			'//sf:FechaExpedicionFactura' => 'FechaExpedicionFactura',
			'//sf:TipoFactura' => 'TipoFactura',
		);

		foreach ( $required_elements as $xpath_query => $element_name ) {
			$nodes = $xpath->query( $xpath_query );
			if ( $nodes->length === 0 ) {
				/* translators: %s: XML Element name */
				$errors[] = sprintf( __( 'Falta elemento requerido: %s', 'freefactu' ), $element_name );
			}
		}

		// Validate TipoFactura value
		$tipo_nodes = $xpath->query( '//sf:TipoFactura' );
		if ( $tipo_nodes->length > 0 ) {
			$tipo = $tipo_nodes->item( 0 )->nodeValue;
			$valid_tipos = array( 'F1', 'F2', 'F3', 'R1', 'R2', 'R3', 'R4', 'R5' );
			if ( ! in_array( $tipo, $valid_tipos, true ) ) {
				/* translators: %s: Invoice type (e.g. F1, R1) */
				$errors[] = sprintf( __( 'TipoFactura inválido: %s', 'freefactu' ), $tipo );
			}
		}

		// Validate NIF format (Spanish NIF/CIF)
		$nif_nodes = $xpath->query( '//sf:ObligadoEmision/sf:NIF' );
		if ( $nif_nodes->length > 0 ) {
			$nif = $nif_nodes->item( 0 )->nodeValue;
			if ( ! self::validate_spanish_nif( $nif ) ) {
				/* translators: %s: NIF value */
				$errors[] = sprintf( __( 'Formato de NIF inválido: %s', 'freefactu' ), $nif );
			}
		}

		// Validate date format (dd-mm-yyyy)
		$date_nodes = $xpath->query( '//sf:FechaExpedicionFactura' );
		if ( $date_nodes->length > 0 ) {
			$date = $date_nodes->item( 0 )->nodeValue;
			if ( ! preg_match( '/^\d{2}-\d{2}-\d{4}$/', $date ) ) {
				/* translators: %s: Date value */
				$errors[] = sprintf( __( 'Formato de fecha inválido: %s (debe ser dd-mm-yyyy)', 'freefactu' ), $date );
			}
		}

		libxml_clear_errors();
		libxml_use_internal_errors( false );

		if ( ! empty( $errors ) ) {
			return new WP_Error( 'basic_validation_error', 
				/* translators: %s: Error details */
				sprintf( __( 'Errores de validación: %s', 'freefactu' ), implode( '; ', $errors ) )
			);
		}

		return true;
	}

	/**
	 * Validate Spanish NIF/CIF format.
	 *
	 * @param string $nif NIF/CIF to validate.
	 * @return bool True if valid format.
	 */
	private static function validate_spanish_nif( $nif ) {
		if ( empty( $nif ) ) {
			return false;
		}

		$nif = strtoupper( preg_replace( '/[^A-Z0-9]/i', '', $nif ) );
		$length = strlen( $nif );

		// NIF: 8 digits + 1 letter OR 1 letter (X,Y,Z for foreigners) + 7 digits + 1 letter
		// CIF: 1 letter + 7 digits + 1 control character (letter or digit)
		
		if ( $length < 8 || $length > 9 ) {
			return false;
		}

		// Basic format check: starts with letter or digit
		if ( ! preg_match( '/^[A-Z0-9][0-9]{7}[A-Z0-9]$/', $nif ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get formatted libxml errors.
	 *
	 * @return array Array of error messages.
	 */
	private static function get_libxml_errors() {
		$errors = array();
		foreach ( libxml_get_errors() as $error ) {
			$message = trim( $error->message );
			if ( ! empty( $message ) ) {
				$errors[] = sprintf( 
					'[Línea %d]: %s', 
					$error->line, 
					$message 
				);
			}
		}
		libxml_clear_errors();
		return array_unique( $errors );
	}

	/**
	 * Clear cached XSD schemas.
	 *
	 * @return int Number of files deleted.
	 */
	public static function clear_cache() {
		$cache_dir = self::get_cache_dir();
		$count = 0;
		
		$files = glob( $cache_dir . '*.xsd' );
		if ( is_array( $files ) ) {
			foreach ( $files as $file ) {
				if ( wp_delete_file( $file ) ) {
					$count++;
				}
			}
		}
		
		return $count;
	}

	/**
	 * Check if XSD validation is available.
	 *
	 * @return bool True if validation can be performed.
	 */
	public static function is_available() {
		// Check if libxml is available with schema validation
		if ( ! function_exists( 'libxml_use_internal_errors' ) ) {
			return false;
		}

		if ( ! class_exists( 'DOMDocument' ) ) {
			return false;
		}

		// Check if DOMDocument has schemaValidate method
		$dom = new DOMDocument();
		if ( ! method_exists( $dom, 'schemaValidate' ) ) {
			return false;
		}

		return true;
	}
}
