<?php

namespace App\Domain\Compliance\Services;

use App\Domain\Compliance\Enums\ConsentType;
use App\Domain\Compliance\Models\Consent;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Recording, reading and withdrawing consent.
 *
 * The current state of a consent is "the most recent row for this participant
 * and this type", never a boolean column somewhere. That is what makes the
 * history readable: a photo consent granted in March and withdrawn in July is
 * two rows, and an academy that published a photograph in May can show it was
 * entitled to.
 */
class ConsentService
{
    /**
     * The version of the consent text currently in force.
     *
     * A consent is a statement about a *particular* document. When the text
     * changes the version changes, previously recorded consents stop counting
     * as current, and members are asked again — which is the only honest
     * behaviour, and is why this is not a boolean.
     */
    public function currentVersion(): string
    {
        return (string) config('compliance.consent_version', '1.0');
    }

    public function record(
        ConsentType $type,
        bool $granted,
        User $givenBy,
        ?Participant $participant = null,
        ?string $ip = null,
        ?string $userAgent = null,
    ): Consent {
        // A guardian may only speak for members they actually speak for. The
        // portal checks this too, but a consent is exactly the kind of record
        // that must not depend on the caller having remembered to.
        if ($participant) {
            $allowed = app(\App\Domain\Identity\Services\GuardianResolver::class)
                ->participantIdsFor($givenBy);

            if (! in_array((int) $participant->id, $allowed, true)) {
                throw new DomainException('لا تملك صلاحية تسجيل موافقة عن هذا العضو');
            }
        }

        return Consent::create([
            'academy_id' => $givenBy->academy_id,
            'participant_id' => $participant?->id,
            'given_by' => $givenBy->id,
            'person_id' => $givenBy->person_id,
            'type' => $type,
            'granted' => $granted,
            'document_version' => $this->currentVersion(),
            'document_hash' => $this->documentHash($type),
            'granted_at' => now(),
            'ip_address' => $ip,
            'user_agent' => $userAgent ? substr($userAgent, 0, 255) : null,
        ]);
    }

    /**
     * The current answer for each type: the latest row, for the version in
     * force. A consent recorded against an older text is history, not an answer.
     *
     * @return array<string, bool>
     */
    public function currentFor(User $user, ?Participant $participant = null): array
    {
        $rows = Consent::withoutGlobalScopes()
            ->where('academy_id', $user->academy_id)
            ->where('document_version', $this->currentVersion())
            ->when(
                $participant,
                fn ($q) => $q->where('participant_id', $participant->id),
                fn ($q) => $q->whereNull('participant_id')->where('given_by', $user->id),
            )
            ->orderByDesc('granted_at')
            ->orderByDesc('id')
            ->get(['type', 'granted']);

        $current = [];

        foreach ($rows as $row) {
            $key = $row->type instanceof ConsentType ? $row->type->value : (string) $row->type;

            // First row wins: the query is newest-first.
            $current[$key] ??= (bool) $row->granted;
        }

        return $current;
    }

    /**
     * Which required consents are still outstanding.
     *
     * @return Collection<int, ConsentType>
     */
    public function outstandingFor(User $user, ?Participant $participant = null): Collection
    {
        $current = $this->currentFor($user, $participant);

        return collect(ConsentType::required())
            ->reject(fn (ConsentType $type) => ($current[$type->value] ?? false) === true)
            ->values();
    }

    public function hasGranted(User $user, ConsentType $type, ?Participant $participant = null): bool
    {
        return ($this->currentFor($user, $participant)[$type->value] ?? false) === true;
    }

    /**
     * Record several answers at once — the shape of an onboarding screen.
     *
     * @param  array<string, bool>  $answers  type value => granted
     */
    public function recordMany(array $answers, User $givenBy, ?Participant $participant = null, ?string $ip = null, ?string $userAgent = null): void
    {
        DB::transaction(function () use ($answers, $givenBy, $participant, $ip, $userAgent) {
            foreach ($answers as $value => $granted) {
                $type = ConsentType::tryFrom((string) $value);

                if (! $type) {
                    continue;
                }

                if ($type->isRequired() && ! $granted) {
                    throw new DomainException(
                        'لا يمكن متابعة الاشتراك دون الموافقة على: ' . $type->label()
                    );
                }

                $this->record($type, (bool) $granted, $givenBy, $participant, $ip, $userAgent);
            }
        });
    }

    /**
     * A fingerprint of the text that was actually shown.
     *
     * Without it, "version 1.2" is just a label somebody typed; with it, the
     * exact wording a member agreed to can be proven years later.
     */
    private function documentHash(ConsentType $type): string
    {
        return substr(hash('sha256', $this->currentVersion() . '|' . $type->value . '|' . $type->description()), 0, 32);
    }
}
