<?php
declare(strict_types=1);

namespace App\Community;

use InvalidArgumentException;
use PDO;
use RuntimeException;

final class FollowService
{
    private const TABLES = [
        'question' => ['table' => 'question_follows', 'column' => 'question_id', 'target_table' => 'questions'],
        'category' => ['table' => 'category_follows', 'column' => 'category_id', 'target_table' => 'categories'],
        'tag' => ['table' => 'tag_follows', 'column' => 'tag_id', 'target_table' => 'tags'],
        'topic' => ['table' => 'topic_follows', 'column' => 'topic_id', 'target_table' => 'topics'],
    ];

    public function __construct(private readonly PDO $pdo) {}

    public function toggle(int $userId, string $type, int $targetId): bool
    {
        [$table, $column, $targetTable] = $this->definition($type);
        $this->requireActiveUser($userId);
        $this->requireTarget($targetTable, $targetId, in_array(strtolower(trim($type)), ['question', 'topic'], true));

        if ($this->isFollowing($userId, $type, $targetId)) {
            $statement = $this->pdo->prepare("DELETE FROM {$table} WHERE user_id = :user_id AND {$column} = :target_id");
            $statement->execute(['user_id' => $userId, 'target_id' => $targetId]);
            return false;
        }

        try {
            $statement = $this->pdo->prepare("INSERT INTO {$table} (user_id, {$column}, created_at) VALUES (:user_id, :target_id, NOW())");
            $statement->execute(['user_id' => $userId, 'target_id' => $targetId]);
            return true;
        } catch (\PDOException $exception) {
            if ($exception->getCode() === '23000' || str_contains($exception->getMessage(), '23000') || str_contains($exception->getMessage(), '1062')) {
                return true;
            }
            throw $exception;
        }
    }

    public function isFollowing(int $userId, string $type, int $targetId): bool
    {
        [$table, $column] = $this->definition($type);
        $statement = $this->pdo->prepare("SELECT 1 FROM {$table} WHERE user_id = :user_id AND {$column} = :target_id LIMIT 1");
        $statement->execute(['user_id' => $userId, 'target_id' => $targetId]);
        return $statement->fetchColumn() !== false;
    }

    /** @return list<int> */
    public function followerUserIds(string $type, int $targetId): array
    {
        [$table, $column] = $this->definition($type);
        $statement = $this->pdo->prepare("SELECT user_id FROM {$table} WHERE {$column} = :target_id ORDER BY user_id ASC");
        $statement->execute(['target_id' => $targetId]);
        return array_values(array_map('intval', $statement->fetchAll(PDO::FETCH_COLUMN)));
    }

    /** @return array{0:string,1:string,2:string} */
    private function definition(string $type): array
    {
        $type = strtolower(trim($type));
        if (!isset(self::TABLES[$type])) throw new InvalidArgumentException('Unsupported follow target.');
        $definition = self::TABLES[$type];
        return [$definition['table'], $definition['column'], $definition['target_table']];
    }

    private function requireActiveUser(int $userId): void
    {
        if ($userId <= 0) throw new RuntimeException('Active user required.');
        $statement = $this->pdo->prepare("SELECT 1 FROM users WHERE id = :id AND status = 'active' LIMIT 1");
        $statement->execute(['id' => $userId]);
        if ($statement->fetchColumn() === false) throw new RuntimeException('Active user required.');
    }

    private function requireTarget(string $table, int $targetId, bool $publishedOnly): void
    {
        if ($targetId <= 0) throw new InvalidArgumentException('Invalid follow target.');
        $sql = "SELECT 1 FROM {$table} WHERE id = :id" . ($publishedOnly ? " AND status = 'published'" : '') . ' LIMIT 1';
        $statement = $this->pdo->prepare($sql);
        $statement->execute(['id' => $targetId]);
        if ($statement->fetchColumn() === false) throw new RuntimeException('Follow target not found.');
    }
}
