<?php
namespace Misuzu\Forum;

use InvalidArgumentException;
use RuntimeException;
use stdClass;
use Index\Colour\Colour;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;

class ForumCategories {
    private IDbConnection $dbConn;
    private DbStatementCache $cache;

    public function __construct(IDbConnection $dbConn) {
        $this->dbConn = $dbConn;
        $this->cache = new DbStatementCache($dbConn);
    }

    public static function convertCategoryListToTree(
        array $catInfos,
        ForumCategoryInfo|string|null $parentInfo = null,
        ?Colour $colour = null
    ): array {
        $colour ??= Colour::none();
        $tree = [];
        $predicate = $parentInfo
            ? fn($catInfo) => $catInfo->isDirectChildOf($parentInfo)
            : fn($catInfo) => !$catInfo->hasParent();

        foreach($catInfos as $catInfo) {
            if(!$predicate($catInfo))
                continue;

            $tree[$catInfo->getId()] = $item = new stdClass;
            $item->info = $catInfo;
            $item->colour = $catInfo->hasColour() ? $catInfo->getColour() : $colour;
            $item->children = self::convertCategoryListToTree($catInfos, $catInfo, $item->colour);
            $item->childIds = [];
            foreach($item->children as $child) {
                $item->childIds[] = $child->info->getId();
                $item->childIds += $child->childIds;
            }
        }

        return $tree;
    }

