<?php
/**
 * Font Downloader for Maple Local Fonts.
 *
 * @package Maple_Local_Fonts
 */

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

/**
 * Class MLF_Font_Downloader
 *
 * Handles downloading fonts from Google Fonts CSS2 API.
 * Attempts variable fonts first, falls back to static fonts.
 */
class MLF_Font_Downloader {

    /**
     * User agent to send with requests (needed to get WOFF2 format).
     *
     * @var string
     */
    private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

    /**
     * Common font weights to request.
     *
     * Most fonts support at least 300-700. We also include 800-900 for fonts
     * that support Black/Heavy weights.
     *
     * @var array
     */
    private $all_weights = [300, 400, 500, 600, 700, 800, 900];

    /**
     * Download a font from Google Fonts.
     *
     * Attempts variable font first, falls back to static if not available.
     *
     * @param string $font_name     Font family name.
     * @param bool   $include_italic Whether to include italic styles.
     * @return array|WP_Error Download result or error.
     */
    public function download($font_name, $include_italic = true) {
        // Validate font name
        if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
            return new WP_Error('invalid_name', 'Invalid font name');
        }

        if (strlen($font_name) > 100) {
            return new WP_Error('invalid_name', 'Font name too long');
        }

        // Try variable font first
        $result = $this->try_variable_font($font_name, $include_italic);

        if (!is_wp_error($result)) {
            return $result;
        }

