<?php

namespace Kitgenix\PDF_Invoicing\Modules\Invoicing;

defined( 'ABSPATH' ) || exit;

/**
 * Custom document font uploads.
 *
 * Deliberately narrow attack surface:
 *  - Only .ttf/.otf/.woff accepted – extension AND binary signature must
 *    both match (a renamed .php with a .ttf extension is rejected because
 *    its first bytes won't match any known font signature).
 *  - Files are stored inside the plugin's existing uploads sub-directory –
 *    the same directory Dompdf's chroot already covers – never outside it.
 *  - No remote URL field exists anywhere in this class or its admin UI.
 *    Fonts can only be added by uploading a local file through this site's
 *    own wp-admin, the same trust boundary as any other media upload.
 *  - The fonts directory gets the same index.php/.htaccess hardening as
 *    the archive directory (see PdfGenerator::harden_archive_directory()).
 */
final class FontManager {

    private const OPTION_KEY = 'kitgenix_pdf_invoicing_for_woocommerce_custom_fonts';

    private const ALLOWED_EXTENSIONS = [ 'ttf', 'otf', 'woff' ];

    public static function get_fonts_directory(): ?string {
        $upload_dir = wp_upload_dir();
        if ( empty( $upload_dir['basedir'] ) || ! is_string( $upload_dir['basedir'] ) ) {
            return null;
        }

        return trailingslashit( $upload_dir['basedir'] ) . 'kitgenix-pdf-invoicing-for-woocommerce' . DIRECTORY_SEPARATOR . 'fonts';
    }

    /**
     * @return array<string,array{family:string,style:string,weight:string,filename:string,uploaded_at:string}>
     */
    public static function get_fonts(): array {
        $fonts = get_option( self::OPTION_KEY, [] );

        return is_array( $fonts ) ? $fonts : [];
    }

    /**
     * @return array<string,string> family => family (for a settings dropdown)
     */
    public static function get_font_choices(): array {
        $choices = [ '' => __( 'Default (template stylesheet)', 'kitgenix-pdf-invoicing-for-woocommerce' ) ];

        foreach ( self::get_fonts() as $font ) {
            $family = (string) ( $font['family'] ?? '' );
            if ( '' !== $family ) {
                $choices[ $family ] = $family;
            }
        }

        return $choices;
    }

