<?php

namespace Acms\Services\Logger;

use Acms\Contracts\ServiceProvider;
use Acms\Services\Container;
use Acms\Services\Logger\Handler\EmailHandler;
use Acms\Services\Logger\Handler\DatabaseHandler;
use Acms\Services\Logger\Processor\ScrubProcessor;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Processor\WebProcessor;
use Monolog\Processor\MemoryUsageProcessor;
use Monolog\Processor\MemoryPeakUsageProcessor;

class LoggerServiceProvider extends ServiceProvider
{
    /**
     * register service
     *
     * @param \Acms\Services\Container $container
     *
     * @return void
     */
    public function register(Container $container): void
    {
        $logger = new Logger('acms-logger');

        // $_POST・HTTPヘッダ・context/extra・URLクエリのマスクを一元的に担うフィルタ。
        // Monolog プロセッサ（extra/context向け）と各ハンドラ（$_POST・ヘッダ・URL定数向け）の
        // 両方から同一インスタンスを参照させ、マスク対象リストを一箇所に保つ。
        $filter = new Filter([
            'password',
            'passwd',
            'pass',
            'retype_pass',
            'code',
            'recovery',
            'takeover',
            'token',
            'api_key',
            'secret',
            'formUniqueToken',
            'formToken',
        ], '***MASKED***');

        // Why not: Monolog\Logger::pushProcessor() は array_unshift() で先頭に積むため、
        // addRecord() での実行順は「後に push したものが先に走る」。ScrubProcessor は
        // WebProcessor が extra.url / extra.referrer に注入する htmlspecialchars 前の生の
        // REQUEST_URI / HTTP_REFERER をスクラブする必要があるため、WebProcessor より後で
        // 実行されなければならない＝ WebProcessor より先に push する。Logger::addRecord() は
        // 全プロセッサ通過後の同一レコードを全ハンドラへ配布するため、ここで一度スクラブすれば
        // DatabaseHandler / EmailHandler / RotatingFileHandler すべてに効く。
        $logger->pushProcessor(new ScrubProcessor($filter));
        $logger->pushProcessor(new WebProcessor());
        $logger->pushProcessor(new MemoryUsageProcessor());
        $logger->pushProcessor(new MemoryPeakUsageProcessor());

        // 監査ログ ID をハンドラ間で共有するコンテキスト。
        // DatabaseHandler が採番した ID を EmailHandler が参照し、通知メールのリンクに付与する。
        $auditLogContext = new AuditLogContext();

        // Rotating file handler
        if (defined('ERROR_LOG_FILE') && ERROR_LOG_FILE) {
            $path = SCRIPT_DIR . ERROR_LOG_FILE;
            $maxFiles = intval(env('LOGGER_ROTATING_MAX_FILES', '60'));
            $this->setHandler($logger, new RotatingFileHandler($path, $maxFiles, Logger::NOTICE));
        }

        // E-mail handler
        $reportingLevel = env('ALERT_REPORTING_LEVEL', 'WARNING');
        if ($reportingLevelValue = Level::getLevelValue($reportingLevel)) {
            $emailHandler = new EmailHandler($reportingLevelValue);
            $emailHandler->setAuditLogContext($auditLogContext);
            $this->setHandler($logger, $emailHandler);
        }

        // Database handler
        $databaseHandler = new DatabaseHandler(Logger::DEBUG);
        $databaseHandler->setAuditLogContext($auditLogContext);
        $this->setHandler($logger, $databaseHandler);

        // bind
        $container->instance('acms-logger.audit-context', $auditLogContext);

        $container->singleton('acms-logger', function () use ($logger) {
            return $logger;
        });

        $container->singleton('acms-logger-filter', function () use ($filter) {
            return $filter;
        });

        $container->singleton('acms-logger-repository', Repository::class);
    }

    /**
     * initialize service
     *
     * @return void
     */
    public function init(): void
    {
    }

    /**
     * ハンドラーをロガーにセット
     * @param \Monolog\Logger $logger
     * @param \Monolog\Handler\AbstractProcessingHandler $handler
     * @return void
     */
    protected function setHandler($logger, $handler): void
    {
        $formatter = new LineFormatter();
        $formatter->includeStacktraces(true);
        $logger->pushHandler($handler->setFormatter($formatter));
    }
}
