<?php
namespace Misuzu\News;

use JsonSerializable;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;

class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {};

class NewsPost implements JsonSerializable {
    // Database fields
    private $post_id = -1;
    private $category_id = -1;
    private $user_id = null;
    private $comment_section_id = null;
    private $post_is_featured = false;
    private $post_title = '';
    private $post_text = '';
    private $post_scheduled = null;
    private $post_created = null;
    private $post_updated = null;
    private $post_deleted = null;

    private $category = null;
    private $user = null;
    private $userLookedUp = false;
    private $comments = null;

    public const TABLE = 'news_posts';
    private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
    private const SELECT = '%1$s.`post_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_section_id`'
        . ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
        . ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
        . ', UNIX_TIMESTAMP(%1$s.`post_created`) AS `post_created`'
        . ', UNIX_TIMESTAMP(%1$s.`post_updated`) AS `post_updated`'
        . ', UNIX_TIMESTAMP(%1$s.`post_deleted`) AS `post_deleted`';

    public function getId(): int {
        return $this->post_id < 1 ? -1 : $this->post_id;
    }

    public function getCategoryId(): int {
        return $this->category_id < 1 ? -1 : $this->category_id;
    }
    public function setCategoryId(int $categoryId): self {
        $this->category_id = max(1, $categoryId);
        $this->category = null;
        return $this;
    }
    public function getCategory(): NewsCategory {
        if($this->category === null)
            $this->category = NewsCategory::byId($this->getCategoryId());
        return $this->category;
    }
    public function setCategory(NewsCategory $category): self {
        $this->category_id = $category->getId();
        $this->category = $category;
        return $this;
    }

