<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\SubjectNotFoundException;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectColor;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Domain\Model\Subject\SubjectStatus;
use App\Administration\Domain\Repository\SubjectRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Override;

final readonly class DoctrineSubjectRepository implements SubjectRepository
{
    public function __construct(
        private Connection $connection,
    ) {
    }

    #[Override]
    public function save(Subject $subject): void
    {
        $this->connection->executeStatement(
            'INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, description, created_at, updated_at, deleted_at)
             VALUES (:id, :tenant_id, :school_id, :name, :code, :color, :status, :description, :created_at, :updated_at, :deleted_at)
             ON CONFLICT (id) DO UPDATE SET
                 name = EXCLUDED.name,
                 code = EXCLUDED.code,
                 color = EXCLUDED.color,
                 status = EXCLUDED.status,
                 description = EXCLUDED.description,
                 updated_at = EXCLUDED.updated_at,
                 deleted_at = EXCLUDED.deleted_at',
            [
                'id' => (string) $subject->id,
                'tenant_id' => (string) $subject->tenantId,
                'school_id' => (string) $subject->schoolId,
                'name' => (string) $subject->name,
                'code' => (string) $subject->code,
                'color' => $subject->color !== null ? (string) $subject->color : null,
                'status' => $subject->status->value,
                'description' => $subject->description,
                'created_at' => $subject->createdAt->format(DateTimeImmutable::ATOM),
                'updated_at' => $subject->updatedAt->format(DateTimeImmutable::ATOM),
                'deleted_at' => $subject->deletedAt?->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function get(SubjectId $id): Subject
    {
        $subject = $this->findById($id);

        if ($subject === null) {
            throw SubjectNotFoundException::withId($id);
        }

        return $subject;
    }

    #[Override]
    public function findById(SubjectId $id): ?Subject
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM subjects WHERE id = :id',
            ['id' => (string) $id],
        );

        if ($row === false) {
            return null;
        }

        return $this->hydrate($row);
    }

    #[Override]
    public function findByCode(
        SubjectCode $code,
        TenantId $tenantId,
        SchoolId $schoolId,
    ): ?Subject {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM subjects
             WHERE tenant_id = :tenant_id
             AND school_id = :school_id
             AND code = :code
             AND deleted_at IS NULL',
            [
                'tenant_id' => (string) $tenantId,
                'school_id' => (string) $schoolId,
                'code' => (string) $code,
            ],
        );

        if ($row === false) {
            return null;
        }

        return $this->hydrate($row);
    }

    #[Override]
    public function findActiveByTenantAndSchool(
        TenantId $tenantId,
        SchoolId $schoolId,
    ): array {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM subjects
             WHERE tenant_id = :tenant_id
             AND school_id = :school_id
             AND status = :status
             ORDER BY name ASC',
            [
                'tenant_id' => (string) $tenantId,
                'school_id' => (string) $schoolId,
                'status' => SubjectStatus::ACTIVE->value,
            ],
        );

        return array_map(fn ($row) => $this->hydrate($row), $rows);
    }

    #[Override]
    public function findAllActiveByTenant(TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM subjects
             WHERE tenant_id = :tenant_id
             AND status = :status
             ORDER BY name ASC',
            [
                'tenant_id' => (string) $tenantId,
                'status' => SubjectStatus::ACTIVE->value,
            ],
        );

        return array_map(fn ($row) => $this->hydrate($row), $rows);
    }

    #[Override]
    public function delete(SubjectId $id): void
    {
        $this->connection->delete('subjects', ['id' => (string) $id]);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): Subject
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $schoolId */
        $schoolId = $row['school_id'];
        /** @var non-empty-string $name */
        $name = $row['name'];
        /** @var non-empty-string $code */
        $code = $row['code'];
        /** @var non-empty-string|null $color */
        $color = $row['color'];
        /** @var string $status */
        $status = $row['status'];
        /** @var string|null $description */
        $description = $row['description'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string $updatedAt */
        $updatedAt = $row['updated_at'];
        /** @var string|null $deletedAt */
        $deletedAt = $row['deleted_at'];

        return Subject::reconstitute(
            id: SubjectId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            schoolId: SchoolId::fromString($schoolId),
            name: new SubjectName($name),
            code: new SubjectCode($code),
            color: $color !== null ? new SubjectColor($color) : null,
            status: SubjectStatus::from($status),
            description: $description,
            createdAt: new DateTimeImmutable($createdAt),
            updatedAt: new DateTimeImmutable($updatedAt),
            deletedAt: $deletedAt !== null ? new DateTimeImmutable($deletedAt) : null,
        );
    }
}
