<?php

namespace Drush\Drupal\Commands\core;

use Consolidation\OutputFormatters\StructuredData\PropertyList;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Database\Database;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\user\Entity\User;
use Drush\Commands\DrushCommands;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Html;
use Drush\Drupal\DrupalUtil;
use Drush\Exceptions\UserAbortException;
use Symfony\Component\Console\Output\OutputInterface;

class WatchdogCommands extends DrushCommands
{
    /**
     * Show watchdog messages.
     *
     * @command watchdog:show
     * @param $substring A substring to look search in error messages.
     * @option count The number of messages to show.
     * @option severity Restrict to messages of a given severity level (numeric or string).
     * @option severity-min Restrict to messages of a given severity level and higher.
     * @option type Restrict to messages of a given type.
     * @option extended Return extended information about each message.
     * @usage  drush watchdog:show
     *   Show a listing of most recent 10 messages.
     * @usage drush watchdog:show "cron run successful"
     *   Show a listing of most recent 10 messages containing the string <info>cron run successful</info>.
     * @usage drush watchdog:show --count=46
     *   Show a listing of most recent 46 messages.
     * @usage drush watchdog:show --severity=Notice
     *   Show a listing of most recent 10 messages with a severity of notice.
     * @usage drush watchdog:show --severity-min=Warning
     *   Show a listing of most recent 10 messages with a severity of warning or higher.
     * @usage drush watchdog:show --type=php
     *   Show a listing of most recent 10 messages of type php
     * @aliases wd-show,ws,watchdog-show
     * @validate-module-enabled dblog
     * @field-labels
     *   wid: ID
     *   type: Type
     *   message: Message
     *   severity: Severity
     *   location: Location
     *   hostname: Hostname
     *   date: Date
     *   username: Username
     *   uid: Uid
     * @default-fields wid,date,type,severity,message
     * @filter-default-field message
     * @return RowsOfFields
     */
    public function show($substring = '', $options = ['format' => 'table', 'count' => 10, 'severity' => self::REQ, 'severity-min' => self::REQ, 'type' => self::REQ, 'extended' => false])
    {
        $where = $this->where($options['type'], $options['severity'], $substring, 'AND', $options['severity-min']);
        $query = Database::getConnection()->select('watchdog', 'w')
            ->range(0, $options['count'])
            ->fields('w')
            ->orderBy('wid', 'DESC');
        if (!empty($where['where'])) {
            $query->where($where['where'], $where['args']);
        }
        $rsc = $query->execute();
        while ($result = $rsc->fetchObject()) {
            $row = $this->formatResult($result, $options['extended']);
            $table[$row->wid] = (array)$row;
        }
        if (empty($table)) {
            $this->logger()->notice(dt('No log messages available.'));
            return;
        } else {
            return new RowsOfFields($table);
        }
    }

    /**
     * Interactively filter the watchdog message listing.
     *
     * @command watchdog:list
     * @param $substring A substring to look search in error messages.
     * @option count The number of messages to show.
     * @option extended Return extended information about each message.
     * @option severity Restrict to messages of a given severity level.
     * @option type Restrict to messages of a given type.
     * @usage  drush watchdog:list
     *   Prompt for message type or severity, then run watchdog-show.
     * @aliases wd-list,watchdog-list
     * @hidden-options type,severity
     * @validate-module-enabled dblog
     * @field-labels
     *   wid: ID
     *   type: Type
     *   message: Message
     *   severity: Severity
     *   location: Location
     *   hostname: Hostname
     *   date: Date
     *   username: Username
     * @default-fields wid,date,type,severity,message
     */
    public function watchdogList($substring = '', $options = ['format' => 'table', 'count' => 10, 'extended' => false]): RowsOfFields
    {
        return $this->show($substring, $options);
    }