    /**
     * Validate + store an uploaded font file. Returns the font family name
     * on success, or a WP_Error describing why it was rejected.
     *
     * @param array{name:string,tmp_name:string,size:int} $file A single $_FILES entry.
     * @return string|\WP_Error
     */
    public static function handle_upload( array $file, string $family_name ) {
        $family_name = trim( sanitize_text_field( $family_name ) );
        if ( '' === $family_name ) {
            return new \WP_Error( 'kitgenix_font_family_required', __( 'Enter a font family name.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        if ( empty( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
            return new \WP_Error( 'kitgenix_font_upload_missing', __( 'Choose a font file to upload.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        $original_name = isset( $file['name'] ) ? (string) $file['name'] : '';
        $extension     = strtolower( (string) pathinfo( $original_name, PATHINFO_EXTENSION ) );

        if ( ! in_array( $extension, self::ALLOWED_EXTENSIONS, true ) ) {
            return new \WP_Error( 'kitgenix_font_bad_extension', __( 'Only .ttf, .otf, or .woff font files are allowed.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        $max_bytes = 5 * MB_IN_BYTES;
        if ( ! empty( $file['size'] ) && (int) $file['size'] > $max_bytes ) {
            return new \WP_Error( 'kitgenix_font_too_large', __( 'Font files must be 5MB or smaller.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        $detected_type = self::detect_font_type( (string) $file['tmp_name'] );
        if ( null === $detected_type || $detected_type !== $extension ) {
            return new \WP_Error(
                'kitgenix_font_signature_mismatch',
                __( 'That file does not look like a valid font of the type its extension claims. Upload the original, unmodified font file.', 'kitgenix-pdf-invoicing-for-woocommerce' )
            );
        }

        $fonts_dir = self::get_fonts_directory();
        if ( null === $fonts_dir || ! wp_mkdir_p( $fonts_dir ) ) {
            return new \WP_Error( 'kitgenix_font_dir_failed', __( 'Could not create the fonts storage directory.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        self::harden_directory( $fonts_dir );

        $family_slug   = sanitize_key( str_replace( ' ', '-', $family_name ) );
        $safe_filename = sanitize_file_name( $family_slug . '-' . wp_generate_password( 8, false, false ) . '.' . $extension );
        $target_path   = trailingslashit( $fonts_dir ) . $safe_filename;

        /* phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_copy -- Validated (extension + binary signature + size) local font upload into a chroot-covered, hardened plugin directory. */
        if ( ! copy( $file['tmp_name'], $target_path ) ) {
            return new \WP_Error( 'kitgenix_font_copy_failed', __( 'Could not save the uploaded font file.', 'kitgenix-pdf-invoicing-for-woocommerce' ) );
        }

        $fonts                 = self::get_fonts();
        $fonts[ $family_slug ] = [
            'family'      => $family_name,
            'style'       => 'normal',
            'weight'      => 'normal',
            'filename'    => $safe_filename,
            'uploaded_at' => current_time( 'mysql' ),
        ];

        update_option( self::OPTION_KEY, $fonts, false );

        return $family_name;
    }

    public static function delete_font( string $font_key ): bool {
        $fonts = self::get_fonts();
        if ( ! isset( $fonts[ $font_key ] ) ) {
            return false;
        }

        $fonts_dir = self::get_fonts_directory();
        $filename  = (string) ( $fonts[ $font_key ]['filename'] ?? '' );

        if ( $fonts_dir && '' !== $filename ) {
            $path = trailingslashit( $fonts_dir ) . $filename;
            if ( file_exists( $path ) ) {
                wp_delete_file( $path );
            }
        }

        unset( $fonts[ $font_key ] );
        update_option( self::OPTION_KEY, $fonts, false );

        return true;
    }

    /**
     * Register every uploaded font with Dompdf's font metrics so document
     * CSS can reference the family name directly (no @font-face / remote
     * fetch required – the whole point of registerFont() is that Dompdf
     * reads the font from local disk).
     */
    public static function register_with_dompdf( \Dompdf\Dompdf $dompdf ): void {
        $fonts_dir = self::get_fonts_directory();
        if ( null === $fonts_dir ) {
            return;
        }

        $font_metrics = $dompdf->getFontMetrics();

        foreach ( self::get_fonts() as $font ) {
            $filename = (string) ( $font['filename'] ?? '' );
            $family   = (string) ( $font['family'] ?? '' );
            if ( '' === $filename || '' === $family ) {
                continue;
            }

            $path = trailingslashit( $fonts_dir ) . $filename;
            if ( ! file_exists( $path ) ) {
                continue;
            }

            $font_metrics->registerFont(
                [
                    'family' => $family,
                    'style'  => (string) ( $font['style'] ?? 'normal' ),
                    'weight' => (string) ( $font['weight'] ?? 'normal' ),
                ],
                $path
            );
        }
    }

    /**
     * Sniff the binary signature of the first bytes rather than trusting
     * the filename extension or a browser-supplied MIME type, both of
     * which are attacker-controlled.
     */
    private static function detect_font_type( string $path ): ?string {
        global $wp_filesystem;
        if ( empty( $wp_filesystem ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }

        $contents = $wp_filesystem->get_contents( $path );
        if ( false === $contents ) {
            return null;
        }

        $header = substr( $contents, 0, 8 );

        if ( strlen( $header ) < 4 ) {
            return null;
        }

        $magic4 = substr( $header, 0, 4 );

        // TrueType: 0x00010000, or 'true'/'ttcf' (collection).
        if ( "\x00\x01\x00\x00" === $magic4 || 'true' === $magic4 || 'ttcf' === $magic4 ) {
            return 'ttf';
        }

        // OpenType (CFF outlines).
        if ( 'OTTO' === $magic4 ) {
            return 'otf';
        }

        // WOFF 1.0.
        if ( 'wOFF' === $magic4 ) {
            return 'woff';
        }

        return null;
    }

    private static function harden_directory( string $absolute_directory ): void {
        $index_path = trailingslashit( $absolute_directory ) . 'index.php';
        if ( ! file_exists( $index_path ) ) {
            /* phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Static directory-listing guard. */
            @file_put_contents( $index_path, "<?php\n// Silence is golden.\n" );
        }

        $htaccess_path = trailingslashit( $absolute_directory ) . '.htaccess';
        if ( ! file_exists( $htaccess_path ) ) {
            $rules = "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n"
                . "<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n";
            /* phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Static access-control guard. */
            @file_put_contents( $htaccess_path, $rules );
        }
    }
}