    public function countCategories(
        ForumCategoryInfo|string|null|false $parentInfo = false,
        string|int|null $type = null,
        ?bool $hidden = null
    ): int {
        if($parentInfo instanceof ForumCategoryInfo)
            $parentInfo = $parentInfo->getId();

        $isRootParent = false;
        $hasParentInfo = $parentInfo !== false;
        $hasType = $type !== null;
        $hasHidden = $hidden !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_forum_categories';
        if($hasParentInfo) {
            ++$args;
            $isRootParent = $parentInfo === null;
            if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL
                $query .= 'WHERE (forum_parent IS NULL OR forum_parent = 0)';
            } else {
                $query .= 'WHERE forum_parent = ?';
            }
        }
        if($hasType) {
            if(is_string($type)) {
                if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES))
                    throw new InvalidArgumentException('$type is not a valid alias.');
                $type = ForumCategoryInfo::TYPE_ALIASES[$type];
            }

            $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE');
        }
        if($hasHidden)
            $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '=');

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasParentInfo && !$isRootParent)
            $stmt->addParameter(++$args, $parentInfo);
        if($hasType)
            $stmt->addParameter(++$args, $type);
        $stmt->execute();

        $result = $stmt->getResult();
        return $result->next() ? $result->getInteger(0) : 0;
    }

    public function getCategories(
        ForumCategoryInfo|string|null|false $parentInfo = false,
        string|int|null $type = null,
        ?bool $hidden = null,
        bool $asTree = false,
        ?Pagination $pagination = null
    ): array {
        $isRootParent = false;
        $hasParentInfo = $parentInfo !== false;
        $hasType = $type !== null;
        $hasHidden = $hidden !== null;
        $hasPagination = $pagination !== null;

        if($hasParentInfo && $asTree)
            throw new InvalidArgumentException('$asTree can only be used with $parentInfo set to false.');

        $args = 0;
        $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories';
        if($hasParentInfo) {
            ++$args;
            $isRootParent = $parentInfo === null;
            if($isRootParent) { // make a migration that makes the field DEFAULT NULL and update all 0s to NULL
                $query .= ' WHERE (forum_parent IS NULL OR forum_parent = 0)';
            } else {
                $query .= ' WHERE forum_parent = ?';
            }
        }
        if($hasType) {
            if(is_string($type)) {
                if(!array_key_exists($type, ForumCategoryInfo::TYPE_ALIASES))
                    throw new InvalidArgumentException('$type is not a valid alias.');
                $type = ForumCategoryInfo::TYPE_ALIASES[$type];
            }

            $query .= sprintf(' %s forum_type = ?', ++$args > 1 ? 'AND' : 'WHERE');
        }
        if($hasHidden)
            $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '=');
        $query .= ' ORDER BY forum_parent, forum_type <> 1, forum_order';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasParentInfo && !$isRootParent) {
            if($parentInfo instanceof ForumCategoryInfo)
                $stmt->addParameter(++$args, $parentInfo->getId());
            else
                $stmt->addParameter(++$args, $parentInfo);
        }
        if($hasType)
            $stmt->addParameter(++$args, $type);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }
        $stmt->execute();

        $result = $stmt->getResult();
        $cats = [];

        while($result->next())
            $cats[] = new ForumCategoryInfo($result);

        if($asTree)
            $cats = self::convertCategoryListToTree($cats);

        return $cats;
    }

    public function getCategory(
        ?string $categoryId = null,
        ForumTopicInfo|string|null $topicInfo = null,
        ForumPostInfo|string|null $postInfo = null
    ): ForumCategoryInfo {
        $hasCategoryId = $categoryId !== null;
        $hasTopicInfo = $topicInfo !== null;
        $hasPostInfo = $postInfo !== null;

        if(!$hasCategoryId && !$hasTopicInfo && !$hasPostInfo)
            throw new InvalidArgumentException('You must specify an argument.');
        if(($hasCategoryId && ($hasTopicInfo || $hasPostInfo))
            || ($hasTopicInfo && ($hasCategoryId || $hasPostInfo))
            || ($hasPostInfo && ($hasCategoryId || $hasTopicInfo)))
            throw new InvalidArgumentException('Only one argument may be specified.');

        $value = null;
        $query = 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories';
        if($hasCategoryId) {
            $query .= ' WHERE forum_id = ?';
            $value = $categoryId;
        }
        if($hasTopicInfo) {
            if($topicInfo instanceof ForumTopicInfo) {
                $query .= ' WHERE forum_id = ?';
                $value = $topicInfo->getCategoryId();
            } else {
                $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_topics WHERE topic_id = ?)';
                $value = $topicInfo;
            }
        }
        if($hasPostInfo) {
            if($postInfo instanceof ForumPostInfo) {
                $query .= ' WHERE forum_id = ?';
                $value = $postInfo->getCategoryId();
            } else {
                $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_posts WHERE post_id = ?)';
                $value = $postInfo;
            }
        }

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $value);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('Forum category info not found.');

        return new ForumCategoryInfo($result);
    }

    public function updateCategory(
        ForumCategoryInfo|string $categoryInfo
    ): void {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
    }

    public function deleteCategory(ForumCategoryInfo|string $categoryInfo): void {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
    }

    public function incrementCategoryClicks(ForumCategoryInfo|string $categoryInfo): void {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();

        // previous implementation also WHERE'd for forum_type = link but i don't think there's any other way to get here anyhow
        $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_link_clicks = forum_link_clicks + 1 WHERE forum_id = ? AND forum_link_clicks IS NOT NULL');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();
    }

    public function incrementCategoryTopics(ForumCategoryInfo|string $categoryInfo): void {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = forum_count_topics + 1 WHERE forum_id = ?');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();
    }

    public function incrementCategoryPosts(ForumCategoryInfo|string $categoryInfo): void {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_posts = forum_count_posts + 1 WHERE forum_id = ?');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();
    }

    public function getCategoryAncestry(
        ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
    ): array {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
            $categoryInfo = $categoryInfo->getCategoryId();

        $query = 'WITH RECURSIVE msz_cte_ancestry AS ('
            . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?'
            . ' UNION ALL'
            . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_ancestry AS ca ON fc.forum_id = ca.forum_parent'
            . ') SELECT * FROM msz_cte_ancestry';

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        $cats = [];

        while($result->next())
            $cats[] = new ForumCategoryInfo($result);

        return $cats;
    }

    public function getCategoryChildren(
        ForumCategoryInfo|string $parentInfo,
        bool $includeSelf = false,
        ?bool $hidden = null,
        bool $asTree = false
    ): array {
        if($parentInfo instanceof ForumCategoryInfo)
            $parentInfo = $parentInfo->getId();

        $hasHidden = $hidden !== null;

        $query = 'WITH RECURSIVE msz_cte_children AS ('
            . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?'
            . ' UNION ALL'
            . ' SELECT fc.forum_id, fc.forum_order, fc.forum_parent, fc.forum_name, fc.forum_type, fc.forum_description, fc.forum_icon, fc.forum_colour, fc.forum_link, fc.forum_link_clicks, UNIX_TIMESTAMP(fc.forum_created), fc.forum_archived, fc.forum_hidden, fc.forum_count_topics, fc.forum_count_posts FROM msz_forum_categories AS fc JOIN msz_cte_children AS cc ON fc.forum_parent = cc.forum_id'
            . ') SELECT * FROM msz_cte_children';

        $args = 0;
        if(!$includeSelf) {
            ++$args;
            $query .= ' WHERE forum_id <> ?';
        }
        if($hasHidden)
            $query .= sprintf(' %s forum_hidden %s 0', ++$args > 1 ? 'AND' : 'WHERE', $hidden ? '<>' : '=');
        $query .= ' ORDER BY forum_parent, forum_order';

        $args = 0;
        $stmt = $this->cache->get($query);
        $stmt->addParameter(++$args, $parentInfo);
        if(!$includeSelf)
            $stmt->addParameter(++$args, $parentInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        $cats = [];

        while($result->next())
            $cats[] = new ForumCategoryInfo($result);

        if($asTree)
            $cats = self::convertCategoryListToTree($cats, $parentInfo);

        return $cats;
    }

    public function checkCategoryUnread(
        ForumCategoryInfo|string|array $categoryInfos,
        UserInfo|string|null $userInfo
    ): bool {
        if($userInfo === null)
            return false;

        if(!is_array($categoryInfos))
            $categoryInfos = [$categoryInfos];
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $args = 0;
        $stmt = $this->cache->get(sprintf(
            'SELECT COUNT(*) FROM msz_forum_topics AS ft LEFT JOIN msz_forum_topics_track AS ftt ON ftt.topic_id = ft.topic_id AND ftt.user_id = ? WHERE ft.forum_id IN (%s) AND ft.topic_deleted IS NULL AND ft.topic_bumped >= NOW() - INTERVAL 1 MONTH AND (ftt.track_last_read IS NULL OR ftt.track_last_read < ft.topic_bumped)',
            DbTools::prepareListString($categoryInfos)
        ));
        $stmt->addParameter(++$args, $userInfo);
        foreach($categoryInfos as $categoryInfo) {
            if($categoryInfo instanceof ForumCategoryInfo)
                $stmt->addParameter(++$args, $categoryInfo->getId());
            elseif(is_string($categoryInfo) || is_int($categoryInfo))
                $stmt->addParameter(++$args, $categoryInfo);
            else
                throw new InvalidArgumentException('Invalid item in $categoryInfos.');
        }
        $stmt->execute();

        $result = $stmt->getResult();
        return $result->next() && $result->getInteger(0) > 0;
    }

    public function updateUserReadCategory(
        UserInfo|string|null $userInfo,
        ForumCategoryInfo|string $categoryInfo
    ): void {
        if($userInfo === null)
            return;

        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($categoryInfo instanceof $categoryInfo)
            $categoryInfo = $categoryInfo->getId();

        $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) SELECT ?, topic_id, forum_id, NOW() FROM msz_forum_topics WHERE forum_id = ? AND topic_bumped >= NOW() - INTERVAL 1 MONTH');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $categoryInfo);
        $stmt->execute();
    }

    public function getCategoryColour(
        ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
    ): Colour {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
            $categoryInfo = $categoryInfo->getCategoryId();

        $query = 'WITH RECURSIVE msz_cte_colours AS ('
            . 'SELECT forum_id, forum_parent, forum_colour FROM msz_forum_categories WHERE forum_id = ?'
            . ' UNION ALL'
            . ' SELECT fc.forum_id, fc.forum_parent, fc.forum_colour FROM msz_forum_categories AS fc JOIN msz_cte_colours AS cc ON fc.forum_id = cc.forum_parent'
            . ') SELECT forum_colour FROM msz_cte_colours WHERE forum_colour IS NOT NULL';

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        return $result->next() ? Colour::fromMisuzu($result->getInteger(0)) : Colour::none();
    }

    public function getMostActiveCategoryInfo(
        UserInfo|string $userInfo,
        array $exceptCategoryInfos = [],
        array $exceptTopicInfos = [],
        ?bool $deleted = null
    ): object {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasExceptCategoryInfos = !empty($exceptCategoryInfos);
        $hasExceptTopicInfos = !empty($exceptTopicInfos);
        $hasDeleted = $deleted !== null;

        $query = 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?';
        if($hasDeleted)
            $query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
        if($hasExceptCategoryInfos)
            $query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos));
        if($hasExceptTopicInfos)
            $query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos));
        $query .= ' GROUP BY forum_id ORDER BY post_count DESC LIMIT 1';

        $args = 0;
        $stmt = $this->cache->get($query);
        $stmt->addParameter(++$args, $userInfo);
        foreach($exceptCategoryInfos as $categoryInfo) {
            if($categoryInfo instanceof ForumCategoryInfo)
                $stmt->addParameter(++$args, $categoryInfo->getId());
            elseif(is_string($categoryInfo) || is_int($categoryInfo))
                $stmt->addParameter(++$args, (string)$categoryInfo);
            else
                throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.');
        }
        foreach($exceptTopicInfos as $topicInfo) {
            if($topicInfo instanceof ForumTopicInfo)
                $stmt->addParameter(++$args, $topicInfo->getId());
            elseif(is_string($topicInfo) || is_int($topicInfo))
                $stmt->addParameter(++$args, (string)$topicInfo);
            else
                throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.');
        }
        $stmt->execute();

        $result = $stmt->getResult();
        $info = new stdClass;
        $info->success = $result->next();
        if($info->success) {
            $info->categoryId = $result->getString(0);
            $info->postCount = $result->getInteger(1);
        }

        return $info;
    }

    public function syncForumCounters(
        ForumCategoryInfo|string|null $categoryInfo = null,
        bool $updateCounters = true
    ): object {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        elseif($categoryInfo === null)
            $categoryInfo = '0';

        $counters = new stdClass;

        $stmt = $this->cache->get('SELECT ? AS target_category_id, (SELECT COUNT(*) FROM msz_forum_topics WHERE forum_id = target_category_id AND topic_deleted IS NULL) AS count_topics, (SELECT COUNT(*) FROM msz_forum_posts WHERE forum_id = target_category_id AND post_deleted IS NULL) AS count_posts');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('Failed to fetch forum category counters.');

        $counters->topics = $result->getInteger(1);
        $counters->posts = $result->getInteger(2);

        $stmt = $this->cache->get('SELECT forum_id FROM msz_forum_categories WHERE forum_parent = ?');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->execute();

        $children = [];
        $result = $stmt->getResult();
        while($result->next())
            $children[] = $result->getString(0);

        foreach($children as $childId) {
            $childCounters = $this->syncForumCounters($childId, $updateCounters);
            $counters->topics += $childCounters->topics;
            $counters->posts += $childCounters->posts;
        }

        if($updateCounters && $categoryInfo !== '0') {
            $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = ?, forum_count_posts = ? WHERE forum_id = ?');
            $stmt->addParameter(1, $counters->topics);
            $stmt->addParameter(2, $counters->posts);
            $stmt->addParameter(3, $categoryInfo);
            $stmt->execute();
        }

        return $counters;
    }
}
