<?php
/**
 * Storage service for filesystem operations.
 *
 * @package PDFDraft
 */

namespace PDFDraft\Services;

use WP_REST_Response;
use WP_Error;

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

/**
 * Filesystem-style service for a configurable base directory.
 */
class StorageService {

	/**
	 * Default base subdirectory path.
	 *
	 * @var string
	 */
	private const DEFAULT_BASE_SUBPATH = 'pdfdraft';

	/**
	 * Base path (null = use default from wp_upload_dir).
	 *
	 * @var string|null
	 */
	private ?string $basePath;

	/**
	 * Constructor.
	 *
	 * @param string|null $basePath Optional custom base path.
	 */
	public function __construct( ?string $basePath = null ) {
		$this->basePath = $basePath;
	}

	/**
	 * Ensure the storage base directory is protected from direct web access.
	 *
	 * Preview thumbnails live in images/*.png|jpg|webp and must be readable by the browser.
	 * A blanket "deny from all" blocks those URLs (403). We deny everything else under
	 * uploads/pdfdraft/ using mod_rewrite when available.
	 *
	 * @return void
	 */
	public function protectDirectory(): void {
		$base = $this->getBasePath( '' );
		if ( ! wp_mkdir_p( $base ) ) {
			return;
		}

		$htaccess = $base . '.htaccess';
		$rules      = $this->getPdfdraftStorageHtaccessRules();
		if ( ! file_exists( $htaccess ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			file_put_contents( $htaccess, $rules );
		} else {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$current = (string) file_get_contents( $htaccess );
			if ( false !== strpos( $current, 'PDFDraft-htaccess-v2' ) ) {
				// Already using current rules.
			} elseif ( 'deny from all' === trim( str_replace( array( "\r\n", "\r" ), "\n", $current ) ) ) {
				// Migrate legacy blanket deny that broke preview image URLs.
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
				file_put_contents( $htaccess, $rules );
			}
		}

		$index = $base . 'index.php';
		if ( ! file_exists( $index ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			file_put_contents( $index, "<?php\n// Silence is golden.\n" );
		}
	}

	/**
	 * Apache rules: allow only design preview images; forbid other direct requests.
	 *
	 * @return string
	 */
	private function getPdfdraftStorageHtaccessRules(): string {
		$lines   = array(
			'# PDFDraft-htaccess-v2 — block direct access except preview thumbnails.',
			'<IfModule mod_rewrite.c>',
			'RewriteEngine On',
			'# Allow: .../uploads/pdfdraft/images/file.png (also jpg, jpeg, webp)',
			'RewriteCond %{REQUEST_URI} !uploads/pdfdraft/images/[^/]+\.(png|jpe?g|webp)$ [NC]',
			'RewriteRule . - [F,L]',
			'</IfModule>',
			'<IfModule !mod_rewrite.c>',
			'<IfModule mod_authz_core.c>',
			'Require all denied',
			'</IfModule>',
			'<IfModule !mod_authz_core.c>',
			'Deny from all',
			'</IfModule>',
			'</IfModule>',
		);
		return implode( "\n", $lines ) . "\n";
	}

	/**
	 * Get the base path, optionally with a subpath appended.
	 *
	 * @param string $subpath Optional subpath (e.g. 'invoices').
	 * @return string Absolute path, normalized (trailing slash when subpath present).
	 */
	public function getBasePath( string $subpath = '' ): string {
		$default = wp_upload_dir()['basedir'] . '/' . self::DEFAULT_BASE_SUBPATH . '/';
		$base    = null !== $this->basePath && '' !== $this->basePath
			? $this->normalizePath( $this->basePath )
			: $default;

		if ( '' !== $subpath ) {
			$base = $this->normalizePath( $base . $subpath . '/' );
		}

		return apply_filters( 'pdfdraft_storage_service_base_path', $base, $subpath );
	}

	/**
	 * Save string content to a file at the given relative path.
	 *
	 * @param string $relativePath Path relative to base (e.g. 'invoices/Invoice_123.pdf').
	 * @param string $content      File content.
	 * @return bool True on success.
	 */
	public function save( string $relativePath, string $content ): bool {
		$fullPath = $this->getFullPath( $relativePath );
		if ( '' === $fullPath ) {
			return false;
		}
		$dir = dirname( $fullPath );
		if ( ! wp_mkdir_p( $dir ) ) {
			return false;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		return file_put_contents( $fullPath, $content ) !== false;
	}

	/**
	 * Copy a file from source path into storage at the given relative path.
	 *
	 * @param string $sourcePath   Absolute path to the source file (e.g. tmp upload).
	 * @param string $relativePath Path relative to base (e.g. 'invoices/Invoice_123.pdf').
	 * @return true|WP_Error True on success.
	 */
	public function put( string $sourcePath, string $relativePath ) {
		if ( ! is_readable( $sourcePath ) || ! is_file( $sourcePath ) ) {
			return new WP_Error( 'storage_source_invalid', __( 'Source file is missing or not readable.', 'pdfdraft' ), array( 'status' => 400 ) );
		}
		$fullPath = $this->getFullPath( $relativePath );
		if ( '' === $fullPath ) {
			return new WP_Error( 'storage_path_invalid', __( 'Invalid storage path.', 'pdfdraft' ), array( 'status' => 500 ) );
		}
		$dir = dirname( $fullPath );
		if ( ! wp_mkdir_p( $dir ) ) {
			return new WP_Error( 'storage_mkdir_failed', __( 'Could not create storage directory.', 'pdfdraft' ), array( 'status' => 500 ) );
		}
		if ( ! copy( $sourcePath, $fullPath ) ) {
			return new WP_Error( 'storage_put_failed', __( 'Could not save file to storage.', 'pdfdraft' ), array( 'status' => 500 ) );
		}
		return true;
	}

	/**
	 * Check if a file exists at the given relative path.
	 *
	 * @param string $relativePath Path relative to base.
	 * @return bool Whether the file exists and is readable.
	 */
	public function fileExists( string $relativePath ): bool {
		$full = $this->getFullPath( $relativePath );
		return '' !== $full && is_file( $full ) && is_readable( $full );
	}

	/**
	 * Serve a file for download (stream response).
	 *
	 * @param string $relativePath     Path relative to base.
	 * @param string $downloadFilename Filename for Content-Disposition (e.g. 'invoice-123.pdf').
	 * @return WP_REST_Response|WP_Error Response or error.
	 */
	public function serve( string $relativePath, string $downloadFilename ) {
		$fullPath = $this->getFullPath( $relativePath );
		if ( '' === $fullPath || ! is_file( $fullPath ) || ! is_readable( $fullPath ) ) {
			return new WP_Error( 'storage_file_not_found', __( 'File not found.', 'pdfdraft' ), array( 'status' => 404 ) );
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$content = file_get_contents( $fullPath );
		if ( false === $content ) {
			return new WP_Error( 'storage_read_failed', __( 'Could not read file.', 'pdfdraft' ), array( 'status' => 500 ) );
		}
		$safeFilename = sanitize_file_name( $downloadFilename );
		if ( '' === $safeFilename ) {
			$safeFilename = basename( $relativePath );
		}
		$contentType = $this->getContentType( $relativePath );
		$response    = new WP_REST_Response( $content, 200 );
		$response->header( 'Content-Type', $contentType );
		$response->header( 'Content-Disposition', 'attachment; filename="' . $safeFilename . '"' );
		return $response;
	}

	/**
	 * Get the full validated path from a relative path.
	 *
	 * @param string $relativePath Path relative to base.
	 * @return string Full path or empty string on failure.
	 */
	private function getFullPath( string $relativePath ): string {
		$relativePath = trim( $relativePath, "/ \t\n\r\0\x0B" );
		if ( '' === $relativePath || false !== strpos( $relativePath, '..' ) || false !== strpos( $relativePath, "\0" ) ) {
			return '';
		}
		$base = $this->getBasePath( '' );

		// Ensure the base path is within the WordPress uploads directory.
		$uploadsDir = wp_upload_dir()['basedir'];
		$realBase   = realpath( $base ) ?: $base;
		if ( 0 !== strpos( $realBase, realpath( $uploadsDir ) ?: $uploadsDir ) ) {
			return '';
		}

		return $base . $relativePath;
	}

	/**
	 * Normalize a file path to use forward slashes with trailing slash.
	 *
	 * @param string $path File path to normalize.
	 * @return string Normalized path.
	 */
	private function normalizePath( string $path ): string {
		$path = str_replace( array( '\\', '//' ), array( '/', '/' ), trim( $path ) );
		return rtrim( $path, '/' ) . '/';
	}

	/**
	 * Get the content type for a file based on extension.
	 *
	 * @param string $relativePath File path to determine content type for.
	 * @return string MIME content type.
	 */
	private function getContentType( string $relativePath ): string {
		$ext = strtolower( pathinfo( $relativePath, PATHINFO_EXTENSION ) );
		$map = array(
			'pdf' => 'application/pdf',
		);
		return $map[ $ext ] ?? 'application/octet-stream';
	}
}
