<?php
/**
 * Fonts service implementation.
 *
 * @package PDFDraft
 */

namespace PDFDraft\Services;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use JsonMachine\Items;
use WP_Error;
use FuzzyWuzzy\Fuzz;
use PDFDraft\Contracts\Services\FontsServiceInterface;
use PDFDraft\Contracts\Log\LoggerInterface;
use PDFDraft\Traits\Hooks;

/**
 * Service for managing Google Fonts data.
 */
class FontsService implements FontsServiceInterface {

	use Hooks;

	/**
	 * Default font data path.
	 *
	 * @var string
	 */
	private string $defaultFontDataPath;

	/**
	 * Logger implementation.
	 *
	 * @var LoggerInterface
	 */
	private $logger;

	/**
	 * Constructor.
	 *
	 * @param LoggerInterface $logger Logger implementation.
	 */
	public function __construct( LoggerInterface $logger ) {
		$this->logger              = $logger;
		$this->defaultFontDataPath = PDFDRAFT_PLUGIN_DIR . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'google-fonts.json';
	}

	/**
	 * Get a list of fonts with optional search and pagination.
	 *
	 * @param array       $args         Search and pagination arguments.
	 * @param string|null $fontDataPath Optional custom path to fonts JSON file.
	 * @return array|WP_Error Array of fonts or WP_Error on failure.
	 */
	public function getFonts( array $args = array(), ?string $fontDataPath = null ) {
		$fontDataPath = $fontDataPath ?? $this->defaultFontDataPath;
		$query        = $args['query'] ?? '';
		$limit        = $args['limit'] ?? 30;
		$offset       = max( $args['offset'] ?? 0, 0 );

		if ( ! file_exists( $fontDataPath ) ) {
			$this->logger->error( 'Google Fonts JSON file not found', array( 'path' => $fontDataPath ) );
			return new WP_Error( 'pdfdraft_font_families_json_not_found', 'Google Fonts JSON file not found.', array( 'status' => 404 ) );
		}

		$query            = trim( strtolower( $query ) );
		$queryWords       = ! empty( $query ) ? explode( ' ', $query ) : array();
		$wordCount        = count( $queryWords );
		$hasMultipleWords = $wordCount > 1;

		$fontsGenerator = function () use (
			$fontDataPath,
			$query,
			$limit,
			$offset,
			$queryWords,
			$wordCount,
			$hasMultipleWords
		) {
			$fonts   = Items::fromFile( $fontDataPath );
			$count   = 0;
			$skipped = 0;
			$fuzz    = new Fuzz();

			foreach ( $fonts as $font ) {
				if ( $skipped < $offset ) {
					++$skipped;
					continue;
				}

				if ( $count >= $limit ) {
					break;
				}
				$font        = (array) $font;
				$font['url'] = $this->createGoogleFontsUrl( array( $font ) );

				if ( empty( $query ) ) {
					yield $font;
					++$count;
					continue;
				}

				$fontFamily = strtolower( $font['family'] );

				if ( false !== strpos( $fontFamily, $query ) ) {
					yield $font;
					++$count;
					continue;
				}

				$matchFound = false;

				if ( $hasMultipleWords ) {
					$matchCount = 0;
					foreach ( $queryWords as $word ) {
						if ( ! empty( $word ) && false !== strpos( $fontFamily, $word ) ) {
							++$matchCount;
						}
					}
					if ( $matchCount / $wordCount >= 0.7 ) {
						$matchFound = true;
					}
				}

				if ( ! $matchFound ) {
					$ratio = $fuzz->partialRatio( $fontFamily, $query );
					if ( $ratio >= 70 ) {
						$matchFound = true;
					}
				}

				if ( $matchFound ) {
					yield $font;
					++$count;
				}
			}
		};

		return iterator_to_array( $fontsGenerator(), false );
	}

