<?php
/**
 * Document orchestration: generate invoice/credit-note (CII + UBL + PDF +
 * Factur-X), store files in uploads/fepw-documents/, register DB rows,
 * nonce-protected downloads, refund => avoir hook.
 *
 * Factur-X assembly uses the vendored atgp/factur-x writer and validates the
 * CII XML against the bundled official XSDs before embedding. The Factur-X
 * output is labeled "validate with your PA" pending veraPDF golden samples.
 *
 * @package FEPW
 */

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

/**
 * Class FEPW_Documents
 */
class FEPW_Documents {

	/**
	 * Singleton.
	 *
	 * @var FEPW_Documents|null
	 */
	private static $instance = null;

	/**
	 * Instance.
	 *
	 * @return FEPW_Documents
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		add_action( 'woocommerce_order_status_completed', array( $this, 'on_order_completed' ) );
		add_action( 'woocommerce_order_refunded', array( $this, 'on_refund' ), 10, 2 );
		add_action( 'admin_post_fepw_generate', array( $this, 'handle_generate' ) );
		add_action( 'admin_post_fepw_download', array( $this, 'handle_download' ) );
	}

	/**
	 * Storage dir. Invoices are legal records containing personal data, so the
	 * directory name carries a per-site random token (guessable URLs would
	 * expose documents on servers that ignore .htaccess, e.g. nginx), plus an
	 * .htaccess deny and an index.html. Downloads go through the
	 * nonce-and-capability-protected endpoint only.
	 *
	 * @return string
	 */
	public static function storage_dir() {
		$token = get_option( 'fepw_docs_token' );
		if ( ! is_string( $token ) || ! preg_match( '/^[a-z0-9]{16}$/', $token ) ) {
			$token = strtolower( wp_generate_password( 16, false, false ) );
			update_option( 'fepw_docs_token', $token );
		}
		$uploads = wp_upload_dir();
		$dir     = trailingslashit( $uploads['basedir'] ) . 'fepw-documents-' . $token . '/';
		if ( ! is_dir( $dir ) ) {
			wp_mkdir_p( $dir );
			file_put_contents( $dir . 'index.html', '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- directory listing guard.
			file_put_contents( $dir . '.htaccess', "Require all denied\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- deny direct access; downloads go through the nonce-protected endpoint.
		}
		return $dir;
	}

	/**
	 * Generate (or return existing) documents for an order or refund.
	 *
	 * @param int $order_id  Order id.
	 * @param int $refund_id Refund id (0 = invoice).
	 * @return array|WP_Error Document row.
	 */
	public function generate( $order_id, $refund_id = 0 ) {
		global $wpdb;
		$table = $wpdb->prefix . 'fepw_documents';

		$existing = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table; issued documents are immutable, caching would only add staleness risk.
			$wpdb->prepare(
				"SELECT * FROM {$table} WHERE order_id = %d AND refund_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table.
				$order_id,
				$refund_id
			),
			ARRAY_A
		);
		if ( $existing ) {
			return $existing; // Immutable once issued (legal numbering).
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return new WP_Error( 'fepw_order', 'Order not found.' );
		}

		$doc_type = $refund_id ? 'credit_note' : 'invoice';
		$refund   = $refund_id ? wc_get_order( $refund_id ) : null;

		$ref_number = '';
		if ( $refund_id ) {
			$parent_doc = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table; single immutable-row lookup at generation time.
				$wpdb->prepare(
					"SELECT doc_number FROM {$table} WHERE order_id = %d AND refund_id = 0 LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table.
					$order_id
				),
				ARRAY_A
			);
			$ref_number = $parent_doc ? $parent_doc['doc_number'] : '';
		}

		$number = FEPW_Numbering::next_number( $doc_type, $order_id );
		$data   = FEPW_Builder::assemble( $order, $number, $doc_type, $refund, $ref_number );

		$cii = FEPW_Builder::to_cii( $data );
		$ubl = FEPW_Builder::to_ubl( $data );
		$pdf = FEPW_PDF::render( $data );

		$dir  = self::storage_dir();
		$base = sanitize_file_name( $number );
		file_put_contents( $dir . $base . '-cii.xml', $cii ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- protected uploads dir.
		file_put_contents( $dir . $base . '-ubl.xml', $ubl ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- protected uploads dir.
		if ( ! is_wp_error( $pdf ) ) {
			file_put_contents( $dir . $base . '.pdf', $pdf ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- protected uploads dir.
			$facturx = $this->build_facturx( $pdf, $cii );
			if ( ! is_wp_error( $facturx ) ) {
				file_put_contents( $dir . $base . '-facturx.pdf', $facturx ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- protected uploads dir.
			} else {
				update_option( 'fepw_last_facturx_error', $facturx->get_error_message(), false );
			}
		}

		$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own-table write (the document register).
			$table,
			array(
				'order_id'   => $order_id,
				'refund_id'  => $refund_id,
				'doc_type'   => $doc_type,
				'doc_number' => $number,
				'issued_at'  => current_time( 'mysql', true ),
				'total_ht'   => $data['total_ht'],
				'total_tva'  => $data['total_tva'],
				'total_ttc'  => $data['total_ttc'],
				'status'     => 'generated',
			),
			array( '%d', '%d', '%s', '%s', '%s', '%f', '%f', '%f', '%s' )
		);

		$order->add_order_note(
			sprintf(
				/* translators: 1: document type, 2: number. */
				__( '%1$s %2$s generated (Facture Électronique).', 'buildsbyluke-facture-electronique-for-woocommerce' ),
				'credit_note' === $doc_type ? __( 'Credit note', 'buildsbyluke-facture-electronique-for-woocommerce' ) : __( 'Invoice', 'buildsbyluke-facture-electronique-for-woocommerce' ),
				$number
			)
		);

		/**
		 * Fires after a document is generated. Pro transmission attaches here.
		 *
		 * @param string $number   Document number.
		 * @param string $doc_type Type.
		 * @param int    $order_id Order.
		 * @param array  $data     Assembled data.
		 */
		do_action( 'fepw_document_generated', $number, $doc_type, $order_id, $data );

		return $this->generate( $order_id, $refund_id ); // Return the stored row.
	}

	/**
	 * Assemble Factur-X (PDF/A-3 + embedded CII) via the vendored library,
	 * validating the XML against the bundled official XSD first.
	 *
	 * @param string $pdf Binary PDF.
	 * @param string $cii CII XML.
	 * @return string|WP_Error
	 */
	private function build_facturx( $pdf, $cii ) {
		if ( ! class_exists( 'Atgp\\FacturX\\Writer' ) ) {
			return new WP_Error( 'fepw_facturx', 'Factur-X library unavailable.' );
		}
		try {
			// FPDI needs genuine FPDF 1.8.x (protected internals) as its base for
			// the PDF/A-3 container assembly. The visual PDF itself was rendered
			// with tFPDF (embedded UTF-8 fonts) and is only IMPORTED here — no
			// font is ever loaded in this lane, and FPDF's constructor resolves
			// its bundled font folder (vendor/fpdf/font/) on its own, so the
			// global FPDF_FONTPATH constant is deliberately never defined.
			if ( ! class_exists( 'FPDF', false ) ) {
				require_once FEPW_DIR . 'vendor/fpdf/fpdf.php';
			}
			$writer = new \Atgp\FacturX\Writer();
			return $writer->generate( $pdf, $cii, 'en16931', true ); // true = validate XSD.
		} catch ( Throwable $e ) {
			return new WP_Error( 'fepw_facturx', $e->getMessage() );
		}
	}

	/**
	 * Auto-generate the invoice when an order reaches "completed" (Settings
	 * toggle, default on). Skipped until the seller identity (SIREN) is filled
	 * in: documents are numbered immutably at generation time, so issuing them
	 * with an empty seller block would create unfixable legal records. The
	 * pause is surfaced as a notice on the plugin's own screens.
	 *
	 * @param int $order_id Order id.
	 */
	public function on_order_completed( $order_id ) {
		$s = get_option( 'fepw_settings', array() );
		if ( isset( $s['auto_generate'] ) && 1 !== (int) $s['auto_generate'] ) {
			return; // Merchant switched automatic generation off.
		}
		if ( empty( $s['seller_siren'] ) ) {
			return; // Paused until the seller identity is complete (admin notice explains).
		}
		$this->generate( (int) $order_id );
	}

	/**
	 * Auto-generate the avoir when a refund is created.
	 *
	 * @param int $order_id  Order id.
	 * @param int $refund_id Refund id.
	 */
	public function on_refund( $order_id, $refund_id ) {
		$this->generate( (int) $order_id, (int) $refund_id );
	}

	/**
	 * Admin action: generate invoice for an order.
	 */
	public function handle_generate() {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to do this.', 'buildsbyluke-facture-electronique-for-woocommerce' ) );
		}
		check_admin_referer( 'fepw_generate' );
		$order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
		$result   = $this->generate( $order_id );
		$url      = wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=fepw' );
		if ( is_wp_error( $result ) ) {
			$url = add_query_arg( 'fepw_error', rawurlencode( $result->get_error_message() ), $url );
		}
		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * Nonce-protected document download.
	 */
	public function handle_download() {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to do this.', 'buildsbyluke-facture-electronique-for-woocommerce' ) );
		}
		check_admin_referer( 'fepw_download' );

		$number = isset( $_GET['number'] ) ? sanitize_file_name( wp_unslash( $_GET['number'] ) ) : '';
		$kind   = isset( $_GET['kind'] ) ? sanitize_key( $_GET['kind'] ) : 'facturx';

		$suffix = array(
			'cii'     => '-cii.xml',
			'ubl'     => '-ubl.xml',
			'pdf'     => '.pdf',
			'facturx' => '-facturx.pdf',
		);
		if ( ! isset( $suffix[ $kind ] ) ) {
			wp_die( esc_html__( 'Unknown document kind.', 'buildsbyluke-facture-electronique-for-woocommerce' ) );
		}

		$path = self::storage_dir() . $number . $suffix[ $kind ];
		// storage_dir() is fixed; $number is sanitize_file_name()d — no traversal.
		if ( ! file_exists( $path ) ) {
			wp_die( esc_html__( 'Document not found. Generate it first.', 'buildsbyluke-facture-electronique-for-woocommerce' ) );
		}

		$is_xml = 'cii' === $kind || 'ubl' === $kind;
		nocache_headers();
		header( 'Content-Type: ' . ( $is_xml ? 'application/xml' : 'application/pdf' ) );
		header( 'Content-Disposition: attachment; filename="' . basename( $path ) . '"' );
		header( 'Content-Length: ' . (string) filesize( $path ) );
		readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- controlled binary download.
		exit;
	}

	/**
	 * List documents (for admin table / export).
	 *
	 * @param array $args {from, to} optional filters (Y-m-d).
	 * @return array[]
	 */
	public static function get_documents( $args = array() ) {
		global $wpdb;
		$table = $wpdb->prefix . 'fepw_documents';
		$where = array( '1=1' );
		$prep  = array();
		if ( ! empty( $args['from'] ) ) {
			$where[] = 'issued_at >= %s';
			$prep[]  = $args['from'] . ' 00:00:00';
		}
		if ( ! empty( $args['to'] ) ) {
			$where[] = 'issued_at <= %s';
			$prep[]  = $args['to'] . ' 23:59:59';
		}
		$sql = "SELECT * FROM {$table} WHERE " . implode( ' AND ', $where ) . ' ORDER BY id DESC LIMIT 500';
		if ( $prep ) {
			$sql = $wpdb->prepare( $sql, $prep ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- placeholders built above.
		}
		return (array) $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery -- own table; prepared above when filters present; admin list screens read fresh rows.
	}
}
