<?php

namespace xmvc\daemon;

use xmvc\daemon\lib\RedisDaemon;

/**
 * Redis 异步日志写入文件服务
 */
class LogDaemon extends RedisDaemon
{
    protected $logKeys = []; // 要取 Redis 写日志的配置键
    protected $channel = 'monolog:'; // 频道 也就是键的前缀
    protected $batch = 100; // 每批量最大取多少条
    public function __construct()
    {
        $redisCfg = $this->getLogRedisCfg();
        if (empty($redisCfg)) {
            echo "没有配置 Redis Log\n";
            exit();
        }
        parent::__construct($redisCfg);
    }

    /**
     * 清理资源
     */
    protected function cleanup()
    {
        parent::cleanup();
    }

    /** 获取 Log Redis 的配置信息 */
    public function getLogRedisCfg(): array
    {
        $arr = cfg('log>*');
        $redisCfg = [];
        if ($arr) {
            foreach ($arr as $key => $item) {
                if ($item['dnsRedis']) {
                    $this->logKeys[] = $key;
                    if (empty($redisCfg)) {
                        $dnsRedis = $item['dnsRedis'];
                        $redisCfg = cfg($dnsRedis);
                    }
                }
            }
        }
        return $redisCfg;
    }

    /**
     * 消费 Redis 队列
     */
    protected function run()
    {
        $this->connectRedis();
        while (!self::$stopRequested) {
            foreach ($this->logKeys as $logKey) {
                $logs = $this->tryRedisCommand('lrange', [$this->channel . $logKey, 0, $this->batch - 1]); // 每次取 100 条
                if ($logs) {
                    try {
                        $this->handle($logKey, $logs);
                    } catch (\Throwable $th) {
                        self::error("处理任务 handle 错误: " . $th->getMessage(), __FILE__, __LINE__);
                    }
                }
            }
            // 1000 毫秒（2 秒）到 2000 毫秒（3 秒）之间的随机毫秒数, 多进程时, 间隔不同的时间
            usleep(mt_rand(2000, 3000) * 1000);
            pcntl_signal_dispatch();  // 处理信号
        }
    }

    /**
     * 处理任务
     */
    private function handle(string $logKey, array $logs)
    {
        $cfg = cfg('log>' . $logKey);
        $maxFiles = $cfg['maxFiles'];
        $logPath = DIR_LOG . '/' . $logKey;
        $logFilePrefix = $logPath . "/" . $logKey;
        $logFile = $logFilePrefix . "-" . date('Y-m-d') . '.log';
        if (!is_file($logFile)) {
            if ($maxFiles) {
                $this->clearMonologFile($logFilePrefix, $maxFiles); // 每天清一次
            }
        }
        try {
            $fp = fopen($logFile, 'ab');
        } catch (\Throwable $th) {
            static::error('打开 Monolog 日志文件 [ ' . $logFile . '] 出错, ' . $th->getMessage(), __FILE__, __LINE__);
        }
        try {
            $count = count($logs);
            foreach ($logs as $line) {
                fwrite($fp, $line);
            }
            // 保留索引 $count 至 未尾 的记录, 也就是截断 0 至 $count -1 的记录
            $this->tryRedisCommand('ltrim', [$this->channel . $logKey, $count, -1]);
        } finally {
            if ($fp) {
                fclose($fp);
            }
        }
    }

    /** 清理过期 Monolog 日志文件 */
    protected static function clearMonologFile(string $logFilePrefix, int $logMaxFile)
    {
        try {
            $files = glob($logFilePrefix . '-*.log');
            $now = time();
            foreach ($files as $file) {
                $fileModifiedTime = filemtime($file);
                if (($now - $fileModifiedTime) > ($logMaxFile * 24 * 60 * 60)) {
                    unlink($file);
                }
            }
        } catch (\Throwable $th) {
            self::error('清理过期 Monolog 日志文件出错, ' . $th->getMessage(), __FILE__, __LINE__);
        }
    }
}
