<?php

/*
 * This file is part of the overtrue/wechat.
 *
 * (c) overtrue <i@overtrue.me>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */
namespace EasyWeChat\Kernel\Log;

use EasyWeChat\Kernel\ServiceContainer;
use InvalidArgumentException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Handler\WhatFailureGroupHandler;
use Monolog\Logger as Monolog;
use Psr\Log\LoggerInterface;
/**
 * Class LogManager.
 *
 * @author overtrue <i@overtrue.me>
 */
class LogManager implements LoggerInterface
{
    /**
     * @var \EasyWeChat\Kernel\ServiceContainer
     */
    protected $app;
    /**
     * The array of resolved channels.
     *
     * @var array
     */
    protected $channels = [];
    /**
     * The registered custom driver creators.
     *
     * @var array
     */
    protected $customCreators = [];
    /**
     * The Log levels.
     *
     * @var array
     */
    protected $levels = ['debug' => Monolog::DEBUG, 'info' => Monolog::INFO, 'notice' => Monolog::NOTICE, 'warning' => Monolog::WARNING, 'error' => Monolog::ERROR, 'critical' => Monolog::CRITICAL, 'alert' => Monolog::ALERT, 'emergency' => Monolog::EMERGENCY];
    /**
     * LogManager constructor.
     *
     * @param \EasyWeChat\Kernel\ServiceContainer $app
     */
    public function __construct(ServiceContainer $app)
    {
        $this->app = $app;
    }
    /**
     * Create a new, on-demand aggregate logger instance.
     *
     * @param array       $channels
     * @param string|null $channel
     *
     * @return \Psr\Log\LoggerInterface
     *
     * @throws \Exception
     */
    public function stack(array $channels, $channel = null)
    {
        return $this->createStackDriver(compact('channels', 'channel'));
    }
    /**
     * Get a log channel instance.
     *
     * @param string|null $channel
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function channel($channel = null)
    {
        return $this->driver($channel);
    }
    /**
     * Get a log driver instance.
     *
     * @param string|null $driver
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function driver($driver = null)
    {
        return $this->get(!empty($driver) ? $driver : $this->getDefaultDriver());
    }
    /**
     * Attempt to get the log from the local cache.
     *
     * @param $name
     *
     * @return \Psr\Log\LoggerInterface
     *
     * @throws \Exception
     */
    protected function get($name)
    {
        try {
            return !empty($this->channels[$name]) ? $this->channels[$name] : ($this->channels[$name] = $this->resolve($name));
        } catch (\Throwable $e) {
            $logger = $this->createEmergencyLogger();
            $logger->emergency('Unable to create configured logger. Using emergency logger.', ['exception' => $e]);
            return $logger;
        }
    }
    /**
     * Resolve the given log instance by name.
     *
     * @param $name
     *
     * @return \Psr\Log\LoggerInterface
     *
     * @throws InvalidArgumentException
     */
    protected function resolve($name)
    {
        $config = $this->app['config']->get(\sprintf('log.channels.%s', $name));
        if (is_null($config)) {
            throw new InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name));
        }
        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($config);
        }
        $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver';
        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        }
        throw new InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver']));
    }
    /**
     * Create an emergency log handler to avoid white screens of death.
     *
     * @return \Monolog\Logger
     *
     * @throws \Exception
     */
    protected function createEmergencyLogger()
    {
        return new Monolog('EasyWeChat', $this->prepareHandlers([new StreamHandler(\sys_get_temp_dir() . '/easywechat/easywechat.log', $this->level(['level' => 'debug']))]));
    }
    /**
     * Call a custom driver creator.
     *
     * @param array $config
     *
     * @return mixed
     */
    protected function callCustomCreator(array $config)
    {
        return $this->customCreators[$config['driver']]($this->app, $config);
    }
    /**
     * Create an aggregate log driver instance.
     *
     * @param array $config
     *
     * @return \Monolog\Logger
     *
     * @throws \Exception
     */
    protected function createStackDriver(array $config)
    {
        $handlers = [];
        foreach (!empty($config['channels']) ? $config['channels'] : [] as $channel) {
            $handlers = \array_merge($handlers, $this->channel($channel)->getHandlers());
        }
        if (isset($config['ignore_exceptions']) ? $config['ignore_exceptions'] : false) {
            $handlers = [new WhatFailureGroupHandler($handlers)];
        }
        return new Monolog($this->parseChannel($config), $handlers);
    }
    /**
     * Create an instance of the single file log driver.
     *
     * @param array $config
     *
     * @return \Psr\Log\LoggerInterface
     *
     * @throws \Exception
     */
    protected function createSingleDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(new StreamHandler($config['path'], $this->level($config), isset($config['bubble']) ? $config['bubble'] : true, isset($config['permission']) ? $config['permission'] : null, isset($config['locking']) ? $config['locking'] : false), $config)]);
    }
    /**
     * Create an instance of the daily file log driver.
     *
     * @param array $config
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function createDailyDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(new RotatingFileHandler($config['path'], isset($config['days']) ? $config['days'] : 7, $this->level($config), isset($config['bubble']) ? $config['bubble'] : true, isset($config['permission']) ? $config['permission'] : null, isset($config['locking']) ? $config['locking'] : false), $config)]);
    }
    /**
     * Create an instance of the Slack log driver.
     *
     * @param array $config
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function createSlackDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(new SlackWebhookHandler($config['url'], isset($config['channel']) ? $config['channel'] : null, isset($config['username']) ? $config['username'] : 'EasyWeChat', isset($config['attachment']) ? $config['attachment'] : true, isset($config['emoji']) ? $config['emoji'] : ':boom:', isset($config['short']) ? $config['short'] : false, isset($config['context']) ? $config['context'] : true, $this->level($config), isset($config['bubble']) ? $config['bubble'] : true, isset($config['exclude_fields']) ? $config['exclude_fields'] : []), $config)]);
    }
    /**
     * Create an instance of the syslog log driver.
     *
     * @param array $config
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function createSyslogDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(new SyslogHandler('EasyWeChat', isset($config['facility']) ? $config['facility'] : LOG_USER, $this->level($config)), $config)]);
    }
    /**
     * Create an instance of the "error log" log driver.
     *
     * @param array $config
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function createErrorlogDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(new ErrorLogHandler(isset($config['type']) ? $config['type'] : ErrorLogHandler::OPERATING_SYSTEM, $this->level($config)))]);
    }
    /**
     * Prepare the handlers for usage by Monolog.
     *
     * @param array $handlers
     *
     * @return array
     */
    protected function prepareHandlers(array $handlers)
    {
        foreach ($handlers as $key => $handler) {
            $handlers[$key] = $this->prepareHandler($handler);
        }
        return $handlers;
    }
    /**
     * Prepare the handler for usage by Monolog.
     *
     * @param \Monolog\Handler\HandlerInterface $handler
     *
     * @return \Monolog\Handler\HandlerInterface
     */
    protected function prepareHandler(HandlerInterface $handler, array $config = [])
    {
        if (!isset($config['formatter'])) {
            if ($handler instanceof FormattableHandlerInterface) {
                $handler->setFormatter($this->formatter());
            }
        }
        return $handler;
    }
    /**
     * Get a Monolog formatter instance.
     *
     * @return \Monolog\Formatter\FormatterInterface
     */
    protected function formatter()
    {
        $formatter = new LineFormatter(null, null, true, true);
        $formatter->includeStacktraces();
        return $formatter;
    }
    /**
     * Extract the log channel from the given configuration.
     *
     * @param array $config
     *
     * @return string
     */
    protected function parseChannel(array $config)
    {
        return isset($config['name']) ? $config['name'] : 'EasyWeChat';
    }
    /**
     * Parse the level into a Monolog constant.
     *
     * @param array $config
     *
     * @return int
     *
     * @throws InvalidArgumentException
     */
    protected function level(array $config)
    {
        $level = isset($config['level']) ? $config['level'] : 'debug';
        if (isset($this->levels[$level])) {
            return $this->levels[$level];
        }
        throw new InvalidArgumentException('Invalid log level.');
    }
    /**
     * Get the default log driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['log.default'];
    }
    /**
     * Set the default log driver name.
     *
     * @param $name
     */
    public function setDefaultDriver($name)
    {
        $this->app['config']['log.default'] = $name;
    }
    /**
     * Register a custom driver creator Closure.
     *
     * @param   $driver
     * @param \Closure $callback
     *
     * @return $this
     */
    public function extend($driver, \Closure $callback)
    {
        $this->customCreators[$driver] = $callback->bindTo($this, $this);
        return $this;
    }
    /**
     * System is unusable.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function emergency($message, array $context = [])
    {
        return $this->driver()->emergency($message, $context);
    }
    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function alert($message, array $context = [])
    {
        return $this->driver()->alert($message, $context);
    }
    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function critical($message, array $context = [])
    {
        return $this->driver()->critical($message, $context);
    }
    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function error($message, array $context = [])
    {
        return $this->driver()->error($message, $context);
    }
    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function warning($message, array $context = [])
    {
        return $this->driver()->warning($message, $context);
    }
    /**
     * Normal but significant events.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function notice($message, array $context = [])
    {
        return $this->driver()->notice($message, $context);
    }
    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function info($message, array $context = [])
    {
        return $this->driver()->info($message, $context);
    }
    /**
     * Detailed debug information.
     *
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function debug($message, array $context = [])
    {
        return $this->driver()->debug($message, $context);
    }
    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param $message
     * @param array  $context
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function log($level, $message, array $context = [])
    {
        return $this->driver()->log($level, $message, $context);
    }
    /**
     * Dynamically call the default driver instance.
     *
     * @param $method
     * @param array  $parameters
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function __call($method, $parameters)
    {
        return $this->driver()->{$method}(...$parameters);
    }
}