<?php
/**
 * Image service.
 *
 * @package PDFDraft
 */

namespace PDFDraft\Services;

use PDFDraft\Filesystem;
use PDFDraft\Contracts\Services\ImageServiceInterface;
use PDFDraft\Contracts\Log\LoggerInterface;
use PDFDraft\Support\Validation\RequestValidator;

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

/**
 * Service for managing image uploads and storage.
 */
class ImageService implements ImageServiceInterface {

	/**
	 * Upload directory relative path.
	 *
	 * @var string
	 */
	private const UPLOAD_DIR = 'pdfdraft/images';

	/**
	 * Allowed image file extensions.
	 *
	 * @var array
	 */
	private const ALLOWED_EXTENSIONS = array( 'png', 'jpg', 'jpeg', 'webp' );

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

	/**
	 * Constructor.
	 *
	 * @param LoggerInterface $logger Logger implementation.
	 */
	public function __construct(
		LoggerInterface $logger
	) {
		$this->logger = $logger;
	}

	/**
	 * Ensure upload directory exists with proper permissions.
	 *
	 * @return bool Whether the directory exists or was created.
	 */
	private function ensureUploadDirectoryExists(): bool {
		$uploadPath = $this->getUploadPath();

		if ( ! is_dir( $uploadPath ) && ! wp_mkdir_p( $uploadPath ) ) {
			return false;
		}

		$indexFile = $uploadPath . DIRECTORY_SEPARATOR . 'index.php';
		if ( ! file_exists( $indexFile ) ) {
			Filesystem::put_contents( $indexFile, '<?php // Silence is golden.' );
		}

		$this->ensurePreviewImagesDirectoryWebAccessible();

		return true;
	}

