<?php

/**
 * @package Dotclear
 * @subpackage Core
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Core;

use ArrayObject;
use Dotclear\Database\Cursor;
use Dotclear\Database\MetaRecord;
use Dotclear\Database\Statement\DeleteStatement;
use Dotclear\Database\Statement\JoinStatement;
use Dotclear\Database\Statement\SelectStatement;
use Dotclear\Database\Statement\UpdateStatement;
use Dotclear\Exception\BadRequestException;
use Dotclear\Exception\UnauthorizedException;
use Dotclear\Interface\Core\UsersInterface;
use Dotclear\Schema\Extension\User;

/**
 * @brief   Users handler.
 *
 * @since   2.28, users features have been grouped in this class
 * @since   2.36, constructor arguments has been replaced by Core instance
 */
class Users implements UsersInterface
{
    /**
     * Constructs a new instance.
     *
     * @param   Core    $core   The core container
     */
    public function __construct(
        protected Core $core
    ) {
    }

    public function getUser(string $id): MetaRecord
    {
        $params['user_id'] = $id;

        return $this->getUsers($params);
    }

    public function getUsers(array|ArrayObject $params = [], bool $count_only = false): MetaRecord
    {
        $sql = new SelectStatement();

        if ($count_only) {
            $sql
                ->column($sql->count('U.user_id'))
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME, 'U'))
                ->where('NULL IS NULL');
        } else {
            $sql
                ->columns([
                    'U.user_id',
                    'user_super',
                    'user_status',
                    'user_pwd',
                    'user_change_pwd',
                    'user_name',
                    'user_firstname',
                    'user_displayname',
                    'user_email',
                    'user_url',
                    'user_desc',
                    'user_lang',
                    'user_tz',
                    'user_post_status',
                    'user_options',
                    'user_creadt',
                    'user_upddt',
                    $sql->count('P.post_id', 'nb_post'),
                ])
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME, 'U'));

            if (!empty($params['columns'])) {
                $values = [];
                if (is_array($params['columns'])) {
                    $values = array_map(fn (mixed $v): string => is_string($v) ? $v : '', $params['columns']);
                } elseif (is_string($params['columns'])) {
                    $values = [$params['columns']];
                }

                $sql->columns($values);
            }

            $sql
                ->join(
                    (new JoinStatement())
                        ->left()
                        ->from($sql->as($this->core->db()->con()->prefix() . $this->core->blog()::POST_TABLE_NAME, 'P'))
                        ->on('U.user_id = P.user_id')
                        ->statement()
                )
                ->where('NULL IS NULL');
        }

        if (!empty($params['q']) && is_string($params['q'])) {
            $q = $sql->escape(str_replace('*', '%', strtolower($params['q'])));
            $sql->and($sql->orGroup([
                $sql->like('LOWER(U.user_id)', $q),
                $sql->like('LOWER(user_name)', $q),
                $sql->like('LOWER(user_firstname)', $q),
            ]));
        }

        if (!empty($params['user_id']) && is_string($params['user_id'])) {
            $sql->and('U.user_id = ' . $sql->quote($params['user_id']));
        }

        if (isset($params['user_status'])) {
            $values = $sql->sanitizeIn($params['user_status'], 'int', false);
            if ($values !== []) {
                $sql->and('U.user_status' . $sql->in($values));
            }
        }

        if (!$count_only) {
            $sql->group([
                'U.user_id',
                'user_super',
                'user_status',
                'user_pwd',
                'user_change_pwd',
                'user_name',
                'user_firstname',
                'user_displayname',
                'user_email',
                'user_url',
                'user_desc',
                'user_lang',
                'user_tz',
                'user_post_status',
                'user_options',
                'user_creadt',
                'user_upddt',
            ]);

            if (!empty($params['order']) && is_string($params['order'])) {
                if (preg_match('`^([^. ]+) (?:asc|desc)`i', $params['order'], $matches)) {
                    if (in_array($matches[1], ['user_id', 'user_name', 'user_firstname', 'user_displayname', 'user_creadt', 'user_upddt'], true)) {
                        $table_prefix = 'U.';
                    } else {
                        $table_prefix = ''; // order = nb_post (asc|desc)
                    }

                    $sql->order($table_prefix . $sql->escape($params['order']));
                } else {
                    $sql->order($sql->escape($params['order']));
                }
            } else {
                $sql->order('U.user_id ASC');
            }
        }

        if (!$count_only && !empty($params['limit'])) {
            $values = is_array($params['limit']) ? array_values($params['limit']) : [$params['limit']];
            // Make $values an array of integer values
            $values = array_map(fn (mixed $v): int => is_numeric($v) ? (int) $v : 0, $values);

            /**
             * @var array{0: int, 1?: int}  $limit
             */
            $limit = [
                $values[0],
            ];
            if (isset($values[1])) {
                $limit[1] = $values[1];
            }

            $sql->limit($limit);
        }

        $rs = $sql->select() ?? MetaRecord::newFromArray([]);
        $rs->extend(User::class);

        return $rs;
    }

    public function addUser(Cursor $cur): string
    {
        if (!$this->core->auth()->isSuperAdmin()) {
            throw new UnauthorizedException(__('You are not an administrator.'));
        }

        if ($cur->strField('user_id') === '') {
            throw new BadRequestException(__('No user ID given.'));
        }

        if ($cur->strField('user_pwd') === '') {
            throw new BadRequestException(__('No password given.'));
        }

        $this->fillUserCursor($cur);

        if ($cur->strField('user_creadt') === '') {
            $cur->setStrField('user_creadt', date('Y-m-d H:i:s'));
        }

        $cur->insert();

        # --BEHAVIOR-- coreAfterAddUser -- Cursor
        $this->core->behavior()->callBehavior('coreAfterAddUser', $cur);

        return $cur->strField('user_id');
    }

    public function updUser(string $id, Cursor $cur): string
    {
        $this->fillUserCursor($cur);

        if (($cur->strField('user_id', true) !== null || $id != $this->core->auth()->userID()) && !$this->core->auth()->isSuperAdmin()) {
            throw new UnauthorizedException(__('You are not an administrator.'));
        }

        $sql = new UpdateStatement();
        $sql->where('user_id = ' . $sql->quote($id));

        $sql->update($cur);

        # --BEHAVIOR-- coreAfterUpdUser -- Cursor
        $this->core->behavior()->callBehavior('coreAfterUpdUser', $cur);

        if ($cur->strField('user_id', true) !== null) {
            $id = $cur->strField('user_id');
        }

        # Updating all user's blogs
        $sql = new SelectStatement();
        $sql
            ->distinct()
            ->column('blog_id')
            ->from($this->core->db()->con()->prefix() . $this->core->blog()::POST_TABLE_NAME)
            ->where('user_id = ' . $sql->quote($id));

        $rs = $sql->select();

        if ($rs instanceof MetaRecord) {
            $old_blog = $this->core->blog()->id();
            while ($rs->fetch()) {
                $this->core->blog()->loadFromBlog($rs->strField('blog_id'));
                $this->core->blog()->triggerBlog();
            }

            $this->core->blog()->loadFromBlog($old_blog);
        }

        return $id;
    }

    public function delUser(string $id): void
    {
        if (!$this->core->auth()->isSuperAdmin()) {
            throw new UnauthorizedException(__('You are not an administrator.'));
        }

        if ($id == $this->core->auth()->userID()) {
            return;
        }

        $rs = $this->getUser($id);

        if ($rs->intField('nb_post') > 0) {
            return;
        }

        $sql = new DeleteStatement();
        $sql
            ->from($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME)
            ->where('user_id = ' . $sql->quote($id));

        $sql->delete();

        # --BEHAVIOR-- coreAfterDelUser -- string
        $this->core->behavior()->callBehavior('coreAfterDelUser', $id);
    }

    public function userExists(string $id): bool
    {
        $sql = new SelectStatement();
        $sql
            ->column('user_id')
            ->from($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME)
            ->where('user_id = ' . $sql->quote($id));

        $rs = $sql->select();

        return !$rs instanceof MetaRecord || !$rs->isEmpty();
    }

    public function getUserPermissions(string $id): array
    {
        $sql = new SelectStatement();
        $sql
            ->columns([
                'B.blog_id',
                'blog_name',
                'blog_url',
                'permissions',
            ])
            ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::PERMISSIONS_TABLE_NAME, 'P'))
            ->join(
                (new JoinStatement())
                ->inner()
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->blog()::BLOG_TABLE_NAME, 'B'))
                ->on('P.blog_id = B.blog_id')
                ->statement()
            )
            ->where('user_id = ' . $sql->quote($id));

        $rs = $sql->select();

        $res = [];

        if ($rs instanceof MetaRecord) {
            while ($rs->fetch()) {
                $blog_id = $rs->strField('blog_id');
                if ($blog_id !== '') {
                    $res[$blog_id] = [
                        'name' => $rs->strField('blog_name', true),
                        'url'  => $rs->strField('blog_url', true),
                        'p'    => $this->core->auth()->parsePermissions($rs->strField('permissions', true)),
                    ];
                }
            }
        }

        return $res;
    }

    public function setUserPermissions(string $id, array $perms): void
    {
        if (!$this->core->auth()->isSuperAdmin()) {
            throw new UnauthorizedException(__('You are not an administrator.'));
        }

        $sql = new DeleteStatement();
        $sql
            ->from($this->core->db()->con()->prefix() . $this->core->auth()::PERMISSIONS_TABLE_NAME)
            ->where('user_id = ' . $sql->quote($id));

        $sql->delete();

        foreach ($perms as $blog_id => $p) {
            $this->setUserBlogPermissions($id, $blog_id, $p, false);
        }
    }

    public function setUserBlogPermissions(string $id, string $blog_id, array $perms, bool $delete_first = true): void
    {
        if (!$this->core->auth()->isSuperAdmin()) {
            throw new UnauthorizedException(__('You are not an administrator.'));
        }

        $no_perm = $perms === [];

        $perms = '|' . implode('|', array_keys($perms)) . '|';

        $cur = $this->core->auth()->openPermCursor();

        $cur->setStrField('user_id', $id);
        $cur->setStrField('blog_id', $blog_id);
        $cur->setStrField('permissions', $perms);

        if ($delete_first || $no_perm) {
            $sql = new DeleteStatement();
            $sql
                ->from($this->core->db()->con()->prefix() . $this->core->auth()::PERMISSIONS_TABLE_NAME)
                ->where('blog_id = ' . $sql->quote($blog_id))
                ->and('user_id = ' . $sql->quote($id));

            $sql->delete();
        }

        if (!$no_perm) {
            $cur->insert();
        }
    }

    public function setUserDefaultBlog(string $id, string $blog_id): void
    {
        $cur = $this->core->auth()->openUserCursor();

        $cur->setStrField('user_default_blog', $blog_id);

        $sql = new UpdateStatement();
        $sql->where('user_id = ' . $sql->quote($id));

        $sql->update($cur);
    }

    public function removeUsersDefaultBlogs(array $ids): void
    {
        $cur = $this->core->auth()->openUserCursor();

        $cur->setStrField('user_default_blog', null);

        $sql = new UpdateStatement();
        $sql->where('user_default_blog' . $sql->in($ids));

        $sql->update($cur);
    }

    /**
     * Fills the user Cursor.
     *
     * @param      Cursor     $cur    The user Cursor
     *
     * @throws     BadRequestException
     */
    private function fillUserCursor(Cursor $cur): void
    {
        if ($cur->isField('user_id')
            && is_string($cur->strField('user_id', true))
            && !preg_match('/^[A-Za-z0-9@._-]{2,}$/', $cur->strField('user_id'))) {
            throw new BadRequestException(__('User ID must contain at least 2 characters using letters, numbers or symbols.'));
        }

        if ($cur->isField('user_url')
            && is_string($cur->strField('user_url', true))
            && $cur->strField('user_url') !== ''
            && !preg_match('|^https?://|', $cur->strField('user_url'))
        ) {
            $cur->setStrField('user_url', 'https://' . $cur->strField('user_url'));
        }

        if ($cur->isField('user_pwd') && is_string($cur->strField('user_pwd', true))) {
            if (strlen($cur->strField('user_pwd')) < 6) {
                throw new BadRequestException(__('Password must contain at least 6 characters.'));
            }

            $cur->setStrField('user_pwd', $this->core->auth()->crypt($cur->strField('user_pwd')));
        }

        if ($cur->isField('user_lang')
            && is_string($cur->strField('user_lang', true))
            && !preg_match('/^[a-z]{2}(-[a-z]{2})?$/', $cur->strField('user_lang'))
        ) {
            throw new BadRequestException(__('Invalid user language code.'));
        }

        if ($cur->strField('user_upddt') === '') {
            $cur->setStrField('user_upddt', date('Y-m-d H:i:s'));
        }

        if ($cur->getField('user_options') !== null) {
            $cur->setStrField('user_options', serialize((array) $cur->getField('user_options')));
        }
    }

    public function userDefaults(): array
    {
        return [
            'edit_size'   => 24,
            'post_format' => 'xhtml',
            'editor'      => [
                'xhtml' => 'dcCKEditor',
                'wiki'  => 'dcLegacyEditor',
            ],
            'enable_wysiwyg' => true,
            'toolbar_bottom' => false,
        ];
    }

    public function getUserCN(string $user_id, ?string $user_name, ?string $user_firstname, ?string $user_displayname): string
    {
        if ($user_displayname !== null && $user_displayname !== '') {
            return $user_displayname;
        }

        if ($user_name !== null && $user_name !== '') {
            if ($user_firstname !== null && $user_firstname !== '') {
                return $user_firstname . ' ' . $user_name;
            }

            return $user_name;
        }

        if ($user_firstname !== null && $user_firstname !== '') {
            return $user_firstname;
        }

        return $user_id;
    }
}
