<?php
namespace Misuzu\Changelog;

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

class Changelog {
    // not a strict list but useful to have
    public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];

    private IDbConnection $dbConn;
    private DbStatementCache $cache;

    private array $tags = [];

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

    public static function convertFromActionId(int $actionId): string {
        return match($actionId) {
            1 => 'add',
            2 => 'remove',
            3 => 'update',
            4 => 'fix',
            5 => 'import',
            6 => 'revert',
            default => 'unknown',
        };
    }

    public static function convertToActionId(string $action): int {
        return match($action) {
            'add' => 1,
            'remove' => 2,
            'update' => 3,
            'fix' => 4,
            'import' => 5,
            'revert' => 6,
            default => 0,
        };
    }

    public static function actionText(string|int $action): string {
        if(is_int($action))
            $action = self::convertFromActionId($action);

        return match($action) {
            'add' => 'Added',
            'remove' => 'Removed',
            'update' => 'Updated',
            'fix' => 'Fixed',
            'import' => 'Imported',
            'revert' => 'Reverted',
            default => 'Changed',
        };
    }

    public function countChanges(
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $dateTime = null,
        ?array $tags = null
    ): int {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($dateTime instanceof DateTime)
            $dateTime = $dateTime->getUnixTimeSeconds();

        $args = 0;
        $hasUserInfo = $userInfo !== null;
        $hasDateTime = $dateTime !== null;
        $hasTags = !empty($tags);

        $query = 'SELECT COUNT(*) FROM msz_changelog_changes';
        if($hasUserInfo) {
            ++$args;
            $query .= ' WHERE user_id = ?';
        }
        if($hasDateTime) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))';
        }
        if($hasTags) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= sprintf(
                ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))',
                DbTools::prepareListString($tags)
            );
        }
        $stmt = $this->cache->get($query);

        $args = 0;
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasDateTime)
            $stmt->addParameter(++$args, $dateTime);
        if($hasTags)
            foreach($tags as $tag)
                $stmt->addParameter(++$args, (string)$tag);

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

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

        return $count;
    }

    public function getChanges(
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $dateTime = null,
        ?array $tags = null,
        ?Pagination $pagination = null
    ): array {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($dateTime instanceof DateTime)
            $dateTime = $dateTime->getUnixTimeSeconds();

        $args = 0;
        $hasUserInfo = $userInfo !== null;
        $hasDateTime = $dateTime !== null;
        $hasTags = !empty($tags);
        $hasPagination = $pagination !== null;

        $query = 'SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes';
        if($hasUserInfo) {
            ++$args;
            $query .= ' WHERE user_id = ?';
        }
        if($hasDateTime) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))';
        }
        if($hasTags) {
            $query .= (++$args > 1 ? ' AND' : ' WHERE');
            $query .= sprintf(
                ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))',
                DbTools::prepareListString($tags)
            );
        }
        $query .= ' GROUP BY change_created, change_id ORDER BY change_created DESC, change_id DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';
        $stmt = $this->cache->get($query);

        $args = 0;
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasDateTime)
            $stmt->addParameter(++$args, $dateTime);
        if($hasTags)
            foreach($tags as $tag)
                $stmt->addParameter(++$args, (string)$tag);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }

        $stmt->execute();

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

        while($result->next())
            $changes[] = new ChangeInfo($result);

        return $changes;
    }

    public function getChange(string $changeId): ChangeInfo {
        $stmt = $this->cache->get('SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes WHERE change_id = ?');
        $stmt->addParameter(1, $changeId);
        $stmt->execute();

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

        return new ChangeInfo($result);
    }

    public function createChange(
        string|int $action,
        string $summary,
        string $body = '',
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $createdAt = null
    ): ChangeInfo {
        if(is_string($action))
            $action = self::convertToActionId($action);
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($createdAt instanceof DateTime)
            $createdAt = $createdAt->getUnixTimeSeconds();

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

        $body = trim($body);
        if(empty($body))
            $body = null;

        $stmt = $this->cache->get('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $action);
        $stmt->addParameter(3, $createdAt);
        $stmt->addParameter(4, $summary);
        $stmt->addParameter(5, $body);
        $stmt->execute();

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

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

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

    public function updateChange(
        ChangeInfo|string $infoOrId,
        string|int|null $action = null,
        ?string $summary = null,
        ?string $body = null,
        bool $updateUserInfo = false,
        UserInfo|string|null $userInfo = null,
        DateTime|int|null $createdAt = null
    ): void {
        if($infoOrId instanceof ChangeInfo)
            $infoOrId = $infoOrId->getId();

        if(is_string($action))
            $action = self::convertToActionId($action);
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($createdAt instanceof DateTime)
            $createdAt = $createdAt->getUnixTimeSeconds();

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

        $hasBody = $body !== null;
        if($hasBody) {
            $body = trim($body);
            if(empty($body))
                $body = null;
        }

        $stmt = $this->cache->get('UPDATE msz_changelog_changes SET change_action = COALESCE(?, change_action), change_log = COALESCE(?, change_log), change_text = IF(?, ?, change_text), user_id = IF(?, ?, user_id), change_created = COALESCE(FROM_UNIXTIME(?), change_created) WHERE change_id = ?');
        $stmt->addParameter(1, $action);
        $stmt->addParameter(2, $summary);
        $stmt->addParameter(3, $hasBody ? 1 : 0);
        $stmt->addParameter(4, $body);
        $stmt->addParameter(5, $updateUserInfo ? 1 : 0);
        $stmt->addParameter(6, $userInfo);
        $stmt->addParameter(7, $createdAt);
        $stmt->addParameter(8, $infoOrId);
        $stmt->execute();
    }

    public function getTags(
        ChangeInfo|string|null $changeInfo = null
    ): array {
        if($changeInfo instanceof ChangeInfo)
            $changeInfo = $changeInfo->getId();

        $hasChangeInfo = $changeInfo !== null;

        $query = 'SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), (SELECT COUNT(*) FROM msz_changelog_change_tags AS ct WHERE ct.tag_id = t.tag_id) AS tag_changes FROM msz_changelog_tags AS t';
        if($hasChangeInfo)
            $query .= ' WHERE tag_id IN (SELECT tag_id FROM msz_changelog_change_tags WHERE change_id = ?)';

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

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

        while($result->next()) {
            $tagId = (string)$result->getInteger(0);
            if(array_key_exists($tagId, $this->tags))
                $tags[] = $this->tags[$tagId];
            else
                $tags[] = $this->tags[$tagId] = new ChangeTagInfo($result);
        }

        return $tags;
    }

    public function getTag(string $tagId): ChangeTagInfo {
        $stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id = ?');
        $stmt->addParameter(1, $tagId);
        $stmt->execute();

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

        return new ChangeTagInfo($result);
    }

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

        $description = trim($description);
        if(empty($description))
            $description = null;

        $stmt = $this->cache->get('INSERT INTO msz_changelog_tags (tag_name, tag_description, tag_archived) VALUES (?, ?, IF(?, NOW(), NULL))');
        $stmt->addParameter(1, $name);
        $stmt->addParameter(2, $description);
        $stmt->addParameter(3, $archived ? 1 : 0);
        $stmt->execute();

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

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

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

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

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

        $hasDescription = $description !== null;
        if($hasDescription) {
            $description = trim($description);
            if(empty($description))
                $description = null;
        }

        $hasArchived = $archived !== null;

        $stmt = $this->cache->get('UPDATE msz_changelog_tags SET tag_name = COALESCE(?, tag_name), tag_description = IF(?, ?, tag_description), tag_archived = IF(?, IF(?, NOW(), NULL), tag_archived) WHERE tag_id = ?');
        $stmt->addParameter(1, $name);
        $stmt->addParameter(2, $hasDescription ? 1 : 0);
        $stmt->addParameter(3, $description);
        $stmt->addParameter(4, $hasArchived ? 1 : 0);
        $stmt->addParameter(5, $archived ? 1 : 0);
        $stmt->addParameter(6, $infoOrId);
        $stmt->execute();
    }

    public function addTagToChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
        if($change instanceof ChangeInfo)
            $change = $change->getId();
        if($tag instanceof ChangeTagInfo)
            $tag = $tag->getId();

        $stmt = $this->cache->get('INSERT INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)');
        $stmt->addParameter(1, $change);
        $stmt->addParameter(2, $tag);
        $stmt->execute();
    }

    public function removeTagFromChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
        if($change instanceof ChangeInfo)
            $change = $change->getId();
        if($tag instanceof ChangeTagInfo)
            $tag = $tag->getId();

        $stmt = $this->cache->get('DELETE FROM msz_changelog_change_tags WHERE change_id = ? AND tag_id = ?');
        $stmt->addParameter(1, $change);
        $stmt->addParameter(2, $tag);
        $stmt->execute();
    }
}
