<?php

declare(strict_types=1);

namespace Ekdos\Users;

use PDO;

final readonly class UserRepository
{
    private const string COLUMNS = 'id, username, display_name, role, is_active, created_at, updated_at, last_login_at';

    public function __construct(private PDO $db) {}

    /** @return list<User> */
    public function all(): array
    {
        $rows = $this->db->query('select ' . self::COLUMNS . ' from users order by lower(display_name)')->fetchAll();

        return array_map(User::fromRow(...), $rows);
    }

    public function find(string $id): ?User
    {
        $statement = $this->db->prepare('select ' . self::COLUMNS . ' from users where id = :id');
        $statement->execute(['id' => $id]);
        $row = $statement->fetch();

        return $row === false ? null : User::fromRow($row);
    }

    /** Liefert den Hash mit. Wird ausschliesslich fuer die Anmeldung verwendet. */
    public function findByUsernameWithHash(string $username): ?User
    {
        $statement = $this->db->prepare('select ' . self::COLUMNS . ', password_hash from users where lower(username) = lower(:username)');
        $statement->execute(['username' => $username]);
        $row = $statement->fetch();

        return $row === false ? null : User::fromRow($row);
    }

    public function usernameTaken(string $username, ?string $exceptId = null): bool
    {
        $statement = $this->db->prepare('select 1 from users where lower(username) = lower(:username) and (cast(:except as uuid) is null or id <> cast(:except as uuid))');
        $statement->execute(['username' => $username, 'except' => $exceptId]);

        return $statement->fetchColumn() !== false;
    }

    /** Zaehlt aktive Administratoren, optional ohne einen bestimmten Benutzer. */
    public function countAdmins(?string $exceptId = null): int
    {
        $statement = $this->db->prepare("select count(*) from users where role = 'admin' and is_active and (cast(:except as uuid) is null or id <> cast(:except as uuid))");
        $statement->execute(['except' => $exceptId]);

        return (int) $statement->fetchColumn();
    }

    public function create(string $username, string $displayName, Role $role, string $passwordHash, bool $isActive = true): User
    {
        $statement = $this->db->prepare(
            'insert into users (username, display_name, role, password_hash, is_active)
             values (:username, :display_name, :role, :password_hash, :is_active)
             returning ' . self::COLUMNS
        );
        $statement->execute([
            'username' => $username,
            'display_name' => $displayName,
            'role' => $role->value,
            'password_hash' => $passwordHash,
            'is_active' => $isActive ? 't' : 'f',
        ]);

        return User::fromRow($statement->fetch());
    }

    /** Nur uebergebene Felder werden geschrieben, alles andere bleibt unangetastet. */
    public function update(string $id, array $fields): ?User
    {
        $allowed = ['username', 'display_name', 'role', 'password_hash', 'is_active'];
        $sets = [];
        $parameters = ['id' => $id];

        foreach ($fields as $column => $value) {
            if (!in_array($column, $allowed, true)) {
                continue;
            }

            $sets[] = $column . ' = :' . $column;
            $parameters[$column] = is_bool($value) ? ($value ? 't' : 'f') : $value;
        }

        if ($sets === []) {
            return $this->find($id);
        }

        $sets[] = 'updated_at = now()';
        $statement = $this->db->prepare('update users set ' . implode(', ', $sets) . ' where id = :id returning ' . self::COLUMNS);
        $statement->execute($parameters);
        $row = $statement->fetch();

        return $row === false ? null : User::fromRow($row);
    }

    public function delete(string $id): bool
    {
        $statement = $this->db->prepare('delete from users where id = :id');
        $statement->execute(['id' => $id]);

        return $statement->rowCount() > 0;
    }

    public function touchLogin(string $id): void
    {
        $statement = $this->db->prepare('update users set last_login_at = now() where id = :id');
        $statement->execute(['id' => $id]);
    }

    public function audit(?string $actorId, string $actorName, string $action, ?string $subjectId, string $subject, array $detail = []): void
    {
        $statement = $this->db->prepare(
            'insert into user_audit (actor_id, actor_name, action, subject_id, subject, detail)
             values (cast(:actor_id as uuid), :actor_name, :action, cast(:subject_id as uuid), :subject, cast(:detail as jsonb))'
        );
        $statement->execute([
            'actor_id' => $actorId,
            'actor_name' => $actorName,
            'action' => $action,
            'subject_id' => $subjectId,
            'subject' => $subject,
            'detail' => json_encode($detail, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
        ]);
    }

    /** @return list<array<string, mixed>> */
    public function recentAudit(int $limit = 50): array
    {
        $statement = $this->db->prepare('select actor_name, action, subject, detail, created_at from user_audit order by created_at desc limit :limit');
        $statement->bindValue('limit', $limit, PDO::PARAM_INT);
        $statement->execute();

        return array_map(static fn (array $row): array => [
            'actor' => $row['actor_name'],
            'action' => $row['action'],
            'subject' => $row['subject'],
            'detail' => json_decode((string) $row['detail'], true) ?: [],
            'createdAt' => $row['created_at'],
        ], $statement->fetchAll());
    }
}
