<?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\Exception\BadRequestException;
use Dotclear\Exception\UnauthorizedException;
use Dotclear\Interface\Core\BlogsInterface;

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

    /**
     * @deprecated  since 2.33, use App::status()->blog()->statuses() instead
     */
    public function getAllBlogStatus(): array
    {
        $this->core->deprecated()->set('App::status()->blog()->statuses()', '2.33');

        return $this->core->status()->blog()->statuses();
    }

    /**
     * @deprecated  since 2.33, use App::status()->blog()->name($s) instead
     */
    public function getBlogStatus(int $s): string
    {
        $this->core->deprecated()->set('App::status()->blog()->status($s)', '2.33');

        return $this->core->status()->blog()->name($s);
    }

    public function getBlogPermissions(string $id, bool $with_super = true): array
    {
        $sql = new SelectStatement();
        $sql
            ->columns([
                'U.user_id as user_id',
                'user_super',
                'user_name',
                'user_firstname',
                'user_displayname',
                'user_email',
                'permissions',
            ])
            ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME, 'U'))
            ->join((new JoinStatement())
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::PERMISSIONS_TABLE_NAME, 'P'))
                ->on('U.user_id = P.user_id')
                ->statement())
            ->where('blog_id = ' . $sql->quote($id));

        if ($with_super) {
            $sql->union(
                (new SelectStatement())
                ->columns([
                    'U.user_id as user_id',
                    'user_super',
                    'user_name',
                    'user_firstname',
                    'user_displayname',
                    'user_email',
                    'NULL AS permissions',
                ])
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::USER_TABLE_NAME, 'U'))
                ->where('user_super = 1')
                ->statement()
            );
        }

        $rs = $sql->select();

        $res = [];

        if ($rs instanceof MetaRecord) {
            while ($rs->fetch()) {
                $user_id = $rs->strField('user_id');
                if ($user_id !== '') {
                    $res[$user_id] = [
                        'name'        => $rs->strField('user_name', true),
                        'firstname'   => $rs->strField('user_firstname', true),
                        'displayname' => $rs->strField('user_displayname', true),
                        'email'       => $rs->strField('user_email', true),
                        'super'       => $rs->boolField('user_super'),
                        'p'           => $this->core->auth()->parsePermissions($rs->strField('permissions', true)),
                    ];
                }
            }
        }

        return $res;
    }

    public function getBlog(string $id): MetaRecord
    {
        return $this->getBlogs(['blog_id' => $id]);
    }

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

        if ($count_only) {
            $sql
                ->column($sql->count('B.blog_id'))
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->blog()::BLOG_TABLE_NAME, 'B'))
                ->where('NULL IS NULL')
            ;
        } else {
            $sql
                ->columns([
                    'B.blog_id',
                    'blog_uid',
                    'blog_url',
                    'blog_name',
                    'blog_desc',
                    'blog_creadt',
                    'blog_upddt',
                    'blog_status',
                ])
                ->from($sql->as($this->core->db()->con()->prefix() . $this->core->blog()::BLOG_TABLE_NAME, 'B'))
                ->where('NULL IS NULL')
            ;

            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);
            }

            if (!empty($params['order']) && is_string($params['order'])) {
                $sql->order($sql->escape($params['order']));
            } else {
                $sql->order('B.blog_id ASC');
            }

            if (!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);
            }
        }

        if ($this->core->auth()->userID() && !$this->core->auth()->isSuperAdmin()) {
            $sql
                ->join(
                    (new JoinStatement())
                        ->inner()
                        ->from($sql->as($this->core->db()->con()->prefix() . $this->core->auth()::PERMISSIONS_TABLE_NAME, 'PE'))
                        ->on('B.blog_id = PE.blog_id')
                        ->statement()
                )
                ->and('PE.user_id = ' . $sql->quote($this->core->auth()->userID()))
                ->and($sql->orGroup([
                    $sql->like('permissions', '%|' . $this->core->auth()::PERMISSION_USAGE . '|%'),
                    $sql->like('permissions', '%|' . $this->core->auth()::PERMISSION_ADMIN . '|%'),
                    $sql->like('permissions', '%|' . $this->core->auth()::PERMISSION_CONTENT_ADMIN . '|%'),
                ]))
                ->and('blog_status >= ' . $this->core->status()->blog()->threshold())
            ;
        } elseif (!$this->core->auth()->userID()) {
            $sql->and('blog_status >= ' . $this->core->status()->blog()->threshold());
        }

        if (isset($params['blog_status'])
            && $params['blog_status'] !== ''
            && is_numeric($params['blog_status'])
            && $this->core->auth()->isSuperAdmin()
        ) {
            $sql->and('blog_status = ' . (int) $params['blog_status']);
        }

        if (!empty($params['blog_id'])) {
            $values = $sql->sanitizeIn($params['blog_id'], 'string', false);
            if ($values !== []) {
                $sql->and('B.blog_id' . $sql->in($values));
            }
        }

        if (!empty($params['q']) && is_string($params['q'])) {
            $params['q'] = strtolower(str_replace('*', '%', $params['q']));
            $sql->and($sql->orGroup([
                $sql->like('LOWER(B.blog_id)', $sql->escape($params['q'])),
                $sql->like('LOWER(B.blog_name)', $sql->escape($params['q'])),
                $sql->like('LOWER(B.blog_url)', $sql->escape($params['q'])),
            ]));
        }

        return $sql->select() ?? MetaRecord::newFromArray([]);
    }

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

        $this->fillBlogCursor($cur);

        $cur->blog_creadt = date('Y-m-d H:i:s');
        $cur->blog_upddt  = date('Y-m-d H:i:s');
        $cur->blog_uid    = md5(uniqid());

        $cur->insert();
    }

    public function updBlog(string $id, Cursor $cur): void
    {
        $this->fillBlogCursor($cur);

        $cur->blog_upddt = date('Y-m-d H:i:s');

        $cur->update("WHERE blog_id = '" . $this->core->db()->con()->escapeStr($id) . "'");
    }

    /**
     * Clean up blog cursor.
     *
     * @throws  BadRequestException
     *
     * @param   Cursor  $cur    The blog cursor
     */
    private function fillBlogCursor(Cursor $cur): void
    {
        if ($cur->blog_id !== null
            && is_string($cur->blog_id)
            && ($cur->blog_id === '' || !preg_match('/^[A-Za-z0-9._-]{2,}$/', $cur->blog_id))
        ) {
            throw new BadRequestException(__('Blog ID must contain at least 2 characters using letters, numbers or symbols.'));
        }

        if (($cur->blog_name !== null && $cur->blog_name == '') || (!$cur->blog_name)) {
            throw new BadRequestException(__('No blog name.'));
        }

        if (($cur->blog_url !== null && $cur->blog_url == '') || (!$cur->blog_url)) {
            throw new BadRequestException(__('No blog URL.'));
        }
    }

    public function delBlog(string $id): 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->blog()::BLOG_TABLE_NAME)
            ->where('blog_id = ' . $sql->quote($id))
            ->delete();
    }

    public function blogExists(string $id): bool
    {
        $sql = new SelectStatement();
        $rs  = $sql
            ->column('blog_id')
            ->from($this->core->db()->con()->prefix() . $this->core->blog()::BLOG_TABLE_NAME)
            ->where('blog_id = ' . $sql->quote($id))
            ->select();

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

    public function countBlogPosts(string $id, ?string $type = null): int
    {
        $sql = new SelectStatement();
        $sql
            ->column($sql->count('post_id'))
            ->from($this->core->db()->con()->prefix() . $this->core->blog()::POST_TABLE_NAME)
            ->where('blog_id = ' . $sql->quote($id));

        if ($type) {
            $sql->and('post_type = ' . $sql->quote($type));
        }

        return (int) $sql->select()?->cardinal();
    }

    public function blogDefaults(?array $defaults = null): void
    {
        if (!is_array($defaults)) {
            $defaults = [
                ['allow_comments', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Allow comments on blog', ],
                ['allow_trackbacks', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Allow trackbacks on blog', ],
                ['blog_timezone', $this->core->blogWorkspace()::NS_STRING, 'Europe/London',
                    'Blog timezone', ],
                ['comments_nofollow', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Add rel="nofollow" to comments URLs', ],
                ['comments_pub', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Publish comments immediately', ],
                ['comments_ttl', $this->core->blogWorkspace()::NS_INT, 0,
                    'Number of days to keep comments open (0 means no ttl)', ],
                ['copyright_notice', $this->core->blogWorkspace()::NS_STRING, '',
                    'Copyright notice (simple text)', ],
                ['date_format', $this->core->blogWorkspace()::NS_STRING, '%A, %B %e %Y',
                    'Date format. See PHP strftime function for patterns', ],
                ['editor', $this->core->blogWorkspace()::NS_STRING, '',
                    'Person responsible of the content', ],
                ['enable_html_filter', $this->core->blogWorkspace()::NS_BOOL, 0,
                    'Enable HTML filter', ],
                ['lang', $this->core->blogWorkspace()::NS_STRING, 'en',
                    'Default blog language', ],
                ['media_exclusion', $this->core->blogWorkspace()::NS_STRING, '/\.(phps?|pht(ml)?|phl|phar|.?html?|inc|xml|js|htaccess)[0-9]*$/i',
                    'File name exclusion pattern in media manager. (PCRE value)', ],
                ['media_img_m_size', $this->core->blogWorkspace()::NS_INT, 448,
                    'Image medium size in media manager', ],
                ['media_img_s_size', $this->core->blogWorkspace()::NS_INT, 240,
                    'Image small size in media manager', ],
                ['media_img_t_size', $this->core->blogWorkspace()::NS_INT, 100,
                    'Image thumbnail size in media manager', ],
                ['media_img_title_pattern', $this->core->blogWorkspace()::NS_STRING, 'Description ;; separator(, )',
                    'Pattern to set image legend when you insert it in a post', ],
                ['media_video_width', $this->core->blogWorkspace()::NS_INT, 400,
                    'Video width in media manager', ],
                ['media_video_height', $this->core->blogWorkspace()::NS_INT, 300,
                    'Video height in media manager', ],
                ['nb_post_for_home', $this->core->blogWorkspace()::NS_INT, 20,
                    'Number of entries on first home page', ],
                ['nb_post_per_page', $this->core->blogWorkspace()::NS_INT, 20,
                    'Number of entries on home pages and category pages', ],
                ['nb_post_per_feed', $this->core->blogWorkspace()::NS_INT, 20,
                    'Number of entries on feeds', ],
                ['nb_comment_per_feed', $this->core->blogWorkspace()::NS_INT, 20,
                    'Number of comments on feeds', ],
                ['post_url_format', $this->core->blogWorkspace()::NS_STRING, '{y}/{m}/{d}/{t}',
                    'Post URL format. {y}: year, {m}: month, {d}: day, {id}: post id, {t}: entry title', ],
                ['public_path', $this->core->blogWorkspace()::NS_STRING, 'public',
                    'Path to public directory, begins with a / for a full system path', ],
                ['public_url', $this->core->blogWorkspace()::NS_STRING, '/public',
                    'URL to public directory', ],
                ['robots_policy', $this->core->blogWorkspace()::NS_STRING, 'INDEX,FOLLOW',
                    'Search engines robots policy', ],
                ['short_feed_items', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Display short feed items', ],
                ['theme', $this->core->blogWorkspace()::NS_STRING, $this->core->config()->defaultTheme(),
                    'Blog theme', ],
                ['themes_path', $this->core->blogWorkspace()::NS_STRING, 'themes',
                    'Themes root path', ],
                ['themes_url', $this->core->blogWorkspace()::NS_STRING, '/themes',
                    'Themes root URL', ],
                ['time_format', $this->core->blogWorkspace()::NS_STRING, '%H:%M',
                    'Time format. See PHP strftime function for patterns', ],
                ['tpl_allow_php', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Allow PHP code in templates', ],
                ['tpl_use_cache', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Use template caching', ],
                ['trackbacks_pub', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Publish trackbacks immediately', ],
                ['trackbacks_ttl', $this->core->blogWorkspace()::NS_INT, 0,
                    'Number of days to keep trackbacks open (0 means no ttl)', ],
                ['url_scan', $this->core->blogWorkspace()::NS_STRING, 'query_string',
                    'URL handle mode (path_info or query_string)', ],
                ['no_public_css', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Don\'t use generic public.css stylesheet', ],
                ['use_smilies', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Show smilies on entries and comments', ],
                ['no_search', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Disable search', ],
                ['inc_subcats', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Include sub-categories in category page and category posts feed', ],
                ['wiki_comments', $this->core->blogWorkspace()::NS_BOOL, false,
                    'Allow commenters to use a subset of wiki syntax', ],
                ['import_feed_url_control', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Control feed URL before import', ],
                ['import_feed_no_private_ip', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Prevent import feed from private IP', ],
                ['import_feed_ip_regexp', $this->core->blogWorkspace()::NS_STRING, '',
                    'Authorize import feed only from this IP regexp', ],
                ['import_feed_port_regexp', $this->core->blogWorkspace()::NS_STRING, '/^(80|443)$/',
                    'Authorize import feed only from this port regexp', ],
                ['jquery_needed', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Load jQuery library', ],
                ['legacy_needed', $this->core->blogWorkspace()::NS_BOOL, true,
                    'Load Legacy JS library', ],
                ['sleepmode_timeout', $this->core->blogWorkspace()::NS_INT, 31536000,
                    'Sleep mode timeout', ],
                ['store_plugin_url', $this->core->blogWorkspace()::NS_STRING, $this->core->config()->release('store_plugin_url'),
                    'Plugins XML feed location', ],
                ['store_theme_url', $this->core->blogWorkspace()::NS_STRING, $this->core->config()->release('store_theme_url'),
                    'Themes XML feed location', ],
            ];
        }

        foreach ($defaults as $v) {
            $this->core->blogSettings()->get('system')->put($v[0], $v[2], $v[1], $v[3], false, true);
        }
    }
}
