<?php

declare(strict_types=1);

namespace App\Services\Audit;

use App\Models\AlertDestination;
use App\Models\AlertRule;
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\BaselineProfile;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\Workspace;
use App\Support\Audit\AuditActionId;
use App\Support\Audit\AuditActorSnapshot;
use App\Support\Audit\AuditActorType;
use App\Support\Audit\AuditContextSanitizer;
use App\Support\Audit\AuditOutcome;
use App\Support\Audit\AuditTargetSnapshot;
use App\Support\OperationCatalog;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Support\Str;

final class AuditEventBuilder
{
    /**
     * @param  array<string, mixed>  $context
     * @return array<string, mixed>
     */
    public function buildRecordAttributes(
        string $action,
        array $context = [],
        ?Workspace $workspace = null,
        ?ManagedEnvironment $tenant = null,
        ?AuditActorSnapshot $actor = null,
        ?AuditTargetSnapshot $target = null,
        string|AuditOutcome|null $outcome = null,
        ?CarbonInterface $recordedAt = null,
        ?string $summary = null,
        ?int $operationRunId = null,
    ): array {
        $metadata = $this->extractMetadata($context);
        $sanitizedMetadata = AuditContextSanitizer::sanitize($metadata);

        $resolvedTarget = $this->resolveTargetSnapshot(
            $target,
            $workspace,
            $tenant,
            $sanitizedMetadata,
        );

        $resolvedActor = $this->resolveActorSnapshot(
            $action,
            $actor,
            $sanitizedMetadata,
        );

        $resolvedOutcome = $outcome instanceof AuditOutcome
            ? $outcome
            : AuditOutcome::normalize($outcome ?? ($sanitizedMetadata['status'] ?? null));

        $resolvedOperationRunId = $this->resolveOperationRunId(
            $operationRunId,
            $resolvedTarget,
            $sanitizedMetadata,
        );

        $resolvedSummary = $summary ?? AuditActionId::summaryFor(
            action: $action,
            targetLabel: $resolvedTarget->labelOrFallback(),
            targetType: $resolvedTarget->type,
            context: $sanitizedMetadata,
        );

        return [
            'managed_environment_id' => $tenant?->getKey(),
            'workspace_id' => $workspace?->getKey() ?? $tenant?->workspace_id,
            'actor_id' => is_numeric($resolvedActor->id) ? (int) $resolvedActor->id : null,
            'actor_email' => $resolvedActor->email,
            'actor_name' => $resolvedActor->label,
            'actor_type' => $resolvedActor->type->value,
            'actor_label' => $resolvedActor->labelOrFallback(),
            'action' => trim($action),
            'resource_type' => $resolvedTarget->type,
            'resource_id' => $resolvedTarget->id !== null ? (string) $resolvedTarget->id : null,
            'target_label' => $resolvedTarget->labelOrFallback(),
            'status' => $resolvedOutcome->value,
            'outcome' => $resolvedOutcome->value,
            'summary' => $resolvedSummary,
            'metadata' => $sanitizedMetadata,
            'operation_run_id' => $resolvedOperationRunId,
            'recorded_at' => ($recordedAt ?? CarbonImmutable::now())->toDateTimeString(),
        ];
    }

