<?php

namespace CleantalkSP\Common;

use RuntimeException;

/**
 * Abstract base class for logging functionality.
 * Provides core logging structure and methods for record handling,
 * time management, and log limit enforcement.
 */
abstract class Logger
{
    /**
     * @var string Fully qualified class name of the record provider
     */
    protected $record_provider;

    /**
     * @var string Name of the time provider function (e.g., 'time', 'microtime')
     */
    protected $time_provider;

    /**
     * @var int Maximum number of log entries allowed before blocking new writes
     */
    protected static $log_limit = 300;

    /**
     * Is limit reached statically
     * @var bool
     */
    private $limits_reached = false;

    /**
     * Constructor.
     *
     * @param string|LoggerRecord|null $record_provider Record provider class name or instance
     * @param string $time_provider Callable function name that returns a timestamp
     *
     * @throws RuntimeException
     */
    public function __construct($record_provider = null, $time_provider = 'time')
    {
        if ($record_provider === null) {
            $this->record_provider = LoggerRecord::class;
        } elseif ($record_provider instanceof LoggerRecord) {
            $this->record_provider = get_class($record_provider);
        } elseif (
            is_string($record_provider) &&
            class_exists($record_provider) &&
            is_a($record_provider, LoggerRecord::class, true)
        ) {
            $this->record_provider = $record_provider;
        } else {
            throw new RuntimeException('Invalid record provider');
        }

        $this->time_provider = $time_provider;
    }

    /**
     * Persist a log record to storage.
     *
     * @param LoggerRecord $record The log record to store
     * @return void
     * @psalm-suppress PossiblyUnusedMethod
     */
    abstract public static function updateStorage($record);

    /**
     * Retrieve all log records from storage.
     *
     * @return LoggerRecord[] Array of log records
     * @psalm-suppress PossiblyUnusedMethod
     */
    abstract public static function loadStorage();

    /**
     * Remove all log records from storage.
     *
     * @return void
     * @psalm-suppress PossiblyUnusedMethod
     */
    abstract public static function clearStorage();

    /**
     * Instantiate and return a new log record object.
     *
     * @return LoggerRecord New log record instance
     * @throws RuntimeException
     */
    protected function getLogRecordProvider()
    {
        if (!class_exists($this->record_provider)) {
            throw new RuntimeException('Invalid record provider');
        }

        return new $this->record_provider();
    }

    /**
     * Get formatted timestamp using the configured time provider.
     *
     * @return string Formatted date string (Y-m-d H:i:s) or 'invalid time' on error
     * @psalm-suppress PossiblyUnusedMethod
     */
    protected function getTime()
    {
        // Call the configured time function (e.g., 'time', 'microtime')
        if (function_exists($this->time_provider)) {
            $timestamp = $this->time_provider === 'microtime'
                ? microtime(true)
                : call_user_func($this->time_provider);
        } else {
            $timestamp = time();
        }

        // Convert integer timestamp to readable format
        $time = null;

        if (is_int($timestamp)) {
            $time = date('Y-m-d H:i:s', $timestamp);
        } elseif (is_float($timestamp)) {
            $time = date('Y-m-d H:i:s', (int)$timestamp);
        }

        // Fallback for invalid timestamp values
        if (!is_string($time)) {
            $time = 'invalid time';
        }

        return $time;
    }

    /**
     * Write a log entry to storage.
     * Respects the log limit - if limit is reached, no new entry is written.
     *
     * @param mixed $msg Main log message (will be JSON encoded)
     * @param mixed $object Optional additional data to append to the message (will be JSON encoded)
     * @return void
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function writeLog($msg, $object = null)
    {
        try {
            // Do not write if log limit has been reached
            if ($this->limits_reached || $this->logLimitReached()) {
                $this->limits_reached = true;
                throw new \Exception('Limit reached.');
            }

            // Encode main message with error suppression
            $msg = is_string($msg) ? $msg : @json_encode($msg);
            if (false === $msg) {
                $msg = 'JSON_ENCODE_ERROR';
            }

            // Create and populate log record
            $record = $this->getLogRecordProvider();
            $record->time = $this->getTime();

            // Encode additional data if provided
            if (null !== $object) {
                $object = @json_encode($object);
                if (false === $object) {
                    $object = 'JSON_ENCODE_ERROR';
                }
            }

            // Assemble final message
            $record->message = $msg;
            $record->message .= !empty($object) ? ', additional data: ' . $object : '';

            // Delegate storage to concrete implementation
            static::updateStorage($record);
        } catch (\Exception $e) {
            // do nothing
        }
    }

    /**
     * Retrieve all logs as an array of formatted strings.
     *
     * @return array Array of strings in format "time : message", empty array if no logs
     * @psalm-suppress PossiblyUnusedMethod
     */
    public static function getLogAsStringsArray()
    {
        $storage_log = static::loadStorage();

        // Validate loaded data
        if (empty($storage_log) || !is_array($storage_log)) {
            return [];
        }

        // Format each record
        $out = [];
        foreach ($storage_log as $log_record) {
            if ($log_record instanceof LoggerRecord) {
                $out[] = $log_record->time . ' : ' . $log_record->message;
            } elseif (is_array($log_record) && isset($log_record[0], $log_record[1])) {
                $time = is_int($log_record[0]) ? $log_record[0] : 'unknown time';
                $out[] = $time . ' : ' . $log_record[1]  ? $log_record[1] : 'unknown message';
            } else {
                $out[] = 'logger_error';
            }
        }

        return $out;
    }

    /**
     * Check whether the log storage has reached its maximum capacity.
     *
     * @return bool True if log limit is reached, false otherwise
     */
    private function logLimitReached()
    {
        $log_count = count(static::loadStorage());
        return $log_count >= static::$log_limit;
    }
}
