<?php
declare(strict_types=1);

namespace App\Questions;

use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

final class VoteService
{
    public function __construct(
        private readonly PDO $pdo,
    ) {
    }

    public function cast(int $userId, string $votableType, int $votableId, int $value): int
    {
        $votableType = strtolower(trim($votableType));

        if (!in_array($votableType, ['question', 'answer'], true)) {
            throw new InvalidArgumentException('Unsupported votable type.');
        }

        if (!in_array($value, [-1, 1], true)) {
            throw new InvalidArgumentException('Vote value must be -1 or 1.');
        }

        $this->requireActiveUser($userId);

        $this->pdo->beginTransaction();

        try {
            $table = $votableType === 'question' ? 'questions' : 'answers';
            $targetOwner = $this->lockTarget($votableType, $votableId);
            if ($targetOwner === $userId) {
                throw new RuntimeException('A user cannot vote on their own content.');
            }

            $existing = $this->lockExistingVote($userId, $votableType, $votableId);
            $delta = $value;

            if ($existing === null) {
                $insert = $this->pdo->prepare(
                    'INSERT INTO votes (user_id, votable_type, votable_id, value, created_at)
                     VALUES (:user_id, :votable_type, :votable_id, :value, NOW())'
                );
                $insert->execute([
                    'user_id' => $userId,
                    'votable_type' => $votableType,
                    'votable_id' => $votableId,
                    'value' => $value,
                ]);
            } else {
                $currentValue = (int) ($existing['value'] ?? 0);
                if ($currentValue === $value) {
                    $delta = -$currentValue;
                    $delete = $this->pdo->prepare('DELETE FROM votes WHERE id = :id');
                    $delete->execute(['id' => $existing['id']]);
                    $value = 0;
                } else {
                    $delta = $value - $currentValue;
                    $update = $this->pdo->prepare(
                        'UPDATE votes
                         SET value = :value
                         WHERE id = :id'
                    );
                    $update->execute([
                        'value' => $value,
                        'id' => $existing['id'],
                    ]);
                }
            }

            if ($delta !== 0) {
                $counter = $this->pdo->prepare(
                    "UPDATE {$table}
                     SET votes_count = votes_count + :delta,
                         updated_at = NOW()
                     WHERE id = :id"
                );
                $counter->bindValue(':delta', $delta, PDO::PARAM_INT);
                $counter->bindValue(':id', $votableId, PDO::PARAM_INT);
                $counter->execute();
            }

            $this->pdo->commit();

            return $value;
        } catch (Throwable $throwable) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $throwable;
        }
    }

    private function requireActiveUser(int $userId): void
    {
        $statement = $this->pdo->prepare(
            'SELECT id
             FROM users
             WHERE id = :id
               AND status = :status
             LIMIT 1'
        );
        $statement->execute([
            'id' => $userId,
            'status' => 'active',
        ]);

        if ($statement->fetchColumn() === false) {
            throw new RuntimeException('An active user is required.');
        }
    }

    private function lockTarget(string $votableType, int $targetId): int
    {
        $sql = $votableType === 'question'
            ? "SELECT q.id, q.user_id
               FROM questions q
               WHERE q.id = :id AND q.status = 'published'
               LIMIT 1
               FOR UPDATE"
            : "SELECT a.id, a.user_id
               FROM answers a
               INNER JOIN questions q ON q.id = a.question_id
               WHERE a.id = :id AND a.status = 'published' AND q.status = 'published'
               LIMIT 1
               FOR UPDATE";
        $statement = $this->pdo->prepare($sql);
        $statement->execute(['id' => $targetId]);

        $row = $statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($row)) {
            throw new RuntimeException('Vote target not found.');
        }
        return (int) ($row['user_id'] ?? 0);
    }

    private function lockExistingVote(int $userId, string $votableType, int $votableId): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT id, value
             FROM votes
             WHERE user_id = :user_id
               AND votable_type = :votable_type
               AND votable_id = :votable_id
             LIMIT 1
             FOR UPDATE'
        );
        $statement->execute([
            'user_id' => $userId,
            'votable_type' => $votableType,
            'votable_id' => $votableId,
        ]);
        $vote = $statement->fetch(PDO::FETCH_ASSOC);

        return is_array($vote) ? $vote : null;
    }
}