        // Fall back to static fonts
        return $this->download_static_fonts($font_name, $include_italic);
    }

    /**
     * Attempt to download variable font.
     *
     * @param string $font_name     Font family name.
     * @param bool   $include_italic Whether to include italic styles.
     * @return array|WP_Error Download result or error.
     */
    private function try_variable_font($font_name, $include_italic) {
        $font_slug = sanitize_title($font_name);
        $downloaded = [];

        // Try to fetch variable font CSS (roman/upright)
        $css = $this->fetch_variable_css($font_name, false);

        if (is_wp_error($css)) {
            return $css;
        }

        // Parse and download roman variable font
        $roman_faces = $this->parse_variable_css($css, $font_name);

        if (is_wp_error($roman_faces) || empty($roman_faces)) {
            return new WP_Error('no_variable', 'Variable font not available');
        }

        // Download roman variable font file(s)
        foreach ($roman_faces as $face) {
            $result = $this->download_single_file(
                $face['url'],
                $font_slug,
                $face['weight'],
                'normal',
                true // is_variable
            );

            if (!is_wp_error($result)) {
                $downloaded[] = [
                    'path'        => $result,
                    'weight'      => $face['weight'],
                    'style'       => 'normal',
                    'is_variable' => true,
                ];
            }
        }

        // Try italic variable font if requested
        if ($include_italic) {
            $italic_css = $this->fetch_variable_css($font_name, true);

            if (!is_wp_error($italic_css)) {
                $italic_faces = $this->parse_variable_css($italic_css, $font_name);

                if (!is_wp_error($italic_faces) && !empty($italic_faces)) {
                    foreach ($italic_faces as $face) {
                        $result = $this->download_single_file(
                            $face['url'],
                            $font_slug,
                            $face['weight'],
                            'italic',
                            true
                        );

                        if (!is_wp_error($result)) {
                            $downloaded[] = [
                                'path'        => $result,
                                'weight'      => $face['weight'],
                                'style'       => 'italic',
                                'is_variable' => true,
                            ];
                        }
                    }
                }
            }
        }

        if (empty($downloaded)) {
            return new WP_Error('download_failed', 'Could not download variable font files');
        }

        return [
            'font_name'   => $font_name,
            'font_slug'   => $font_slug,
            'files'       => $downloaded,
            'is_variable' => true,
        ];
    }

    /**
     * Download static fonts (fallback when variable not available).
     *
     * Tries different weight combinations since fonts support varying weights.
     *
     * @param string $font_name     Font family name.
     * @param bool   $include_italic Whether to include italic styles.
     * @return array|WP_Error Download result or error.
     */
    private function download_static_fonts($font_name, $include_italic) {
        $styles = $include_italic ? ['normal', 'italic'] : ['normal'];

        // Try different weight combinations - fonts support varying weights
        $weight_sets = [
            [300, 400, 500, 600, 700, 800, 900],  // Full range
            [300, 400, 500, 600, 700, 800],       // Without 900
            [300, 400, 500, 600, 700],            // Common range
            [400, 500, 600, 700],                 // Without light
            [400, 700],                           // Just regular and bold
            [400],                                // Single weight (display fonts)
        ];

        $css = null;
        foreach ($weight_sets as $weights) {
            $css = $this->fetch_static_css($font_name, $weights, $styles);

            if (!is_wp_error($css)) {
                break;
            }

            // If error is not "font not found", stop trying
            if ($css->get_error_code() !== 'font_not_found') {
                return $css;
            }
        }

        if (is_wp_error($css)) {
            return $css;
        }

        // Parse CSS to get font face data
        $font_faces = $this->parse_static_css($css, $font_name);

        if (is_wp_error($font_faces)) {
            return $font_faces;
        }

        // Download each font file
        $font_slug = sanitize_title($font_name);
        $downloaded = $this->download_files($font_faces, $font_slug);

        if (is_wp_error($downloaded)) {
            return $downloaded;
        }

        return [
            'font_name'   => $font_name,
            'font_slug'   => $font_slug,
            'files'       => $downloaded,
            'is_variable' => false,
        ];
    }

    /**
     * Fetch variable font CSS from Google Fonts API.
     *
     * Tries progressively smaller weight ranges until one works,
     * since different fonts support different weight ranges.
     *
     * @param string $font_name Font family name.
     * @param bool   $italic    Whether to fetch italic variant.
     * @return string|WP_Error CSS content or error.
     */
    private function fetch_variable_css($font_name, $italic = false) {
        $family = str_replace(' ', '+', $font_name);

        // Try different weight ranges - fonts support varying ranges
        // Also try single weight 400 for fonts that only have one weight
        $weight_ranges = ['300..900', '300..800', '300..700', '400..700', '400'];

        foreach ($weight_ranges as $range) {
            if ($italic) {
                $url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,{$range}&display=swap";
            } else {
                $url = "https://fonts.googleapis.com/css2?family={$family}:wght@{$range}&display=swap";
            }

            $result = $this->fetch_css($url);

            // If successful or error is not "font not found", return
            if (!is_wp_error($result) || $result->get_error_code() !== 'font_not_found') {
                return $result;
            }
        }

        // All ranges failed
        return new WP_Error('no_variable', 'Variable font not available');
    }

    /**
     * Fetch static font CSS from Google Fonts API.
     *
     * @param string $font_name Font family name.
     * @param array  $weights   Weights to fetch.
     * @param array  $styles    Styles to fetch.
     * @return string|WP_Error CSS content or error.
     */
    private function fetch_static_css($font_name, $weights, $styles) {
        $url = $this->build_static_url($font_name, $weights, $styles);
        return $this->fetch_css($url);
    }

    /**
     * Fetch CSS from a Google Fonts URL.
     *
     * @param string $url Google Fonts CSS URL.
     * @return string|WP_Error CSS content or error.
     */
    private function fetch_css($url) {
        // Validate URL
        if (!$this->is_valid_google_fonts_url($url)) {
            return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
        }

        $response = wp_remote_get($url, [
            'timeout'    => MLF_REQUEST_TIMEOUT,
            'sslverify'  => true,
            'user-agent' => $this->user_agent,
        ]);

        if (is_wp_error($response)) {
            return new WP_Error('request_failed', $response->get_error_message());
        }

        $status = wp_remote_retrieve_response_code($response);

        if ($status === 400) {
            return new WP_Error('font_not_found', 'Font not found');
        }

        if ($status !== 200) {
            return new WP_Error('http_error', 'HTTP ' . $status);
        }

        $css = wp_remote_retrieve_body($response);

        if (empty($css)) {
            return new WP_Error('empty_response', 'Empty response from Google Fonts');
        }

        // Check CSS response size
        $max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
        if (strlen($css) > $max_size) {
            return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
        }

        // Verify we got WOFF2
        if (strpos($css, '.woff2)') === false) {
            return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
        }

        return $css;
    }

    /**
     * Build static font URL.
     *
     * @param string $font_name Font family name.
     * @param array  $weights   Array of weights.
     * @param array  $styles    Array of styles.
     * @return string Google Fonts CSS2 URL.
     */
    private function build_static_url($font_name, $weights, $styles) {
        $family = str_replace(' ', '+', $font_name);
        sort($weights);

        $has_italic = in_array('italic', $styles, true);
        $has_normal = in_array('normal', $styles, true);

        if ($has_normal && !$has_italic) {
            $wght = implode(';', $weights);
            return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
        }

        $variations = [];
        foreach ($weights as $weight) {
            if ($has_normal) {
                $variations[] = "0,{$weight}";
            }
            if ($has_italic) {
                $variations[] = "1,{$weight}";
            }
        }

        $variation_string = implode(';', $variations);
        return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
    }

    /**
     * Parse variable font CSS.
     *
     * @param string $css       CSS content.
     * @param string $font_name Expected font family name.
     * @return array|WP_Error Array of font face data or error.
     */
    private function parse_variable_css($css, $font_name) {
        $font_faces = [];

        // Match all @font-face blocks
        $pattern = '/@font-face\s*\{([^}]+)\}/s';
        if (!preg_match_all($pattern, $css, $matches)) {
            return new WP_Error('parse_failed', 'No @font-face rules found');
        }

        foreach ($matches[1] as $block) {
            $face_data = $this->parse_font_face_block($block, true);

            if (is_wp_error($face_data)) {
                continue;
            }

            // Verify font family matches
            if (strcasecmp($face_data['family'], $font_name) !== 0) {
                continue;
            }

            // For variable fonts, prefer latin subset
            $key = $face_data['weight'] . '-' . $face_data['style'];
            $is_latin = $this->is_latin_subset($face_data['unicode_range']);

            if (!isset($font_faces[$key]) || $is_latin) {
                $font_faces[$key] = $face_data;
            }
        }

        return array_values($font_faces);
    }

    /**
     * Parse static font CSS.
     *
     * @param string $css       CSS content.
     * @param string $font_name Expected font family name.
     * @return array|WP_Error Array of font face data or error.
     */
    private function parse_static_css($css, $font_name) {
        $font_faces = [];

        $pattern = '/@font-face\s*\{([^}]+)\}/s';
        if (!preg_match_all($pattern, $css, $matches)) {
            return new WP_Error('parse_failed', 'No @font-face rules found');
        }

        foreach ($matches[1] as $block) {
            $face_data = $this->parse_font_face_block($block, false);

            if (is_wp_error($face_data)) {
                continue;
            }

            if (strcasecmp($face_data['family'], $font_name) !== 0) {
                continue;
            }

            $key = $face_data['weight'] . '-' . $face_data['style'];
            $is_latin = $this->is_latin_subset($face_data['unicode_range']);

            if (!isset($font_faces[$key]) || $is_latin) {
                $font_faces[$key] = $face_data;
            }
        }

        if (empty($font_faces)) {
            return new WP_Error('no_fonts', 'No valid font faces found');
        }

        // Limit number of font faces
        $max_faces = defined('MLF_MAX_FONT_FACES') ? MLF_MAX_FONT_FACES : 20;
        $result = array_values($font_faces);

        if (count($result) > $max_faces) {
            $result = array_slice($result, 0, $max_faces);
        }

        return $result;
    }

    /**
     * Parse a single @font-face block.
     *
     * @param string $block       Content inside @font-face { }.
     * @param bool   $is_variable Whether this is a variable font.
     * @return array|WP_Error Parsed data or error.
     */
    private function parse_font_face_block($block, $is_variable = false) {
        $data = [];

        // Extract font-family
        if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) {
            $data['family'] = trim($m[1]);
        } else {
            return new WP_Error('missing_family', 'Missing font-family');
        }

        // Extract font-weight (can be single value or range for variable)
        if (preg_match('/font-weight:\s*(\d+(?:\s+\d+)?);/i', $block, $m)) {
            $data['weight'] = trim($m[1]);
        } else {
            return new WP_Error('missing_weight', 'Missing font-weight');
        }

        // Extract font-style
        if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
            $data['style'] = $m[1];
        } else {
            $data['style'] = 'normal';
        }

        // Extract src URL - MUST be fonts.gstatic.com
        if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) {
            $data['url'] = $m[1];
        } else {
            return new WP_Error('missing_src', 'Missing or invalid src URL');
        }

        // Extract unicode-range
        if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
            $data['unicode_range'] = trim($m[1]);
        } else {
            $data['unicode_range'] = '';
        }

        return $data;
    }

    /**
     * Check if unicode-range indicates latin subset.
     *
     * @param string $range Unicode range string.
     * @return bool True if appears to be latin subset.
     */
    private function is_latin_subset($range) {
        if (empty($range)) {
            return true;
        }

        if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) {
            return true;
        }

        return false;
    }

    /**
     * Download all font files.
     *
     * @param array  $font_faces Array of font face data.
     * @param string $font_slug  Font slug for filename.
     * @return array|WP_Error Array of downloaded file info or error.
     */
    private function download_files($font_faces, $font_slug) {
        $downloaded = [];
        $errors = [];

        foreach ($font_faces as $face) {
            $result = $this->download_single_file(
                $face['url'],
                $font_slug,
                $face['weight'],
                $face['style'],
                false
            );

            if (is_wp_error($result)) {
                $errors[] = $result->get_error_message();
                continue;
            }

            $downloaded[] = [
                'path'        => $result,
                'weight'      => $face['weight'],
                'style'       => $face['style'],
                'is_variable' => false,
            ];
        }

        if (empty($downloaded)) {
            return new WP_Error(
                'download_failed',
                'Could not download any font files: ' . implode(', ', $errors)
            );
        }

        return $downloaded;
    }

    /**
     * Download a single WOFF2 file.
     *
     * @param string $url         Google Fonts static URL.
     * @param string $font_slug   Font slug for filename.
     * @param string $weight      Font weight (single or range).
     * @param string $style       Font style.
     * @param bool   $is_variable Whether this is a variable font.
     * @return string|WP_Error Local file path or error.
     */
    private function download_single_file($url, $font_slug, $weight, $style, $is_variable = false) {
        if (!$this->is_valid_google_fonts_url($url)) {
            return new WP_Error('invalid_url', 'URL is not from Google Fonts');
        }

        // Build filename
        $weight_slug = str_replace(' ', '-', $weight);
        if ($is_variable) {
            $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
        } else {
            $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight_slug);
        }
        $filename = sanitize_file_name($filename);

        $filename = $this->sanitize_font_filename($filename);
        if ($filename === false) {
            return new WP_Error('invalid_filename', 'Invalid filename');
        }

        $font_dir = wp_get_font_dir();
        $destination = trailingslashit($font_dir['path']) . $filename;

        if (!$this->validate_font_path($destination)) {
            return new WP_Error('invalid_path', 'Invalid destination path');
        }

        if (!wp_mkdir_p($font_dir['path'])) {
            return new WP_Error('mkdir_failed', 'Could not create fonts directory');
        }

        $response = wp_remote_get($url, [
            'timeout'    => MLF_REQUEST_TIMEOUT,
            'sslverify'  => true,
            'user-agent' => $this->user_agent,
        ]);

        if (is_wp_error($response)) {
            return new WP_Error('download_failed', 'Failed to download: ' . $response->get_error_message());
        }

        $status = wp_remote_retrieve_response_code($response);
        if ($status !== 200) {
            return new WP_Error('http_error', 'Download returned HTTP ' . $status);
        }

        $content = wp_remote_retrieve_body($response);
        if (empty($content)) {
            return new WP_Error('empty_file', 'Downloaded file is empty');
        }

        $max_size = defined('MLF_MAX_FONT_FILE_SIZE') ? MLF_MAX_FONT_FILE_SIZE : 5 * 1024 * 1024;
        if (strlen($content) > $max_size) {
            return new WP_Error('file_too_large', 'Font file exceeds maximum size');
        }

        // Verify WOFF2 magic bytes
        if (substr($content, 0, 4) !== 'wOF2') {
            return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2');
        }

        global $wp_filesystem;
        if (empty($wp_filesystem)) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }

        if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
            return new WP_Error('write_failed', 'Could not write font file');
        }

        return $destination;
    }

    /**
     * Validate Google Fonts URL.
     *
     * @param string $url URL to validate.
     * @return bool True if valid.
     */
    private function is_valid_google_fonts_url($url) {
        $parsed = wp_parse_url($url);

        if (!$parsed || !isset($parsed['host'])) {
            return false;
        }

        $allowed_hosts = [
            'fonts.googleapis.com',
            'fonts.gstatic.com',
        ];

        return in_array($parsed['host'], $allowed_hosts, true);
    }

    /**
     * Sanitize font filename.
     *
     * @param string $filename Filename to sanitize.
     * @return string|false Sanitized filename or false.
     */
    private function sanitize_font_filename($filename) {
        $filename = sanitize_file_name($filename);

        if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
            return false;
        }

        if ($filename !== basename($filename)) {
            return false;
        }

        if (strlen($filename) > 200) {
            return false;
        }

        return $filename;
    }

    /**
     * Validate font path is within fonts directory.
     *
     * @param string $path Path to validate.
     * @return bool True if valid.
     */
    private function validate_font_path($path) {
        $font_dir = wp_get_font_dir();
        $fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));

        $real_path = realpath($path);

        if ($real_path === false) {
            $dir = dirname($path);
            $real_dir = realpath($dir);
            if ($real_dir === false) {
                $parent_dir = dirname($dir);
                $real_parent = realpath($parent_dir);
                if ($real_parent === false) {
                    return false;
                }
                $real_path = wp_normalize_path($real_parent . '/' . basename($dir) . '/' . basename($path));
            } else {
                $real_path = wp_normalize_path($real_dir . '/' . basename($path));
            }
        } else {
            $real_path = wp_normalize_path($real_path);
        }

        return strpos($real_path, $fonts_path) === 0;
    }
}