	/**
	 * Allow HTTP access to preview files in this directory.
	 *
	 * The parent {@see StorageService::protectDirectory()} may place "deny from all"
	 * on uploads/pdfdraft/, which blocks direct URLs to png/jpeg/webp previews here.
	 * An .htaccess in this folder overrides that for thumbnails only.
	 *
	 * @return void
	 */
	private function ensurePreviewImagesDirectoryWebAccessible(): void {
		$uploadPath = $this->getUploadPath();
		if ( ! is_dir( $uploadPath ) ) {
			return;
		}

		$htaccess = $uploadPath . DIRECTORY_SEPARATOR . '.htaccess';
		if ( is_readable( $htaccess ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$existing = (string) file_get_contents( $htaccess );
			if ( false !== strpos( $existing, 'PDFDraft preview thumbnails' ) ) {
				return;
			}
		}

		$lines   = array(
			'# PDFDraft preview thumbnails — public read access.',
			'# Overrides parent "deny from all" on uploads/pdfdraft/ for this directory only.',
			'<IfModule mod_authz_core.c>',
			"\tRequire all granted",
			'</IfModule>',
			'<IfModule !mod_authz_core.c>',
			"\tOrder Allow,Deny",
			"\tAllow from all",
			'</IfModule>',
		);
		$content = implode( "\n", $lines ) . "\n";
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		file_put_contents( $htaccess, $content );
	}

	/**
	 * Get the full upload directory path.
	 *
	 * @return string Full upload directory path.
	 */
	private function getUploadPath(): string {
		$uploadDir = wp_upload_dir();
		return $uploadDir['basedir'] . DIRECTORY_SEPARATOR . self::UPLOAD_DIR;
	}

	/**
	 * Get the full file path for a basename.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return string Full file path.
	 */
	private function getFilePath( string $basename, string $extension = 'png' ): string {
		$sanitizedBasename = $this->sanitizeBasename( $basename );
		return $this->getUploadPath() . DIRECTORY_SEPARATOR . $sanitizedBasename . '.' . $extension;
	}

	/**
	 * Get the full URL for a basename.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return string Full URL to the file.
	 */
	private function getFileUrl( string $basename, string $extension = 'png' ): string {
		$uploadDir         = wp_upload_dir();
		$sanitizedBasename = $this->sanitizeBasename( $basename );
		return $uploadDir['baseurl'] . '/' . self::UPLOAD_DIR . '/' . $sanitizedBasename . '.' . $extension;
	}

	/**
	 * Sanitize basename to prevent directory traversal and other security issues.
	 *
	 * @param string $basename Filename without extension.
	 * @return string Sanitized basename.
	 */
	private function sanitizeBasename( string $basename ): string {
		$basename = basename( $basename );

		$basename = preg_replace( '/[^a-zA-Z0-9_-]/', '', $basename );

		if ( empty( $basename ) ) {
			$basename = 'image_' . wp_generate_password( 12, false );
		}

		return $basename;
	}

	/**
	 * Save image.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $content   Image content.
	 * @param string $extension File extension (default: png).
	 * @return string|false URL on success or false on failure.
	 */
	public function save( string $basename, string $content, string $extension = 'png' ) {
		if ( empty( $basename ) || empty( $content ) ) {
			$this->logger->warning( 'Invalid image save parameters', array( 'basename' => $basename ) );
			return false;
		}

		$extension = strtolower( $extension );
		if ( ! in_array( $extension, self::ALLOWED_EXTENSIONS, true ) ) {
			$this->logger->warning( 'Invalid image extension', array( 'extension' => $extension ) );
			return false;
		}

		if ( ! $this->ensureUploadDirectoryExists() ) {
			$this->logger->error( 'Failed to create upload directory' );
			return false;
		}

		$filePath = $this->getFilePath( $basename, $extension );
		$result   = Filesystem::put_contents( $filePath, $content );

		if ( false === $result ) {
			$this->logger->error( 'Failed to save image', array( 'filePath' => $filePath ) );
			return false;
		}

		$this->logger->info(
			'Image saved successfully',
			array(
				'basename'  => $basename,
				'extension' => $extension,
			)
		);

		return $this->getFileUrl( $basename, $extension );
	}

	/**
	 * Delete image.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return bool True on success and false on failure.
	 */
	public function delete( string $basename, string $extension = 'png' ): bool {
		if ( empty( $basename ) ) {
			return false;
		}

		$filePath = $this->getFilePath( $basename, $extension );

		if ( ! Filesystem::exists( $filePath ) ) {
			$this->logger->debug( 'Image not found for deletion', array( 'basename' => $basename ) );
			return false;
		}

		$result = Filesystem::delete( $filePath );

		if ( $result ) {
			$this->logger->info( 'Image deleted successfully', array( 'basename' => $basename ) );
		} else {
			$this->logger->error( 'Failed to delete image', array( 'filePath' => $filePath ) );
		}

		return $result;
	}

	/**
	 * Get URL of image.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return string|false URL or false if not found.
	 */
	public function getUrl( string $basename, string $extension = 'png' ) {
		if ( empty( $basename ) ) {
			return false;
		}

		// Try the requested extension first, then fall back to all allowed
		// extensions so that both legacy PNG previews and newer JPEG previews
		// (generated after the thumbnail-size optimisation) are found correctly.
		$candidates = array_unique( array_merge( array( $extension ), self::ALLOWED_EXTENSIONS ) );

		foreach ( $candidates as $ext ) {
			$filePath = $this->getFilePath( $basename, $ext );
			if ( Filesystem::exists( $filePath ) ) {
				$this->ensurePreviewImagesDirectoryWebAccessible();
				return $this->getFileUrl( $basename, $ext );
			}
		}

		return false;
	}

	/**
	 * Check if image exists.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return bool Whether the image exists.
	 */
	public function exists( string $basename, string $extension = 'png' ): bool {
		if ( empty( $basename ) ) {
			return false;
		}

		return Filesystem::exists( $this->getFilePath( $basename, $extension ) );
	}

	/**
	 * Get image size in bytes.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return int|false Size in bytes or false if not found.
	 */
	public function getSize( string $basename, string $extension = 'png' ) {
		$filePath = $this->getFilePath( $basename, $extension );

		if ( ! Filesystem::exists( $filePath ) ) {
			return false;
		}

		return filesize( $filePath );
	}

	/**
	 * Get image modification timestamp.
	 *
	 * @param string $basename  Filename without extension.
	 * @param string $extension File extension (default: png).
	 * @return int|false Unix timestamp or false if not found.
	 */
	public function getModifiedAt( string $basename, string $extension = 'png' ) {
		$filePath = $this->getFilePath( $basename, $extension );
		if ( ! Filesystem::exists( $filePath ) ) {
			return false;
		}
		return filemtime( $filePath );
	}

	/**
	 * Process and save image from base64 data URL.
	 *
	 * @param string $basename Filename without extension.
	 * @param string $dataUrl  Base64 data URL (e.g., data:image/png;base64,...).
	 * @return array{url: string, size: int, modified_at: int}|false Array with image info on success, false on failure.
	 */
	public function saveFromDataUrl( string $basename, string $dataUrl ) {
		try {
			RequestValidator::validateNotEmpty( $basename, 'basename' );
			RequestValidator::validateDataUrl( $dataUrl, 'dataUrl' );
		} catch ( \PDFDraft\Exceptions\ValidationException $e ) {
			$this->logger->warning(
				'Validation failed for saveFromDataUrl',
				array(
					'basename' => $basename,
					'errors'   => $e->getErrors(),
				)
			);
			return false;
		}

		// Extract extension from data URL.
		if ( ! preg_match( '/^data:image\/([a-z]+);base64,/', $dataUrl, $matches ) ) {
			$this->logger->warning( 'Invalid data URL format', array( 'basename' => $basename ) );
			return false;
		}

		$extension = $matches[1] ?? 'png';
		$base64    = preg_replace( '/^data:image\/[a-z]+;base64,/', '', $dataUrl );
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$decoded = base64_decode( $base64, true );

		if ( false === $decoded ) {
			$this->logger->error( 'Failed to decode base64 image', array( 'basename' => $basename ) );
			return false;
		}

		$imageInfo = getimagesizefromstring( $decoded );

		if ( false === $imageInfo ) {
			$this->logger->error( 'Invalid image data', array( 'basename' => $basename ) );
			return false;
		}

		$url = $this->save( $basename, $decoded, $extension );

		if ( false === $url ) {
			$this->logger->error( 'Failed to save image', array( 'basename' => $basename ) );
			return false;
		}

		$size       = $this->getSize( $basename, $extension );
		$modifiedAt = $this->getModifiedAt( $basename, $extension );

		return array(
			'url'         => $url,
			'size'        => false !== $size ? $size : 0,
			'modified_at' => false !== $modifiedAt ? $modifiedAt : time(),
		);
	}

	/**
	 * Store image from request data.
	 *
	 * @param \WP_Post             $post    Post object.
	 * @param array<string, mixed> $request Request data containing image.
	 * @return void
	 */
	public function storeFromRequest( \WP_Post $post, array $request ): void {
		$dataUrl = $request['previewImage'] ?? '';

		if ( ! $dataUrl ) {
			$this->logger->debug( 'No preview image in request', array( 'post_id' => $post->ID ) );
			return;
		}

		$result = $this->saveFromDataUrl( $post->post_name, $dataUrl );

		if ( false === $result ) {
			$this->logger->error(
				'Failed to save preview image',
				array(
					'post_id' => $post->ID,
					'slug'    => $post->post_name,
				)
			);
		} else {
			$this->logger->info(
				'Preview image processed successfully',
				array(
					'post_id' => $post->ID,
					'url'     => $result['url'],
				)
			);
		}
	}
}
