<?php
/**
 * Logger implementation.
 *
 * @package PDFDraft
 */

namespace PDFDraft\Support\Log;

use Nette\Utils\FileSystem as UtilsFileSystem;
use PDFDraft\Config;
use PDFDraft\Contracts\Log\LoggerInterface;
use PDFDraft\Filesystem;
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;

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

/**
 * Logger that writes to files, syslog, or error log.
 */
class Logger extends AbstractLogger implements LoggerInterface {
	/**
	 * The log channel configuration.
	 *
	 * @var array
	 */
	protected $config;

	/**
	 * The log level hierarchy.
	 *
	 * @var array
	 */
	protected $levels = array(
		LogLevel::EMERGENCY => 0,
		LogLevel::ALERT     => 1,
		LogLevel::CRITICAL  => 2,
		LogLevel::ERROR     => 3,
		LogLevel::WARNING   => 4,
		LogLevel::NOTICE    => 5,
		LogLevel::INFO      => 6,
		LogLevel::DEBUG     => 7,
	);


	/**
	 * Configuration manager instance.
	 *
	 * @var Config
	 */
	private Config $configManager;

	/**
	 * Constructor.
	 *
	 * @param array|string|Config|null $config Channel configuration array, log level string, Config instance, or null.
	 */
	public function __construct( $config = null ) {
		$this->configManager = new Config();

		// Handle backward compatibility: if string is passed, treat it as log level.
		if ( is_string( $config ) ) {
			$config = array( 'level' => $config );
		}

		// If Config instance is passed, use it.
		if ( $config instanceof Config ) {
			$this->configManager = $config;
			$config              = array();
		}

		$defaultConfig = $this->loadConfig();
		$this->config  = wp_parse_args( $config ?? array(), $defaultConfig );
	}

	/**
	 * Load logging configuration from config file.
	 *
	 * @return array Configuration array.
	 */
	private function loadConfig(): array {
		$config = $this->configManager->all( 'logging' );

		// Fallback to defaults if config file doesn't exist or is empty.
		if ( empty( $config ) ) {
			$upload_dir = wp_upload_dir();
			return array(
				'prefix'   => 'PDFDraft',
				'default'  => 'daily',
				'channels' => array(
					'single'    => array(
						'driver' => 'single',
						'path'   => $upload_dir['basedir'] . '/pdfdraft/logs/pdfdraft.log',
						'level'  => 'debug',
					),
					'daily'     => array(
						'driver' => 'daily',
						'path'   => $upload_dir['basedir'] . '/pdfdraft/logs/pdfdraft.log',
						'level'  => 'debug',
						'days'   => 14,
					),
					'syslog'    => array(
						'driver' => 'syslog',
						'level'  => 'debug',
					),
					'errorlog'  => array(
						'driver' => 'errorlog',
						'level'  => 'debug',
					),
					'null'      => array(
						'driver' => 'null',
					),
					'emergency' => array(
						'driver' => 'single',
						'path'   => $upload_dir['basedir'] . '/pdfdraft/logs/emergency.log',
						'level'  => 'emergency',
					),
				),
				'levels'   => array(
					'emergency' => 0,
					'alert'     => 1,
					'critical'  => 2,
					'error'     => 3,
					'warning'   => 4,
					'notice'    => 5,
					'info'      => 6,
					'debug'     => 7,
				),
			);
		}

		return $config;
	}

	/**
	 * Logs with an arbitrary level.
	 *
	 * @param mixed  $level   Log level.
	 * @param string $message Log message.
	 * @param array  $context Additional context data.
	 * @return void
	 */
	public function log( $level, $message, array $context = array() ): void {
		if ( ! $this->shouldLog( $level ) ) {
			return;
		}

		$message = $this->interpolate( $message, $context );
		$entry   = $this->formatLogEntry( $level, $message, $context );

		$this->write( $entry );
	}

	/**
	 * Check if the given level should be logged.
	 *
	 * @param mixed $level Log level.
	 * @return bool Whether the level should be logged.
	 */
	protected function shouldLog( $level ): bool {
		$configLevel = $this->config['level'] ?? LogLevel::DEBUG;

		if ( ! isset( $this->levels[ $level ] ) || ! isset( $this->levels[ $configLevel ] ) ) {
			return true;
		}

		return $this->levels[ $level ] <= $this->levels[ $configLevel ];
	}

	/**
	 * Interpolate context values into the message placeholders.
	 *
	 * @param string $message Message with placeholders.
	 * @param array  $context Context values.
	 * @return string Interpolated message.
	 */
	protected function interpolate( string $message, array $context = array() ): string {
		$replace = array();
		foreach ( $context as $key => $val ) {
			if ( 'exception' === $key ) {
				continue;
			}
			if ( ! is_array( $val ) && ( ! is_object( $val ) || method_exists( $val, '__toString' ) ) ) {
				$replace[ '{' . $key . '}' ] = $val;
			} elseif ( is_array( $val ) || is_object( $val ) ) {
				$replace[ '{' . $key . '}' ] = wp_json_encode( $val );
			}
		}

		return strtr( $message, $replace );
	}

