<?php
require_once 'DatabaseInitializer.php';
require_once "CommentManagerDAO.php";
require_once "Comment.php";

/**
 * Verwaltet die Speicherung und das Laden von Kommentaren
 * über eine SQLite-Datenbank.
 *
 * @author Caroline Schulte
 */
class DatabaseCommentManager implements CommentManagerDAO
{
    private static $instance = null;
    private $dbPath;

    /**
     * Erstellt die Kommentartabelle, falls diese noch nicht existiert.
     */
    private function __construct()
    {
        $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
        DatabaseInitializer::initialize($this->dbPath);
    }

    /**
     * Baut die Verbindung zur SQLite-Datenbank auf.
     *
     * @return PDO Datenbankverbindung
     */
    private function getConnection()
    {
        try {
            $db = new PDO('sqlite:' . $this->dbPath, null, null);
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $db->exec('PRAGMA foreign_keys = ON;');
            return $db;
        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }

    /**
     * Gibt die Singleton-Instanz zurück.
     *
     * @return DatabaseCommentManager
     */
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new DatabaseCommentManager();
        }

        return self::$instance;
    }

    /**
     * Speichert einen neuen Kommentar oder eine Antwort.
     *
     * @param int $articleId ID des Beitrags
     * @param string $author Autor des Kommentars
     * @param string $content Inhalt des Kommentars
     * @param int|null $parentCommentId ID des Eltern-Kommentars oder null
     *
     * @return int ID des neu gespeicherten Kommentars
     */
    public function addComment(
        $articleId,
        $author,
        $content,
        $parentCommentId = null
    ) {
        try {
            $db = $this->getConnection();

            if ($parentCommentId === "" || $parentCommentId === 0 || $parentCommentId === "0") {
                $parentCommentId = null;
            }

            $sql = "
                INSERT INTO comments (
                    article_id,
                    parent_comment_id,
                    author,
                    content
                )
                VALUES (
                    :articleId,
                    :parentCommentId,
                    :author,
                    :content
                )
            ";

            $command = $db->prepare($sql);

            $command->execute([
                ":articleId" => $articleId,
                ":parentCommentId" => $parentCommentId,
                ":author" => $author,
                ":content" => $content
            ]);

            return intval($db->lastInsertId());

        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }

    /**
     * Lädt alle Kommentare eines Beitrags.
     *
     * @param int $articleId ID des Beitrags
     *
     * @return Comment[]
     */
    public function getCommentsByArticle($articleId)
    {
        try {
            $db = $this->getConnection();

            $sql = "
                SELECT
                    id,
                    article_id,
                    CASE
                        WHEN parent_comment_id IS NULL THEN NULL
                        WHEN parent_comment_id = '' THEN NULL
                        WHEN parent_comment_id = 0 THEN NULL
                        ELSE parent_comment_id
                    END AS parent_comment_id,
                    author,
                    content,
                    created
                FROM comments
                WHERE article_id = :articleId
                ORDER BY created ASC
            ";

            $command = $db->prepare($sql);
            $command->execute([":articleId" => $articleId]);

            return $this->mapRowsToComments($command);

        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }

    /**
     * Lädt alle Kommentare eines Autors.
     *
     * @param string $author E-Mail-Adresse des Autors
     *
     * @return Comment[]
     */
    public function getCommentsByAuthor($author)
    {
        try {
            $db = $this->getConnection();

            $sql = "
                SELECT
                    id,
                    article_id,
                    CASE
                        WHEN parent_comment_id IS NULL THEN NULL
                        WHEN parent_comment_id = '' THEN NULL
                        WHEN parent_comment_id = 0 THEN NULL
                        ELSE parent_comment_id
                    END AS parent_comment_id,
                    author,
                    content,
                    created
                FROM comments
                WHERE author = :author
                ORDER BY created DESC
            ";

            $command = $db->prepare($sql);
            $command->execute([":author" => $author]);

            return $this->mapRowsToComments($command);

        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }

    /**
     * Wandelt Datenbankzeilen in Comment-Objekte um.
     *
     * @param PDOStatement $command Ausgeführtes Statement
     *
     * @return Comment[]
     */
    private function mapRowsToComments($command)
    {
        $comments = [];

        while ($row = $command->fetch(PDO::FETCH_ASSOC)) {
            $parentCommentId = null;

            if (
                isset($row["parent_comment_id"])
                && $row["parent_comment_id"] !== null
                && $row["parent_comment_id"] !== ""
                && intval($row["parent_comment_id"]) !== 0
            ) {
                $parentCommentId = intval($row["parent_comment_id"]);
            }

            $comments[] = new Comment(
                intval($row["id"]),
                intval($row["article_id"]),
                $parentCommentId,
                $row["author"],
                $row["content"],
                $row["created"]
            );
        }

        return $comments;
    }
    /**
     * Bearbeitet einen eigenen Kommentar.
     *
     * Der Kommentar wird nur geändert, wenn er dem
     * angemeldeten Nutzer gehört.
     *
     * @param int $commentId ID des Kommentars
     * @param string $author E-Mail-Adresse des Autors
     * @param string $content Neuer Kommentarinhalt
     * @return bool true, wenn der Kommentar bearbeitet wurde
     */
    public function updateComment(
        int $commentId,
        string $author,
        string $content
    ): bool {
        try {
            $db = $this->getConnection();

            $command = $db->prepare("
            UPDATE comments
            SET content = :content
            WHERE id = :commentId
              AND author = :author
        ");

            $command->execute([
                ":content" => $content,
                ":commentId" => $commentId,
                ":author" => $author
            ]);

            return $command->rowCount() > 0;

        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }
    /**
     * Löscht einen eigenen Kommentar.
     *
     * Hat der Kommentar Antworten, wird er anonymisiert.
     * Hat er keine Antworten, wird er vollständig gelöscht.
     *
     * @param int $commentId ID des Kommentars
     * @param string $author E-Mail-Adresse des Autors
     * @return bool true, wenn der Kommentar gelöscht wurde
     */
    public function deleteComment(int $commentId, string $author): bool
    {
        try {
            $db = $this->getConnection();

            /*
             * Zuerst wird geprüft, ob der Kommentar existiert
             * und wirklich dem angemeldeten Nutzer gehört.
             */
            $checkCommand = $db->prepare("
            SELECT id
            FROM comments
            WHERE id = :commentId
              AND author = :author
        ");

            $checkCommand->execute([
                ":commentId" => $commentId,
                ":author" => $author
            ]);

            if ($checkCommand->fetch() === false) {
                return false;
            }

            /*
             * Danach wird geprüft, ob Antworten auf den Kommentar existieren.
             */
            $replyCommand = $db->prepare("
            SELECT COUNT(*)
            FROM comments
            WHERE parent_comment_id = :commentId
        ");

            $replyCommand->execute([
                ":commentId" => $commentId
            ]);

            $hasReplies = (int) $replyCommand->fetchColumn() > 0;

            if ($hasReplies) {
                /*
                 * Der Kommentar wird für den Kommentarbaum benötigt.
                 * Deshalb bleibt er als anonymer Platzhalter erhalten.
                 */
                $deleteCommand = $db->prepare("
                UPDATE comments
                SET author = NULL,
                    content = 'Dieser Kommentar wurde gelöscht.'
                WHERE id = :commentId
                  AND author = :author
            ");
            } else {
                /*
                 * Ohne Antworten kann der Kommentar vollständig
                 * aus der Datenbank entfernt werden.
                 */
                $deleteCommand = $db->prepare("
                DELETE FROM comments
                WHERE id = :commentId
                  AND author = :author
            ");
            }

            $deleteCommand->execute([
                ":commentId" => $commentId,
                ":author" => $author
            ]);

            return $deleteCommand->rowCount() > 0;

        } catch (PDOException $e) {
            throw new RuntimeException("internal_error");
        }
    }
    /**
     * Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
     *
     * Kommentare ohne Antworten werden vollständig gelöscht.
     * Kommentare mit Antworten bleiben als anonyme Platzhalter erhalten.
     *
     * @param string $author E-Mail-Adresse des Nutzers
     * @return void
     */
    public function deleteCommentsByAuthor(string $author): void
    {
        $db = $this->getConnection();

        try {
            $db->beginTransaction();

            /*
             * Zuerst werden alle Kommentare ohne Antworten gelöscht.
             *
             * Die Schleife ist wichtig, weil durch das Löschen einer Antwort
             * eventuell auch der darüberliegende Kommentar keine Antworten
             * mehr besitzt und anschließend ebenfalls gelöscht werden kann.
             */
            do {
                $deleteCommand = $db->prepare("
                DELETE FROM comments
                WHERE author = :author
                  AND NOT EXISTS (
                      SELECT 1
                      FROM comments AS replies
                      WHERE replies.parent_comment_id = comments.id
                  )
            ");

                $deleteCommand->execute([
                    ":author" => $author
                ]);

                $deletedRows = $deleteCommand->rowCount();

            } while ($deletedRows > 0);

            /*
             * Kommentare, auf die noch Antworten anderer Nutzer folgen,
             * müssen für den Kommentarbaum erhalten bleiben.
             */
            $placeholderCommand = $db->prepare("
            UPDATE comments
            SET author = NULL,
                content = 'Dieser Kommentar wurde gelöscht.'
            WHERE author = :author
        ");

            $placeholderCommand->execute([
                ":author" => $author
            ]);

            $db->commit();

        } catch (PDOException $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }

            throw new RuntimeException("internal_error");
        }
    }
}