    /**
     * Tail watchdog messages.
     *
     * @command watchdog:tail
     * @param OutputInterface $output
     * @param $substring A substring to look search in error messages.
     * @option severity Restrict to messages of a given severity level (numeric or string).
     * @option severity-min Restrict to messages of a given severity level and higher.
     * @option type Restrict to messages of a given type.
     * @option extended Return extended information about each message.
     * @usage  drush watchdog:tail
     *   Continuously tail watchdog messages.
     * @usage drush watchdog:tail "cron run successful"
     *   Continuously tail watchdog messages, filtering on the string <info>cron run successful</info>.
     * @usage drush watchdog:tail --severity=Notice
     *   Continuously tail watchdog messages, filtering severity of notice.
     * @usage drush watchdog:tail --severity-min=Warning
     *   Continuously tail watchdog messages, filtering for a severity of warning or higher.
     * @usage drush watchdog:tail --type=php
     *   Continuously tail watchdog messages, filtering on type equals php.
     * @aliases wd-tail,wt,watchdog-tail
     * @validate-module-enabled dblog
     * @version 10.6
     */
    public function tail(OutputInterface $output, $substring = '', $options = ['severity' => self::REQ, 'severity-min' => self::REQ, 'type' => self::REQ, 'extended' => false]): void
    {
        $where = $this->where($options['type'], $options['severity'], $substring, 'AND', $options['severity-min']);
        if (empty($where['where'])) {
            $where = [
              'where' => 'wid > :wid',
              'args' => [],
            ];
        } else {
            $where['where'] .= " AND wid > :wid";
        }

        $last_seen_wid = 0;
        $iteration = 1;
        while (true) {
            $iteration++;
            $where['args'][':wid'] = $last_seen_wid;
            $query = Database::getConnection()->select('watchdog', 'w')
                ->fields('w')
                ->orderBy('wid', 'DESC');
            if ($last_seen_wid === 0) {
                $query->range(0, 10);
            }
            $query->where($where['where'], $where['args']);

            $rsc = $query->execute();
            while ($result = $rsc->fetchObject()) {
                if ($result->wid > $last_seen_wid) {
                    $last_seen_wid = $result->wid;
                }
                $row = $this->formatResult($result, $options['extended']);
                $msg = "{$row->wid}\t{$row->date}\t{$row->type}\t{$row->severity}\t{$row->message}";
                $output->writeln($msg);
            }
            sleep(2);
        }
    }

    /**
     * @hook interact watchdog-list
     * @throws UserAbortException
     */
    public function interactList($input, $output): void
    {

        $choices['-- types --'] = dt('== message types ==');
        $types = $this->messageTypes();
        foreach ($types as $key => $type) {
            $choices[$key] = $type;
        }
        $choices['-- levels --'] = dt('== severity levels ==');
        $severities = RfcLogLevel::getLevels();

        foreach ($severities as $key => $value) {
            $choices[$key] = $value;
        }
        $option = $this->io()->choice(dt('Select a message type or severity level'), $choices);
        if (isset($types[$option])) {
            $input->setOption('type', $types[$option]);
        } else {
            $input->setOption('severity', $option);
        }
    }

    /**
     * Delete watchdog log records.
     *
     * @command watchdog:delete
     * @param $substring Delete all log records with this text in the messages.
     * @option severity Delete messages of a given severity level.
     * @option type Delete messages of a given type.
     * @usage drush watchdog:delete all
     *   Delete all messages.
     * @usage drush watchdog:delete 64
     *   Delete messages with id 64.
     * @usage drush watchdog:delete "cron run succesful"
     *   Delete messages containing the string "cron run succesful".
     * @usage drush watchdog:delete --severity=Notice
     *   Delete all messages with a severity of notice.
     * @usage drush watchdog:delete --type=cron
     *   Delete all messages of type cron.
     * @aliases wd-del,wd-delete,wd,watchdog-delete
     * @validate-module-enabled dblog
     */
    public function delete($substring = '', $options = ['severity' => self::REQ, 'type' => self::REQ]): void
    {
        if ($substring == 'all') {
            $this->output()->writeln(dt('All watchdog messages will be deleted.'));
            if (!$this->io()->confirm(dt('Do you really want to continue?'))) {
                throw new UserAbortException();
            }
            $ret = Database::getConnection()->truncate('watchdog')->execute();
            $this->logger()->success(dt('All watchdog messages have been deleted.'));
        } elseif (is_numeric($substring)) {
            $this->output()->writeln(dt('Watchdog message #!wid will be deleted.', ['!wid' => $substring]));
            if (!$this->io()->confirm(dt('Do you want to continue?'))) {
                throw new UserAbortException();
            }
            $affected_rows = Database::getConnection()->delete('watchdog')->condition('wid', $substring)->execute();
            if ($affected_rows == 1) {
                $this->logger()->success(dt('Watchdog message #!wid has been deleted.', ['!wid' => $substring]));
            } else {
                throw new \Exception(dt('Watchdog message #!wid does not exist.', ['!wid' => $substring]));
            }
        } else {
            if ((empty($substring)) && (!isset($options['type'])) && (!isset($options['severity']))) {
                throw new \Exception(dt('No options provided.'));
            }
            $where = $this->where($options['type'], $options['severity'], $substring, 'OR');
            $this->output()->writeln(dt('All messages with !where will be deleted.', ['!where' => preg_replace("/message LIKE %$substring%/", "message body containing '$substring'", strtr($where['where'], $where['args']))]));
            if (!$this->io()->confirm(dt('Do you want to continue?'))) {
                throw new UserAbortException();
            }
            $affected_rows = Database::getConnection()->delete('watchdog')
                ->where($where['where'], $where['args'])
                ->execute();
            $this->logger()->success(dt('!affected_rows watchdog messages have been deleted.', ['!affected_rows' => $affected_rows]));
        }
    }