	/**
	 * Format the log entry.
	 *
	 * @param string $level   Log level.
	 * @param string $message Log message.
	 * @param array  $context Additional context.
	 * @return string Formatted log entry.
	 */
	protected function formatLogEntry( string $level, string $message, array $context = array() ): string {
		$timestamp = current_time( 'Y-m-d H:i:s' );
		$level     = strtoupper( $level );
		$prefix    = $this->config['prefix'] ?? 'PDFDraft';

		$entry = "[{$timestamp}] {$prefix}.{$level}: {$message}";

		if ( isset( $context['exception'] ) && $context['exception'] instanceof \Throwable ) {
			$entry .= $this->formatException( $context['exception'] );
		}

		if ( ! empty( $context ) ) {
			$contextData = $context;
			unset( $contextData['exception'] );
			if ( ! empty( $contextData ) ) {
				$entry .= ' ' . wp_json_encode( $contextData );
			}
		}

		return $entry . PHP_EOL;
	}

	/**
	 * Format exception details.
	 *
	 * @param \Throwable $exception The exception to format.
	 * @return string Formatted exception string.
	 */
	protected function formatException( \Throwable $exception ): string {
		return sprintf(
			"\n%s: %s in %s:%d\nStack trace:\n%s",
			get_class( $exception ),
			$exception->getMessage(),
			$exception->getFile(),
			$exception->getLine(),
			$exception->getTraceAsString()
		);
	}

	/**
	 * Write the log entry to the destination.
	 *
	 * @param string $entry Formatted log entry.
	 * @return void
	 */
	protected function write( string $entry ): void {
		$driver = $this->config['driver'] ?? 'single';

		switch ( $driver ) {
			case 'single':
				$this->writeToFile( $entry, $this->config['path'] ?? '' );
				break;

			case 'daily':
				$path = $this->getDailyLogPath();
				$this->writeToFile( $entry, $path );
				$this->rotateLogs();
				break;

			case 'syslog':
				$this->writeToSyslog( $entry );
				break;

			case 'errorlog':
				$this->writeToErrorLog( $entry );
				break;

			case 'null':
				// Do nothing.
				break;

			default:
				$this->writeToFile( $entry, $this->config['path'] ?? '' );
				break;
		}
	}

	/**
	 * Write log entry to a file.
	 *
	 * @param string $entry Formatted log entry.
	 * @param string $path  File path.
	 * @return void
	 */
	protected function writeToFile( string $entry, string $path ): void {
		if ( empty( $path ) ) {
			return;
		}

		$dir = dirname( $path );
		if ( ! file_exists( $dir ) ) {
			wp_mkdir_p( $dir );
		}

		$existing = Filesystem::get_contents( $path ) ?: '';

		Filesystem::put_contents( $path, $existing . $entry, FS_CHMOD_FILE );
	}

	/**
	 * Get the daily log file path.
	 *
	 * @return string Daily log file path.
	 */
	protected function getDailyLogPath(): string {
		$basePath = $this->config['path'] ?? '';
		$date     = current_time( 'Y-m-d' );

		if ( empty( $basePath ) ) {
			return '';
		}

		$pathInfo  = pathinfo( $basePath );
		$directory = $pathInfo['dirname'];
		$filename  = $pathInfo['filename'];
		$extension = isset( $pathInfo['extension'] ) ? '.' . $pathInfo['extension'] : '';

		return "{$directory}/{$filename}-{$date}{$extension}";
	}

	/**
	 * Rotate old log files.
	 *
	 * @return void
	 */
	protected function rotateLogs(): void {
		$days     = $this->config['days'] ?? 7;
		$basePath = $this->config['path'] ?? '';

		if ( empty( $basePath ) ) {
			return;
		}

		$pathInfo  = pathinfo( $basePath );
		$directory = $pathInfo['dirname'];
		$filename  = $pathInfo['filename'];
		$extension = isset( $pathInfo['extension'] ) ? '.' . $pathInfo['extension'] : '';

		$pattern = "{$directory}/{$filename}-*{$extension}";
		$files   = glob( $pattern );

		if ( ! is_array( $files ) ) {
			return;
		}

		usort(
			$files,
			function ( $a, $b ) {
				return filemtime( $b ) - filemtime( $a );
			}
		);

		$filesToKeep = $days;
		foreach ( array_slice( $files, $filesToKeep ) as $file ) {
			if ( Filesystem::exists( $file ) && Filesystem::is_writable( $file ) ) {
				Filesystem::delete( $file );
			}
		}
	}

	/**
	 * Write to syslog.
	 *
	 * @param string $entry Formatted log entry.
	 * @return void
	 */
	protected function writeToSyslog( string $entry ): void {
		syslog( LOG_INFO, $entry );
	}

	/**
	 * Write to PHP error log.
	 *
	 * @param string $entry Formatted log entry.
	 * @return void
	 */
	protected function writeToErrorLog( string $entry ): void {
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		error_log( $entry );
	}
}