    public function getUserId(): int {
        return $this->user_id < 1 ? -1 : $this->user_id;
    }
    public function setUserId(int $userId): self {
        $this->user_id = $userId < 1 ? null : $userId;
        $this->userLookedUp = false;
        $this->user = null;
        return $this;
    }
    public function getUser(): ?User {
        if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
            $this->userLookedUp = true;
            try {
                $this->user = User::byId($userId);
            } catch(UserNotFoundException $ex) {}
        }
        return $this->user;
    }
    public function setUser(?User $user): self {
        $this->user_id = $user === null ? null : $user->getId();
        $this->userLookedUp = true;
        $this->user = $user;
        return $this;
    }

    public function getCommentsCategoryId(): int {
        return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
    }
    public function hasCommentsCategory(): bool {
        return $this->getCommentsCategoryId() > 0;
    }
    public function getCommentsCategory(): CommentsCategory {
        if($this->comments === null)
            $this->comments = CommentsCategory::byId($this->getCommentsCategoryId());
        return $this->comments;
    }

    public function isFeatured(): bool {
        return $this->post_is_featured !== 0;
    }
    public function setFeatured(bool $featured): self {
        $this->post_is_featured = $featured ? 1 : 0;
        return $this;
    }

    public function getTitle(): string {
        return $this->post_title;
    }
    public function setTitle(string $title): self {
        $this->post_title = $title;
        return $this;
    }

    public function getText(): string {
        return $this->post_text;
    }
    public function setText(string $text): self {
        $this->post_text = $text;
        return $this;
    }
    public function getParsedText(): string {
        return Parser::instance(Parser::MARKDOWN)->parseText($this->getText());
    }
    public function getFirstParagraph(): string {
        return first_paragraph($this->getText());
    }
    public function getParsedFirstParagraph(): string {
        return Parser::instance(Parser::MARKDOWN)->parseText($this->getFirstParagraph());
    }

    public function getScheduledTime(): int {
        return $this->post_scheduled === null ? -1 : $this->post_scheduled;
    }
    public function setScheduledTime(int $scheduled): self {
        $time = ($time = $this->getCreatedTime()) < 0 ? time() : $time;
        $this->post_scheduled = $scheduled < $time ? $time : $scheduled;
        return $this;
    }
    public function isPublished(): bool {
        return $this->getScheduledTime() < time();
    }

    public function getCreatedTime(): int {
        return $this->post_created === null ? -1 : $this->post_created;
    }

    public function getUpdatedTime(): int {
        return $this->post_updated === null ? -1 : $this->post_updated;
    }
    public function isEdited(): bool {
        return $this->getUpdatedTime() >= 0;
    }

    public function getDeletedTime(): int {
        return $this->post_deleted === null ? -1 : $this->post_deleted;
    }
    public function isDeleted(): bool {
        return $this->getDeletedTime() >= 0;
    }
    public function setDeleted(bool $isDeleted): self {
        if($this->isDeleted() !== $isDeleted)
            $this->post_deleted = $isDeleted ? time() : null;
        return $this;
    }

    public function jsonSerialize(): mixed {
        return [
            'id'          => $this->getId(),
            'category'    => $this->getCategoryId(),
            'user'        => $this->getUserId(),
            'comments'    => $this->getCommentsCategoryId(),
            'is_featured' => $this->isFeatured(),
            'title'       => $this->getTitle(),
            'text'        => $this->getText(),
            'scheduled'   => ($time = $this->getScheduledTime()) < 0 ? null : date('c', $time),
            'created'     => ($time = $this->getCreatedTime())   < 0 ? null : date('c', $time),
            'updated'     => ($time = $this->getUpdatedTime())   < 0 ? null : date('c', $time),
            'deleted'     => ($time = $this->getDeletedTime())   < 0 ? null : date('c', $time),
        ];
    }

    public function ensureCommentsCategory(): void {
        if($this->hasCommentsCategory())
            return;

        $this->comments = new CommentsCategory("news-{$this->getId()}");
        $this->comments->save();

        $this->comment_section_id = $this->comments->getId();
        DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
            ->execute([
                'comment_section_id' => $this->getCommentsCategoryId(),
                'post_id' => $this->getId(),
            ]);
    }

    public function save(): void {
        $isInsert = $this->getId() < 1;
        if($isInsert) {
            $query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `post_is_featured`, `post_title`'
                . ', `post_text`, `post_scheduled`, `post_deleted`) VALUES'
                . ' (:category, :user, :featured, :title, :text, FROM_UNIXTIME(:scheduled), FROM_UNIXTIME(:deleted))';
        } else {
            $query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `post_is_featured` = :featured'
                . ', `post_title` = :title, `post_text` = :text, `post_scheduled` = FROM_UNIXTIME(:scheduled)'
                . ', `post_deleted` = FROM_UNIXTIME(:deleted)'
                . ' WHERE `post_id` = :post';
        }

        $savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
            ->bind('category', $this->category_id)
            ->bind('user', $this->user_id)
            ->bind('featured', $this->post_is_featured)
            ->bind('title', $this->post_title)
            ->bind('text', $this->post_text)
            ->bind('scheduled', $this->post_scheduled)
            ->bind('deleted', $this->post_deleted);

        if($isInsert) {
            $this->post_id = $savePost->executeGetId();
            $this->post_created = time();
        } else {
            $this->post_updated = time();
            $savePost->bind('post', $this->getId())
                ->execute();
        }
    }

    private static function countQueryBase(): string {
        return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`post_id`)', self::TABLE));
    }
    public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int {
        return (int)DB::prepare(self::countQueryBase()
            . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)'
            . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
            . ($includeDeleted   ? '' : ' AND `post_deleted` IS NULL'))
            ->bind('only_featured', $onlyFeatured ? 1 : 0)
            ->fetchColumn();
    }
    public static function countByCategory(NewsCategory $category, bool $includeScheduled = false, bool $includeDeleted = false): int {
        return (int)DB::prepare(self::countQueryBase()
            . ' WHERE `category_id` = :cat_id'
            . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
            . ($includeDeleted   ? '' : ' AND `post_deleted` IS NULL'))
            ->bind('cat_id', $category->getId())
            ->fetchColumn();
    }

    private static function byQueryBase(): string {
        return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
    }
    public static function byId(int $postId): self {
        $post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id')
            ->bind('post_id', $postId)
            ->fetchObject(self::class);
        if(!$post)
            throw new NewsPostNotFoundException;
        return $post;
    }
    public static function bySearchQuery(string $query, bool $includeScheduled = false, bool $includeDeleted = false): array {
        return DB::prepare(
            self::byQueryBase()
            . ' WHERE MATCH(`post_title`, `post_text`) AGAINST (:query IN NATURAL LANGUAGE MODE)'
            . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
            . ($includeDeleted   ? '' : ' AND `post_deleted` IS NULL')
            . ' ORDER BY `post_id` DESC'
        )   ->bind('query', $query)
            ->fetchObjects(self::class);
    }
    public static function byCategory(NewsCategory $category, ?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array {
        $postsQuery = self::byQueryBase()
            . ' WHERE `category_id` = :cat_id'
            . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
            . ($includeDeleted   ? '' : ' AND `post_deleted` IS NULL')
            . ' ORDER BY `post_id` DESC';

        if($pagination !== null)
            $postsQuery .= ' LIMIT :range OFFSET :offset';

        $getPosts = DB::prepare($postsQuery)
            ->bind('cat_id', $category->getId());

        if($pagination !== null)
            $getPosts->bind('range', $pagination->getRange())
                ->bind('offset', $pagination->getOffset());

        return $getPosts->fetchObjects(self::class);
    }
    public static function all(?Pagination $pagination = null, bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): array {
        $postsQuery = self::byQueryBase()
            . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)'
            . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()')
            . ($includeDeleted   ? '' : ' AND `post_deleted` IS NULL')
            . ' ORDER BY `post_id` DESC';

        if($pagination !== null)
            $postsQuery .= ' LIMIT :range OFFSET :offset';

        $getPosts = DB::prepare($postsQuery)
            ->bind('only_featured', $onlyFeatured ? 1 : 0);

        if($pagination !== null)
            $getPosts->bind('range', $pagination->getRange())
                ->bind('offset', $pagination->getOffset());

        return $getPosts->fetchObjects(self::class);
    }
}