    /**
     * Show one log record by ID.
     *
     * @command watchdog:show-one
     * @param $id Watchdog Id
     * @aliases wd-one,watchdog-show-one
     * @validate-module-enabled dblog
     */
    public function showOne($id, $options = ['format' => 'yaml']): PropertyList
    {
        $rsc = Database::getConnection()->select('watchdog', 'w')
            ->fields('w')
            ->condition('wid', (int)$id)
            ->range(0, 1)
            ->execute();
        $result = $rsc->fetchObject();
        if (!$result) {
            throw new \Exception(dt('Watchdog message #!wid not found.', ['!wid' => $id]));
        }
        return new PropertyList($this->formatResult($result, true));
    }

    /**
     * Build a WHERE snippet based on given parameters.
     *
     * @param $type
     *   String. Valid watchdog type.
     * @param $severity
     *   Int or String for a valid watchdog severity message.
     * @param $filter
     *   String. Value to filter watchdog messages by.
     * @param $criteria
     *   ('AND', 'OR'). Criteria for the WHERE snippet.
     * @param $severity_min
     *   Int or String for the minimum severity to return.
     * @return
     *   An array with structure ('where' => string, 'args' => array())
     */
    protected function where($type = null, $severity = null, $filter = null, $criteria = 'AND', $severity_min = null): array
    {
        $args = [];
        $conditions = [];
        if ($type) {
            $types = $this->messageTypes();
            if (!in_array($type, $types)) {
                $msg = "Unrecognized message type: !type.\nRecognized types are: !types.";
                throw new \Exception(dt($msg, ['!type' => $type, '!types' => implode(', ', $types)]));
            }
            $conditions[] = "type = :type";
            $args[':type'] = $type;
        }
        if (!empty($severity) && !empty($severity_min)) {
            $msg = "--severity=!severity  --severity-min=!severity_min\nYou may provide a value for one of these parameters but not both.";
            throw new \Exception(dt($msg, ['!severity' => $severity, '!severity_min' => $severity_min]));
        }
        // From here we know that only one of --severity or --severity-min might
        // have a value but not both of them.
        if (!empty($severity) || !empty($severity_min)) {
            if (empty($severity)) {
                $severity = $severity_min;
                $operator = '<=';
            } else {
                $operator = '=';
            }
            $severities = RfcLogLevel::getLevels();
            if (isset($severities[$severity])) {
                $level = $severity;
            } elseif (($key = array_search($severity, $severities)) !== false) {
                $level = $key;
            } else {
                $level = false;
            }
            if ($level === false) {
                foreach ($severities as $key => $value) {
                    $levels[] = "$value($key)";
                }
                $msg = "Unknown severity level: !severity\nValid severity levels are: !levels.";
                throw new \Exception(dt($msg, ['!severity' => $severity, '!levels' => implode(', ', $levels)]));
            }
            $conditions[] = "severity {$operator} :severity";
            $args[':severity'] = $level;
        }
        if ($filter) {
            $conditions[] = "message LIKE :filter";
            $args[':filter'] = '%' . $filter . '%';
        }

        $where = implode(" $criteria ", $conditions);

        return ['where' => $where, 'args' => $args];
    }

    /**
     * Format a watchdog database row.
     *
     * @param $result
     *   Array. A database result object.
     * @param $extended
     *   Boolean. Return extended message details.
     * @return
     *   Array. The result object with some attributes themed.
     */
    protected function formatResult($result, $extended = false)
    {
        // Severity.
        $severities = RfcLogLevel::getLevels();
        $result->severity = trim(DrupalUtil::drushRender($severities[$result->severity]));

        // Date.
        $result->date = date('d/M H:i', $result->timestamp);
        unset($result->timestamp);

        // Username.
        $result->username = (new AnonymousUserSession())->getAccountName() ?: dt('Anonymous');
        $account = User::load($result->uid);
        if ($account && !$account->isAnonymous()) {
            $result->username = $account->getAccountName();
        }

        // Message.
        $variables = $result->variables;
        if (is_string($variables)) {
            $variables = unserialize($variables);
        }
        if (is_array($variables)) {
            $result->message = strtr($result->message, $variables);
        }
        unset($result->variables);
        $message_length = 188;

        // Print all the data available
        if ($extended) {
            // Possible empty values.
            if (empty($result->link)) {
                unset($result->link);
            }
            if (empty($result->referer)) {
                unset($result->referer);
            }
            $message_length = PHP_INT_MAX;
        }
        $result->message = Unicode::truncate(strip_tags(Html::decodeEntities($result->message)), $message_length, false, false);

        return $result;
    }

    /**
     * Helper function to obtain the message types based on drupal version.
     *
     * @return
     *   Array of watchdog message types.
     */
    public static function messageTypes()
    {
        return _dblog_get_message_types();
    }
}