    /**
     * @param  array<string, mixed>  $attributes
     * @return array<string, mixed>
     */
    public function fillMissingDerivedAttributes(array $attributes): array
    {
        $tenant = null;
        if (is_numeric($attributes['managed_environment_id'] ?? null)) {
            $tenant = ManagedEnvironment::query()->whereKey((int) $attributes['managed_environment_id'])->first();
        }

        $workspace = null;
        if (is_numeric($attributes['workspace_id'] ?? null)) {
            $workspace = Workspace::query()->whereKey((int) $attributes['workspace_id'])->first();
        }

        $metadata = $this->normalizeMetadata($attributes['metadata'] ?? null);

        $actorType = filled($attributes['actor_type'] ?? null)
            ? AuditActorType::normalize($attributes['actor_type'])
            : AuditActorType::infer(
                action: is_string($attributes['action'] ?? null) ? (string) $attributes['action'] : null,
                actorId: is_numeric($attributes['actor_id'] ?? null) ? (int) $attributes['actor_id'] : null,
                actorEmail: is_string($attributes['actor_email'] ?? null) ? (string) $attributes['actor_email'] : null,
                actorName: is_string($attributes['actor_name'] ?? null) ? (string) $attributes['actor_name'] : null,
                context: $metadata,
            );

        $actor = AuditActorSnapshot::fromLegacy(
            type: $actorType,
            id: is_numeric($attributes['actor_id'] ?? null) ? (int) $attributes['actor_id'] : null,
            email: is_string($attributes['actor_email'] ?? null) ? (string) $attributes['actor_email'] : null,
            label: is_string($attributes['actor_label'] ?? $attributes['actor_name'] ?? null)
                ? (string) ($attributes['actor_label'] ?? $attributes['actor_name'])
                : null,
        );

        $target = new AuditTargetSnapshot(
            type: is_string($attributes['resource_type'] ?? null) ? (string) $attributes['resource_type'] : null,
            id: is_string($attributes['resource_id'] ?? null) || is_numeric($attributes['resource_id'] ?? null)
                ? (string) $attributes['resource_id']
                : null,
            label: is_string($attributes['target_label'] ?? null) ? (string) $attributes['target_label'] : null,
        );

        return array_replace(
            $attributes,
            $this->buildRecordAttributes(
                action: (string) ($attributes['action'] ?? 'audit.event'),
                context: $metadata,
                workspace: $workspace,
                tenant: $tenant,
                actor: $actor,
                target: $target,
                outcome: (string) ($attributes['outcome'] ?? $attributes['status'] ?? AuditOutcome::Info->value),
                recordedAt: isset($attributes['recorded_at']) && $attributes['recorded_at'] !== null
                    ? CarbonImmutable::parse((string) $attributes['recorded_at'])
                    : null,
                summary: is_string($attributes['summary'] ?? null) ? (string) $attributes['summary'] : null,
                operationRunId: is_numeric($attributes['operation_run_id'] ?? null)
                    ? (int) $attributes['operation_run_id']
                    : null,
            ),
        );
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array<string, mixed>
     */
    private function extractMetadata(array $context): array
    {
        $metadata = $context['metadata'] ?? [];
        unset($context['metadata']);

        if (! is_array($metadata)) {
            $metadata = [];
        }

        return $metadata + $context;
    }

    /**
     * @param  array<string, mixed>  $metadata
     */
    private function resolveActorSnapshot(
        string $action,
        ?AuditActorSnapshot $actor,
        array $metadata,
    ): AuditActorSnapshot {
        if ($actor instanceof AuditActorSnapshot) {
            return $actor;
        }

        return AuditActorSnapshot::fromLegacy(
            type: AuditActorType::infer($action, null, null, null, $metadata),
        );
    }

    /**
     * @param  array<string, mixed>  $metadata
     */
    private function resolveTargetSnapshot(
        ?AuditTargetSnapshot $target,
        ?Workspace $workspace,
        ?ManagedEnvironment $tenant,
        array $metadata,
    ): AuditTargetSnapshot {
        $type = $target?->type;
        $id = $target?->id;

        if (! filled($type) || ! filled($id)) {
            [$type, $id] = $this->inferTargetIdentity($type, $id, $metadata);
        }

        $label = $target?->label;

        if (! filled($label)) {
            $label = $this->resolveTargetLabel($type, $id, $workspace, $tenant);
        }

        return new AuditTargetSnapshot(
            type: $type,
            id: $id,
            label: $label,
        );
    }

    /**
     * @param  array<string, mixed>  $metadata
     * @return array{0: ?string, 1: int|string|null}
     */
    private function inferTargetIdentity(?string $type, int|string|null $id, array $metadata): array
    {
        if (filled($type) && filled($id)) {
            return [$type, $id];
        }

        foreach ([
            'finding_id' => 'finding',
            'baseline_profile_id' => 'baseline_profile',
            'baseline_snapshot_id' => 'baseline_snapshot',
            'backup_set_id' => 'backup_set',
            'backup_schedule_id' => 'backup_schedule',
            'restore_run_id' => 'restore_run',
            'operation_run_id' => 'operation_run',
            'workspace_id' => 'workspace',
            'managed_environment_id' => 'tenant',
            'alert_rule_id' => 'alert_rule',
            'alert_destination_id' => 'alert_destination',
        ] as $key => $resolvedType) {
            if (! filled($metadata[$key] ?? null)) {
                continue;
            }

            return [$type ?? $resolvedType, (string) $metadata[$key]];
        }

        return [$type, $id];
    }

    private function resolveOperationRunId(
        ?int $operationRunId,
        AuditTargetSnapshot $target,
        array $metadata,
    ): ?int {
        if ($operationRunId !== null) {
            return $this->existingOperationRunId($operationRunId);
        }

        if (is_numeric($metadata['operation_run_id'] ?? null)) {
            return $this->existingOperationRunId((int) $metadata['operation_run_id']);
        }

        if ($target->type === 'operation_run' && is_numeric($target->id)) {
            return $this->existingOperationRunId((int) $target->id);
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    private function normalizeMetadata(mixed $metadata): array
    {
        if (is_array($metadata)) {
            return $metadata;
        }

        if (! is_string($metadata) || trim($metadata) === '') {
            return [];
        }

        $decoded = json_decode($metadata, true);

        return is_array($decoded) ? $decoded : [];
    }

    private function existingOperationRunId(?int $operationRunId): ?int
    {
        if ($operationRunId === null || $operationRunId <= 0) {
            return null;
        }

        return OperationRun::query()->whereKey($operationRunId)->exists()
            ? $operationRunId
            : null;
    }

    private function resolveTargetLabel(
        ?string $type,
        int|string|null $id,
        ?Workspace $workspace,
        ?ManagedEnvironment $tenant,
    ): ?string {
        if (! filled($type) && ! filled($id)) {
            return null;
        }

        if ($type === 'workspace' && $workspace instanceof Workspace) {
            return $workspace->name;
        }

        if ($type === 'tenant' && $tenant instanceof ManagedEnvironment) {
            return $tenant->name;
        }

        if (! filled($type) || ! filled($id)) {
            return (new AuditTargetSnapshot($type, $id))->labelOrFallback();
        }

        $numericId = is_numeric($id) ? (int) $id : null;

        return match ($type) {
            'backup_schedule' => $numericId !== null
                ? BackupSchedule::query()->whereKey($numericId)->value('name') ?: sprintf('Backup schedule #%d', $numericId)
                : null,
            'backup_set' => $numericId !== null
                ? BackupSet::query()->withTrashed()->whereKey($numericId)->value('name') ?: sprintf('Backup set #%d', $numericId)
                : null,
            'baseline_profile' => $numericId !== null
                ? BaselineProfile::query()->whereKey($numericId)->value('name') ?: sprintf('Baseline profile #%d', $numericId)
                : null,
            'finding' => $numericId !== null
                ? $this->resolveFindingLabel($numericId)
                : null,
            'operation_run' => $numericId !== null
                ? $this->resolveOperationRunLabel($numericId)
                : null,
            'restore_run' => $numericId !== null
                ? sprintf('Restore run #%d', $numericId)
                : null,
            'alert_rule' => $numericId !== null
                ? AlertRule::query()->whereKey($numericId)->value('name') ?: sprintf('Alert rule #%d', $numericId)
                : null,
            'alert_destination' => $numericId !== null
                ? AlertDestination::query()->whereKey($numericId)->value('name') ?: sprintf('Alert destination #%d', $numericId)
                : null,
            'workspace_setting' => is_string($id) ? $this->formatWorkspaceSettingLabel($id) : null,
            default => (new AuditTargetSnapshot($type, $id))->labelOrFallback(),
        };
    }

    private function resolveFindingLabel(int $id): string
    {
        /** @var Finding|null $finding */
        $finding = Finding::query()->whereKey($id)->first();

        if (! $finding instanceof Finding) {
            return sprintf('Finding #%d', $id);
        }

        $findingType = str_replace('_', ' ', (string) $finding->finding_type);

        return sprintf('%s finding #%d', ucfirst($findingType), $id);
    }

    private function resolveOperationRunLabel(int $id): string
    {
        /** @var OperationRun|null $run */
        $run = OperationRun::query()->whereKey($id)->first();

        if (! $run instanceof OperationRun) {
            return sprintf('Operation run #%d', $id);
        }

        return sprintf('%s #%d', OperationCatalog::label((string) $run->type), $id);
    }

    private function formatWorkspaceSettingLabel(string $value): string
    {
        return Str::of(str_replace('.', ' ', trim($value)))->headline()->toString();
    }
}
