<?php
namespace Misuzu\Forum;

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

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

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

    public function countTopics(
        ForumCategoryInfo|string|array|null $categoryInfo = null,
        UserInfo|string|null $userInfo = null,
        ?bool $global = null,
        ?bool $deleted = null
    ): int {
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasCategoryInfo = $categoryInfo !== null;
        $hasUserInfo = $userInfo !== null;
        $hasGlobal = $global !== null;
        $hasDeleted = $deleted !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_forum_topics';
        if($hasCategoryInfo || $hasGlobal) {
            ++$args;

            // wow this sucks
            $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal;
            $query .= ' WHERE ';
            if($hasGlobalAndCategory)
                $query .= '(';

            if($hasCategoryInfo) {
                if(is_array($categoryInfo))
                    $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo));
                else
                    $query .= 'forum_id = ?';
            }

            if($hasGlobalAndCategory)
                $query .= ' OR ';

            if($hasGlobal) // not sure why you would ever set this to false, but consistency!
                $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL);

            if($hasGlobalAndCategory)
                $query .= ')';
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasDeleted)
            $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasCategoryInfo) {
            if(is_array($categoryInfo)) {
                foreach($categoryInfo as $categoryInfoEntry)
                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry);
            } else
                $stmt->addParameter(++$args, $categoryInfo);
        }
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        $stmt->execute();

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

    public function getTopics(
        ForumCategoryInfo|string|array|null $categoryInfo = null,
        UserInfo|string|null $userInfo = null,
        ?array $searchQuery = null,
        ?bool $global = null,
        ?bool $deleted = null,
        ?Pagination $pagination = null
    ): array {
        // remove this hack when search server
        $hasSearchQuery = $searchQuery !== null;
        $hasAfterTopicId = false;
        $afterTopicId = null;
        $doSearchOrder = false;
        if($hasSearchQuery) {
            if(!empty($searchQuery['type'])
                && $searchQuery['type'] !== 'forum'
                && $searchQuery['type'] !== 'forum:topic')
                return [];

            $deleted = false;
            $pagination = null;
            $doSearchOrder = true;

            if(!empty($searchQuery['author']))
                $userInfo = $searchQuery['author'];

            if(!empty($searchQuery['after'])) {
                $hasAfterTopicId = true;
                $afterTopicId = $searchQuery['after'];
            }

            $searchQuery = $searchQuery['query_string'];
            $hasSearchQuery = !empty($searchQuery);
        }

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

        $hasCategoryInfo = $categoryInfo !== null;
        $hasUserInfo = $userInfo !== null;
        $hasGlobal = $global !== null;
        $hasDeleted = $deleted !== null;
        $hasPagination = $pagination !== null;

        $args = 0;
        $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft';
        if($hasCategoryInfo || $hasGlobal) {
            ++$args;

            // wow this sucks
            $hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal;
            $query .= ' WHERE ';
            if($hasGlobalAndCategory)
                $query .= '(';

            if($hasCategoryInfo) {
                if(is_array($categoryInfo))
                    $query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo));
                else
                    $query .= 'forum_id = ?';
            }

            if($hasGlobalAndCategory)
                $query .= ' OR ';

            if($hasGlobal) // not sure why you would ever set this to false, but consistency!
                $query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL);

            if($hasGlobalAndCategory)
                $query .= ')';
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasAfterTopicId)
            $query .= sprintf(' %s topic_id > ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasSearchQuery)
            $query .= sprintf(' %s MATCH(topic_title) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasDeleted)
            $query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
        if($doSearchOrder) {
            $query .= ' ORDER BY topic_id ASC LIMIT 20';
        } else {
            $query .= ' ORDER BY topic_type DESC, topic_bumped DESC';
            if($hasPagination)
                $query .= ' LIMIT ? OFFSET ?';
        }

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasCategoryInfo) {
            if(is_array($categoryInfo)) {
                foreach($categoryInfo as $categoryInfoEntry)
                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry);
            } else
                $stmt->addParameter(++$args, $categoryInfo);
        }
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasAfterTopicId)
            $stmt->addParameter(++$args, $afterTopicId);
        if($hasSearchQuery)
            $stmt->addParameter(++$args, $searchQuery);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }
        $stmt->execute();

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

        while($result->next())
            $topics[] = new ForumTopicInfo($result);

        return $topics;
    }

    public function getTopic(
        ?string $topicId = null,
        ForumPostInfo|string|null $postInfo = null
    ): ForumTopicInfo {
        $hasTopicId = $topicId !== null;
        $hasPostInfo = $postInfo !== null;

        if(!$hasTopicId && !$hasPostInfo)
            throw new InvalidArgumentException('At least one argument must be specified.');
        if($hasTopicId && $hasPostInfo)
            throw new InvalidArgumentException('Only one argument may be specified.');

        $value = null;
        $query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft';
        if($hasTopicId) {
            $query .= ' WHERE topic_id = ?';
            $value = $topicId;
        }
        if($hasPostInfo) {
            if($postInfo instanceof ForumPostInfo) {
                $query .= ' WHERE topic_id = ?';
                $value = $postInfo->getTopicId();
            } else {
                $query .= ' WHERE topic_id = (SELECT topic_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 topic not found.');

        return new ForumTopicInfo($result);
    }

    public function createTopic(
        ForumCategoryInfo|string $categoryInfo,
        UserInfo|string|null $userInfo,
        string $title,
        string|int $type = ForumTopicInfo::TYPE_DISCUSSION
    ): ForumTopicInfo {
        if(is_string($type)) {
            if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES))
                throw new InvalidArgumentException('$type is not a valid alias.');
            $type = ForumTopicInfo::TYPE_ALIASES[$type];
        }
        if($categoryInfo instanceof ForumCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $stmt = $this->cache->get('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title) VALUES (?, ?, ?, ?)');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->addParameter(2, $userInfo);
        $stmt->addParameter(3, $type);
        $stmt->addParameter(4, $title);
        $stmt->execute();

        return $this->getTopic(topicId: (string)$this->dbConn->getLastInsertId());
    }

    public function updateTopic(
        ForumTopicInfo|string $topicInfo,
        ?string $title = null,
        string|int|null $type = null
    ): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $fields = [];
        $values = [];

        if($title !== null) {
            $fields[] = 'topic_title = ?';
            $values[] = $title;
        }

        if($type !== null) {
            if(is_string($type)) {
                if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES))
                    throw new InvalidArgumentException('$type is not a valid type alias.');

                $type = ForumTopicInfo::TYPE_ALIASES[$type];
            }

            $fields[] = 'topic_type = ?';
            $values[] = $type;
        }

        if(empty($fields))
            return;

        $args = 0;
        $stmt = $this->cache->get(sprintf('UPDATE msz_forum_topics SET %s WHERE topic_id = ?', implode(', ', $fields)));
        foreach($values as $value)
            $stmt->addParameter(++$args, $value);
        $stmt->addParameter(++$args, $topicInfo);
        $stmt->execute();
    }

    public function incrementTopicViews(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_count_views = topic_count_views + 1 WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function bumpTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_bumped = NOW() WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function lockTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NOW() WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function unlockTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NULL WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function deleteTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = COALESCE(topic_deleted, NOW()) WHERE topic_id = ? AND topic_deleted IS NULL');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();

        $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id) WHERE topic_id = ? AND post_deleted = NULL');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function restoreTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = NULL WHERE topic_id = ? AND post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id)');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();

        $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = NULL WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function nukeTopic(ForumTopicInfo|string $topicInfo): void {
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();

        $stmt = $this->cache->get('DELETE FROM msz_forum_topics WHERE topic_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->execute();
    }

    public function checkTopicParticipated(
        ForumTopicInfo|string $topicInfo,
        UserInfo|string|null $userInfo
    ): bool {
        if($userInfo === null)
            return false;
        if($topicInfo instanceof ForumTopicInfo)
            $topicInfo = $topicInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ? AND user_id = ?');
        $stmt->addParameter(1, $topicInfo);
        $stmt->addParameter(2, $userInfo);
        $stmt->execute();
        $result = $stmt->getResult();

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

    public function checkTopicUnread(
        ForumTopicInfo|string $topicInfo,
        UserInfo|string|null $userInfo
    ): bool {
        if($userInfo === null)
            return false;

        $topicInfoIsInstance = $topicInfo instanceof ForumTopicInfo;
        if($topicInfoIsInstance && !$topicInfo->isActive())
            return false;

        $query = 'SELECT UNIX_TIMESTAMP(track_last_read) FROM msz_forum_topics_track AS ftt WHERE user_id = ? AND topic_id = ?';
        if(!$topicInfoIsInstance)
            $query .= ' AND track_last_read = (SELECT topic_bumped FROM msz_forum_topics WHERE topic_id = ftt.topic_id AND topic_bumped >= NOW() - INTERVAL 1 MONTH)';

        $stmt = $this->cache->get($query);
        $stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
        $stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->getId() : $topicInfo);
        $stmt->execute();
        $result = $stmt->getResult();

        // user has never read this topic, return unread
        if(!$result->next())
            return true;

        return $result->getInteger(0) < $topicInfo->getBumpedTime();
    }

    public function getMostActiveTopicInfo(
        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 topic_id, 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 topic_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->topicId = $result->getString(0);
            $info->categoryId = $result->getString(1);
            $info->postCount = $result->getInteger(2);
        }

        return $info;
    }

    public function checkUserHasReadTopic(
        UserInfo|string|null $userInfo,
        ForumTopicInfo|string $topicInfo
    ): bool {
        // this method is primarily used to check if we should increment the view count
        // guests shouldn't increment it so we just
        if($userInfo === null)
            return true;

        $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_track WHERE topic_id = ? AND user_id = ?');
        $stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->getId() : $topicInfo);
        $stmt->addParameter(2, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
        $stmt->execute();
        $result = $stmt->getResult();

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

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

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

        if($topicInfo instanceof ForumTopicInfo) {
            $categoryInfo = $topicInfo->getCategoryId();
            $topicInfo = $topicInfo->getId();
        } else {
            if($categoryInfo === null)
                throw new InvalidArgumentException('$categoryInfo must be specified if $topicInfo is not an instance of ForumTopicInfo.');
            if($categoryInfo instanceof ForumCategoryInfo)
                $categoryInfo = $categoryInfo->getId();
        }

        $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) VALUES (?, ?, ?, NOW())');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $topicInfo);
        $stmt->addParameter(3, $categoryInfo);
        $stmt->execute();
    }
}
