<?php

declare(strict_types=1);

namespace App\Services\Verification;

use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\VerificationCheckAcknowledgement;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\Verification\VerificationCheckStatus;
use App\Support\Verification\VerificationReportSanitizer;
use App\Support\Verification\VerificationReportSchema;
use Carbon\CarbonImmutable;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Gate;
use InvalidArgumentException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class VerificationCheckAcknowledgementService
{
    public function __construct(
        private readonly WorkspaceAuditLogger $audit,
    ) {}

    public function acknowledge(
        ManagedEnvironment $tenant,
        OperationRun $run,
        string $checkKey,
        string $ackReason,
        ?string $expiresAt,
        User $actor,
    ): VerificationCheckAcknowledgement {
        if (! $actor->canAccessTenant($tenant)) {
            throw new NotFoundHttpException;
        }

        Gate::forUser($actor)->authorize(Capabilities::TENANT_VERIFICATION_ACKNOWLEDGE, $tenant);

        if ((int) $run->managed_environment_id !== (int) $tenant->getKey()) {
            throw new NotFoundHttpException;
        }

        if ((int) $run->workspace_id !== (int) $tenant->workspace_id) {
            throw new NotFoundHttpException;
        }

        $checkKey = trim($checkKey);
        if ($checkKey === '') {
            throw new InvalidArgumentException('check_key is required.');
        }

        $ackReason = trim($ackReason);
        if ($ackReason === '') {
            throw new InvalidArgumentException('ack_reason is required.');
        }

        if (mb_strlen($ackReason) > 160) {
            throw new InvalidArgumentException('ack_reason must be at most 160 characters.');
        }

        $report = $this->reportForRun($run);
        $check = $this->findCheckByKey($report, $checkKey);

        $status = $check['status'] ?? null;

        if (! is_string($status) || ! in_array($status, [VerificationCheckStatus::Fail->value, VerificationCheckStatus::Warn->value], true)) {
            throw new InvalidArgumentException('Only failing or warning checks can be acknowledged.');
        }

        $reasonCode = $check['reason_code'] ?? null;
        if (! is_string($reasonCode) || trim($reasonCode) === '') {
            throw new InvalidArgumentException('Check reason_code is required.');
        }

        $expiresAtParsed = null;

        if ($expiresAt !== null && trim($expiresAt) !== '') {
            try {
                $expiresAtParsed = CarbonImmutable::parse($expiresAt);
            } catch (\Throwable) {
                throw new InvalidArgumentException('expires_at must be a valid date-time.');
            }

            if ($expiresAtParsed->isBefore(CarbonImmutable::now())) {
                throw new InvalidArgumentException('expires_at must be in the future.');
            }
        }

        $acknowledgedAt = CarbonImmutable::now();

        try {
            $ack = VerificationCheckAcknowledgement::create([
                'managed_environment_id' => (int) $tenant->getKey(),
                'workspace_id' => (int) $tenant->workspace_id,
                'operation_run_id' => (int) $run->getKey(),
                'check_key' => $checkKey,
                'ack_reason' => $ackReason,
                'expires_at' => $expiresAtParsed,
                'acknowledged_at' => $acknowledgedAt,
                'acknowledged_by_user_id' => (int) $actor->getKey(),
            ]);
        } catch (QueryException $e) {
            $ack = VerificationCheckAcknowledgement::query()
                ->where('operation_run_id', (int) $run->getKey())
                ->where('check_key', $checkKey)
                ->first();

            if (! $ack instanceof VerificationCheckAcknowledgement) {
                throw $e;
            }

            return $ack;
        }

        if ($ack->wasRecentlyCreated) {
            $workspace = $tenant->workspace;

            if ($workspace !== null) {
                $this->audit->log(
                    workspace: $workspace,
                    action: AuditActionId::VerificationCheckAcknowledged->value,
                    context: [
                        'managed_environment_id' => (int) $tenant->getKey(),
                        'operation_run_id' => (int) $run->getKey(),
                        'report_id' => (int) $run->getKey(),
                        'flow' => (string) $run->type,
                        'check_key' => $checkKey,
                        'reason_code' => $reasonCode,
                    ],
                    actor: $actor,
                    resourceType: 'operation_run',
                    resourceId: (string) $run->getKey(),
                );
            }
        }

        return $ack;
    }

    /**
     * @return array<string, mixed>
     */
    private function reportForRun(OperationRun $run): array
    {
        $context = is_array($run->context) ? $run->context : [];
        $report = $context['verification_report'] ?? null;

        if (! is_array($report)) {
            throw new InvalidArgumentException('Verification report is missing.');
        }

        $report = VerificationReportSanitizer::sanitizeReport($report);

        if (! VerificationReportSchema::isValidReport($report)) {
            throw new InvalidArgumentException('Verification report is invalid.');
        }

        return $report;
    }

    /**
     * @param  array<string, mixed>  $report
     * @return array<string, mixed>
     */
    private function findCheckByKey(array $report, string $checkKey): array
    {
        $checks = $report['checks'] ?? null;
        $checks = is_array($checks) ? $checks : [];

        foreach ($checks as $check) {
            if (! is_array($check)) {
                continue;
            }

            if (($check['key'] ?? null) === $checkKey) {
                return $check;
            }
        }

        throw new InvalidArgumentException('Check not found in verification report.');
    }
}