	/**
	 * Get font data by ID or array of IDs.
	 *
	 * @param string|array $ids          Font ID or array of IDs to retrieve.
	 * @param string|null  $fontDataPath Optional custom path to fonts JSON file.
	 * @return array|WP_Error Font data or WP_Error on failure.
	 */
	public function getFontById( $ids, ?string $fontDataPath = null ) {
		if ( empty( $ids ) ) {
			$this->logger->warning( 'Invalid font ID provided' );
			return new WP_Error( 'pdfdraft_invalid_font_id', 'Invalid font ID.', array( 'status' => 400 ) );
		}
		$ids          = is_string( $ids ) ? array( $ids ) : $ids;
		$fontDataPath = $fontDataPath ?? $this->defaultFontDataPath;

		try {
			if ( ! file_exists( $fontDataPath ) ) {
				return new WP_Error( 'pdfdraft_font_families_json_not_found', 'Google Fonts JSON file not found.', array( 'status' => 404 ) );
			}

			$result = array();
			foreach ( $ids as $id ) {
				try {
					$font     = Items::fromFile(
						$fontDataPath,
						array(
							'pointer' => '/' . $id,
						)
					);
					$r        = iterator_to_array( $font );
					$r['url'] = $this->createGoogleFontsUrl( array( $r ) );
					$result[] = $r;
				} catch ( \Exception $e ) {
					$this->logger->warning(
						'Failed to retrieve font',
						array(
							'id'    => $id,
							'error' => $e->getMessage(),
						)
					);
				}
			}

			return $result;
		} catch ( \Exception $e ) {
			$this->logger->error(
				'Error retrieving font family',
				array(
					'error' => $e->getMessage(),
					'ids'   => $ids,
				)
			);
			return new WP_Error(
				'pdfdraft_font_family_error',
				'Error retrieving font family: ' . $e->getMessage(),
				array( 'status' => 500 )
			);
		}
	}

	/**
	 * Get all available font categories.
	 *
	 * @param string|null $fontDataPath Optional custom path to fonts JSON file.
	 * @return array|WP_Error Array of font categories or WP_Error on failure.
	 */
	public function getFontCategories( ?string $fontDataPath = null ) {
		$fontDataPath = $fontDataPath ?? $this->defaultFontDataPath;

		if ( ! file_exists( $fontDataPath ) ) {
			return new WP_Error( 'pdfdraft_font_families_json_not_found', 'Google Fonts JSON file not found.', array( 'status' => 404 ) );
		}

		try {
			$fonts      = Items::fromFile( $fontDataPath );
			$categories = array();

			foreach ( $fonts as $font ) {
				if ( isset( $font->category ) && ! in_array( $font->category, $categories, true ) ) {
					$categories[] = $font->category;
				}
			}

			sort( $categories );
			return $categories;
		} catch ( \Exception $e ) {
			$this->logger->error( 'Error retrieving font categories', array( 'error' => $e->getMessage() ) );
			return new WP_Error(
				'pdfdraft_font_categories_error',
				'Error retrieving font categories: ' . $e->getMessage(),
				array( 'status' => 500 )
			);
		}
	}

	/**
	 * Create a Google Fonts CSS URL from font data.
	 *
	 * @param array $fontData Array of font data entries.
	 * @return string Google Fonts CSS URL.
	 */
	public function createGoogleFontsUrl( array $fontData ): string {
		$parts = array();
		foreach ( $fontData as $font ) {
			$part     = rawurlencode( $font['family'] );
			$variants = implode( ',', $font['variants'] );

			if ( ! empty( $variants ) ) {
				$part .= ':' . $variants;
			}
			$parts[] = $part;
		}
		return 'https://fonts.googleapis.com/css?family=' . implode( '|', $parts );
	}

	/**
	 * Validate that a variant sequence is consecutive multiples of 100.
	 *
	 * @param array $variants Array of variant weight values.
	 * @return bool True if the sequence is valid.
	 */
	private function validateVariantSequence( array $variants ): bool {
		sort( $variants );
		$len = count( $variants );
		for ( $i = 0; $i < $len - 1; $i++ ) {
			if ( ( $variants[ $i + 1 ] - $variants[ $i ] ) !== 100 ) {
				return false;
			}
		}
		return true;
	}
}
