<?php
namespace Misuzu\News;

use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategoryInfo;
use Misuzu\Users\UserInfo;

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

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

    public function countCategories(
        ?bool $hidden = null
    ): int {
        $hasHidden = $hidden !== null;

        $query = 'SELECT COUNT(*) FROM msz_news_categories';
        if($hasHidden)
            $query .= sprintf(' WHERE category_is_hidden %s 0', $hidden ? '<>' : '=');

        $result = $this->dbConn->query($query);
        $count = 0;

        if($result->next())
            $count = $result->getInteger(0);

        return $count;
    }

    public function getCategories(
        ?bool $hidden = null,
        ?Pagination $pagination = null
    ): array {
        $hasHidden = $hidden !== null;
        $hasPagination = $pagination !== null;

        $query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created), (SELECT COUNT(*) FROM msz_news_posts AS np WHERE np.category_id = nc.category_id) AS category_posts_count FROM msz_news_categories AS nc';
        if($hasHidden)
            $query .= sprintf(' WHERE category_is_hidden %s 0', $hidden ? '<>' : '=');
        $query .= ' ORDER BY category_created ASC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';
        $stmt = $this->cache->get($query);

        $args = 0;
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }

        $stmt->execute();

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

        while($result->next())
            $categories[] = new NewsCategoryInfo($result);

        return $categories;
    }

    public function getCategory(
        ?string $categoryId = null,
        NewsPostInfo|string|null $postInfo = null
    ): NewsCategoryInfo {
        $hasCategoryId = $categoryId !== null;
        $hasPostInfo = $postInfo !== null;

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

        $value = null;
        $query = 'SELECT category_id, category_name, category_description, category_is_hidden, UNIX_TIMESTAMP(category_created) FROM msz_news_categories';

        if($hasCategoryId) {
            $query .= ' WHERE category_id = ?';
            $value = $categoryId;
        }
        if($hasPostInfo) {
            if($postInfo instanceof NewsPostInfo) {
                $query .= ' WHERE category_id = ?';
                $value = $postInfo->getCategoryId();
            } else {
                $query .= ' WHERE category_id = (SELECT category_id FROM msz_news_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('News category not found.');

        return new NewsCategoryInfo($result);
    }

    public function createCategory(
        string $name,
        string $description,
        bool $hidden
    ): NewsCategoryInfo {
        $name = trim($name);
        if(empty($name))
            throw new InvalidArgumentException('$name may not be empty');

        $description = trim($description);
        if(empty($description))
            throw new InvalidArgumentException('$description may not be empty');

        $stmt = $this->cache->get('INSERT INTO msz_news_categories (category_name, category_description, category_is_hidden) VALUES (?, ?, ?)');
        $stmt->addParameter(1, $name);
        $stmt->addParameter(2, $description);
        $stmt->addParameter(3, $hidden ? 1 : 0);
        $stmt->execute();

        return $this->getCategory(categoryId: (string)$this->dbConn->getLastInsertId());
    }

    public function deleteCategory(NewsCategoryInfo|string $infoOrId): void {
        if($infoOrId instanceof NewsCategoryInfo)
            $infoOrId = $infoOrId->getId();

        $stmt = $this->cache->get('DELETE FROM msz_news_categories WHERE category_id = ?');
        $stmt->addParameter(1, $infoOrId);
        $stmt->execute();
    }

    public function updateCategory(
        NewsCategoryInfo|string $infoOrId,
        ?string $name = null,
        ?string $description = null,
        ?bool $hidden = null
    ): void {
        if($infoOrId instanceof NewsCategoryInfo)
            $infoOrId = $infoOrId->getId();

        if($name !== null) {
            $name = trim($name);
            if(empty($name))
                throw new InvalidArgumentException('$name may not be empty');
        }

        if($description !== null) {
            $description = trim($description);
            if(empty($description))
                throw new InvalidArgumentException('$description may not be empty');
        }

        $hasHidden = $hidden !== null;

        $stmt = $this->cache->get('UPDATE msz_news_categories SET category_name = COALESCE(?, category_name), category_description = COALESCE(?, category_description), category_is_hidden = IF(?, ?, category_is_hidden) WHERE category_id = ?');
        $stmt->addParameter(1, $name);
        $stmt->addParameter(2, $description);
        $stmt->addParameter(3, $hasHidden ? 1 : 0);
        $stmt->addParameter(4, $hidden ? 1 : 0);
        $stmt->addParameter(5, $infoOrId);
        $stmt->execute();
    }

    public function countPosts(
        NewsCategoryInfo|string|null $categoryInfo = null,
        bool $onlyFeatured = false,
        bool $includeScheduled = false,
        bool $includeDeleted = false
    ): int {
        if($categoryInfo instanceof NewsCategoryInfo)
            $categoryInfo = $categoryInfo->getId();

        $hasCategoryInfo = $categoryInfo !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_news_posts';
        if($hasCategoryInfo) {
            ++$args;
            $query .= ' WHERE category_id = ?';
        }
        if($onlyFeatured) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_is_featured = 1';
        }
        if(!$includeScheduled) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_scheduled <= NOW()';
        }
        if(!$includeDeleted) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_deleted IS NULL';
        }

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

        $result = $stmt->getResult();
        $count = 0;

        if($result->next())
            $count = $result->getInteger(0);

        return $count;
    }

    public function getPosts(
        NewsCategoryInfo|string|null $categoryInfo = null,
        string $searchQuery = null,
        bool $onlyFeatured = false,
        bool $includeScheduled = false,
        bool $includeDeleted = false,
        ?Pagination $pagination = null
    ): array {
        if($categoryInfo instanceof NewsCategoryInfo)
            $categoryInfo = $categoryInfo->getId();

        $hasCategoryInfo = $categoryInfo !== null;
        $hasSearchQuery = $searchQuery !== null;
        $hasPagination = $pagination !== null;

        $args = 0;
        $query = 'SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts';
        if($hasCategoryInfo) {
            ++$args;
            $query .= ' WHERE category_id = ?';
        }
        if($hasSearchQuery) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' MATCH(post_title, post_text) AGAINST (? IN NATURAL LANGUAGE MODE)';
        }
        if($onlyFeatured) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_is_featured = 1';
        }
        if(!$includeScheduled) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_scheduled <= NOW()';
        }
        if(!$includeDeleted) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' post_deleted IS NULL';
        }
        $query .= ' ORDER BY post_scheduled DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';
        $stmt = $this->cache->get($query);

        $args = 0;
        if($hasCategoryInfo)
            $stmt->addParameter(++$args, $categoryInfo);
        if($hasSearchQuery)
            $stmt->addParameter(++$args, $searchQuery);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }

        $stmt->execute();

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

        while($result->next())
            $posts[] = new NewsPostInfo($result);

        return $posts;
    }

    public function getPost(string $postId): NewsPostInfo {
        $stmt = $this->cache->get('SELECT post_id, category_id, user_id, comment_section_id, post_is_featured, post_title, post_text, UNIX_TIMESTAMP(post_scheduled), UNIX_TIMESTAMP(post_created), UNIX_TIMESTAMP(post_updated), UNIX_TIMESTAMP(post_deleted) FROM msz_news_posts WHERE post_id = ?');
        $stmt->addParameter(1, $postId);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('No news post with that ID exists.');

        return new NewsPostInfo($result);
    }

    public function createPost(
        NewsCategoryInfo|string $categoryInfo,
        string $title,
        string $body,
        bool $featured = false,
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $schedule = null
    ): NewsPostInfo {
        if($categoryInfo instanceof NewsCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($schedule instanceof DateTime)
            $schedule = $schedule->getUnixTimeSeconds();

        $title = trim($title);
        if(empty($title))
            throw new InvalidArgumentException('$title may not be empty');

        $body = trim($body);
        if(empty($body))
            throw new InvalidArgumentException('$body may not be empty');

        $stmt = $this->cache->get('INSERT INTO msz_news_posts (category_id, user_id, post_is_featured, post_title, post_text, post_scheduled) VALUES (?, ?, ?, ?, ?, ?)');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->addParameter(2, $userInfo);
        $stmt->addParameter(3, $featured ? 1 : 0);
        $stmt->addParameter(4, $title);
        $stmt->addParameter(5, $body);
        $stmt->addParameter(6, $schedule);
        $stmt->execute();

        return $this->getPost((string)$this->dbConn->getLastInsertId());
    }

    public function deletePost(NewsPostInfo|string $postInfo): void {
        if($postInfo instanceof NewsPostInfo)
            $postInfo = $postInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?');
        $stmt->addParameter(1, $postInfo);
        $stmt->execute();
    }

    public function restorePost(NewsPostInfo|string $postInfo): void {
        if($postInfo instanceof NewsPostInfo)
            $postInfo = $postInfo->getId();

        $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = NULL WHERE post_id = ?');
        $stmt->addParameter(1, $postInfo);
        $stmt->execute();
    }

    public function nukePost(NewsPostInfo|string $postInfo): void {
        if($postInfo instanceof NewsPostInfo)
            $postInfo = $postInfo->getId();

        // should this enforce a soft delete first? (AND post_deleted IS NOT NULL)
        $stmt = $this->cache->get('DELETE FROM msz_news_posts WHERE post_id = ?');
        $stmt->addParameter(1, $postInfo);
        $stmt->execute();
    }

    public function updatePost(
        NewsPostInfo|string $postInfo,
        NewsCategoryInfo|string|null $categoryInfo = null,
        ?string $title = null,
        ?string $body = null,
        ?bool $featured = null,
        bool $updateUserInfo = false,
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $schedule = null
    ): void {
        if($postInfo instanceof NewsPostInfo)
            $postInfo = $postInfo->getId();
        if($categoryInfo instanceof NewsCategoryInfo)
            $categoryInfo = $categoryInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($schedule instanceof DateTime)
            $schedule = $schedule->getUnixTimeSeconds();

        if($title !== null) {
            $title = trim($title);
            if(empty($title))
                throw new InvalidArgumentException('$title may not be empty');
        }

        if($body !== null) {
            $body = trim($body);
            if(empty($body))
                throw new InvalidArgumentException('$body may not be empty');
        }

        $hasFeatured = $featured !== null;

        $stmt = $this->cache->get('UPDATE msz_news_posts SET category_id = COALESCE(?, category_id), user_id = IF(?, ?, user_id), post_is_featured = IF(?, ?, post_is_featured), post_title = COALESCE(?, post_title), post_text = COALESCE(?, post_text), post_scheduled = COALESCE(?, post_scheduled) WHERE post_id = ?');
        $stmt->addParameter(1, $categoryInfo);
        $stmt->addParameter(2, $updateUserInfo ? 1 : 0);
        $stmt->addParameter(3, $userInfo);
        $stmt->addParameter(4, $hasFeatured ? 1 : 0);
        $stmt->addParameter(5, $featured ? 1 : 0);
        $stmt->addParameter(6, $title);
        $stmt->addParameter(7, $body);
        $stmt->addParameter(8, $schedule);
        $stmt->addParameter(9, $postInfo);
        $stmt->execute();
    }

    public function updatePostCommentCategory(
        NewsPostInfo|string $postInfo,
        CommentsCategoryInfo|string $commentsCategory
    ): void {
        if($postInfo instanceof NewsPostInfo)
            $postInfo = $postInfo->getId();
        if($commentsCategory instanceof CommentsCategoryInfo)
            $commentsCategory = $commentsCategory->getId();

        $stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ? WHERE post_id = ?');
        $stmt->addParameter(1, $commentsCategory);
        $stmt->addParameter(2, $postInfo);
        $stmt->execute();
    }
}
