<?php

declare(strict_types=1);

namespace App\Services\Findings;

use App\Models\Finding;
use App\Models\Tenant;
use App\Models\TenantMembership;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\Intune\AuditLogger;
use App\Support\Auth\Capabilities;
use Carbon\CarbonImmutable;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class FindingWorkflowService
{
    public function __construct(
        private readonly FindingSlaPolicy $slaPolicy,
        private readonly AuditLogger $auditLogger,
        private readonly CapabilityResolver $capabilityResolver,
    ) {}

    public function triage(Finding $finding, Tenant $tenant, User $actor): Finding
    {
        $this->authorize($finding, $tenant, $actor, [
            Capabilities::TENANT_FINDINGS_TRIAGE,
            Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
        ]);

        $currentStatus = (string) $finding->status;

        if (! in_array($currentStatus, [
            Finding::STATUS_NEW,
            Finding::STATUS_REOPENED,
            Finding::STATUS_ACKNOWLEDGED,
        ], true)) {
            throw new InvalidArgumentException('Finding cannot be triaged from the current status.');
        }

        $now = CarbonImmutable::now();

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.triaged',
            context: [
                'metadata' => [
                    'triaged_at' => $now->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($now): void {
                $record->status = Finding::STATUS_TRIAGED;
                $record->triaged_at = $record->triaged_at ?? $now;
            },
        );
    }

    public function startProgress(Finding $finding, Tenant $tenant, User $actor): Finding
    {
        $this->authorize($finding, $tenant, $actor, [
            Capabilities::TENANT_FINDINGS_TRIAGE,
            Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
        ]);

        if (! in_array((string) $finding->status, [Finding::STATUS_TRIAGED, Finding::STATUS_ACKNOWLEDGED], true)) {
            throw new InvalidArgumentException('Finding cannot be moved to in-progress from the current status.');
        }

        $now = CarbonImmutable::now();

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.in_progress',
            context: [
                'metadata' => [
                    'in_progress_at' => $now->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($now): void {
                $record->status = Finding::STATUS_IN_PROGRESS;
                $record->in_progress_at = $record->in_progress_at ?? $now;
                $record->triaged_at = $record->triaged_at ?? $now;
            },
        );
    }

    public function assign(
        Finding $finding,
        Tenant $tenant,
        User $actor,
        ?int $assigneeUserId = null,
        ?int $ownerUserId = null,
    ): Finding {
        $this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_ASSIGN]);

        if (! $finding->hasOpenStatus()) {
            throw new InvalidArgumentException('Only open findings can be assigned.');
        }

        $this->assertTenantMemberOrNull($tenant, $assigneeUserId, 'assignee_user_id');
        $this->assertTenantMemberOrNull($tenant, $ownerUserId, 'owner_user_id');

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.assigned',
            context: [
                'metadata' => [
                    'assignee_user_id' => $assigneeUserId,
                    'owner_user_id' => $ownerUserId,
                ],
            ],
            mutate: function (Finding $record) use ($assigneeUserId, $ownerUserId): void {
                $record->assignee_user_id = $assigneeUserId;
                $record->owner_user_id = $ownerUserId;
            },
        );
    }

    public function resolve(Finding $finding, Tenant $tenant, User $actor, string $reason): Finding
    {
        $this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_RESOLVE]);

        if (! $finding->hasOpenStatus()) {
            throw new InvalidArgumentException('Only open findings can be resolved.');
        }

        $reason = $this->validatedReason($reason, 'resolved_reason');
        $now = CarbonImmutable::now();

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.resolved',
            context: [
                'metadata' => [
                    'resolved_reason' => $reason,
                    'resolved_at' => $now->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($reason, $now): void {
                $record->status = Finding::STATUS_RESOLVED;
                $record->resolved_reason = $reason;
                $record->resolved_at = $now;
            },
        );
    }

    public function close(Finding $finding, Tenant $tenant, User $actor, string $reason): Finding
    {
        $this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_CLOSE]);

        $reason = $this->validatedReason($reason, 'closed_reason');
        $now = CarbonImmutable::now();

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.closed',
            context: [
                'metadata' => [
                    'closed_reason' => $reason,
                    'closed_at' => $now->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($reason, $now, $actor): void {
                $record->status = Finding::STATUS_CLOSED;
                $record->closed_reason = $reason;
                $record->closed_at = $now;
                $record->closed_by_user_id = (int) $actor->getKey();
            },
        );
    }

    public function riskAccept(Finding $finding, Tenant $tenant, User $actor, string $reason): Finding
    {
        $this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_RISK_ACCEPT]);

        $reason = $this->validatedReason($reason, 'closed_reason');
        $now = CarbonImmutable::now();

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.risk_accepted',
            context: [
                'metadata' => [
                    'closed_reason' => $reason,
                    'closed_at' => $now->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($reason, $now, $actor): void {
                $record->status = Finding::STATUS_RISK_ACCEPTED;
                $record->closed_reason = $reason;
                $record->closed_at = $now;
                $record->closed_by_user_id = (int) $actor->getKey();
            },
        );
    }

    public function reopen(Finding $finding, Tenant $tenant, User $actor): Finding
    {
        $this->authorize($finding, $tenant, $actor, [
            Capabilities::TENANT_FINDINGS_TRIAGE,
            Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
        ]);

        if (! in_array((string) $finding->status, Finding::terminalStatuses(), true)) {
            throw new InvalidArgumentException('Only terminal findings can be reopened.');
        }

        $now = CarbonImmutable::now();
        $slaDays = $this->slaPolicy->daysForFinding($finding, $tenant);
        $dueAt = $this->slaPolicy->dueAtForSeverity((string) $finding->severity, $tenant, $now);

        return $this->mutateAndAudit(
            finding: $finding,
            tenant: $tenant,
            actor: $actor,
            action: 'finding.reopened',
            context: [
                'metadata' => [
                    'reopened_at' => $now->toIso8601String(),
                    'sla_days' => $slaDays,
                    'due_at' => $dueAt->toIso8601String(),
                ],
            ],
            mutate: function (Finding $record) use ($now, $slaDays, $dueAt): void {
                $record->status = Finding::STATUS_REOPENED;
                $record->reopened_at = $now;
                $record->resolved_at = null;
                $record->resolved_reason = null;
                $record->closed_at = null;
                $record->closed_reason = null;
                $record->closed_by_user_id = null;
                $record->sla_days = $slaDays;
                $record->due_at = $dueAt;
            },
        );
    }

    /**
     * @param  array<int, string>  $capabilities
     */
    private function authorize(Finding $finding, Tenant $tenant, User $actor, array $capabilities): void
    {
        if (! $actor->canAccessTenant($tenant)) {
            throw new NotFoundHttpException;
        }

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

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

        foreach ($capabilities as $capability) {
            if ($this->capabilityResolver->can($actor, $tenant, $capability)) {
                return;
            }
        }

        throw new AuthorizationException('Missing capability for finding workflow action.');
    }

    private function assertTenantMemberOrNull(Tenant $tenant, ?int $userId, string $field): void
    {
        if ($userId === null) {
            return;
        }

        if ($userId <= 0) {
            throw new InvalidArgumentException(sprintf('%s must be a positive user id.', $field));
        }

        $isMember = TenantMembership::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->where('user_id', $userId)
            ->exists();

        if (! $isMember) {
            throw new InvalidArgumentException(sprintf('%s must reference a current tenant member.', $field));
        }
    }

    private function validatedReason(string $reason, string $field): string
    {
        $reason = trim($reason);

        if ($reason === '') {
            throw new InvalidArgumentException(sprintf('%s is required.', $field));
        }

        if (mb_strlen($reason) > 255) {
            throw new InvalidArgumentException(sprintf('%s must be at most 255 characters.', $field));
        }

        return $reason;
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private function mutateAndAudit(
        Finding $finding,
        Tenant $tenant,
        User $actor,
        string $action,
        array $context,
        callable $mutate,
    ): Finding {
        $before = $this->auditSnapshot($finding);

        DB::transaction(function () use ($finding, $mutate): void {
            $mutate($finding);
            $finding->save();
        });

        $finding->refresh();

        $metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
        $metadata = array_merge($metadata, [
            'finding_id' => (int) $finding->getKey(),
            'before_status' => $before['status'] ?? null,
            'after_status' => $finding->status,
            'before' => $before,
            'after' => $this->auditSnapshot($finding),
        ]);

        $this->auditLogger->log(
            tenant: $tenant,
            action: $action,
            actorId: (int) $actor->getKey(),
            actorEmail: $actor->email,
            actorName: $actor->name,
            resourceType: 'finding',
            resourceId: (string) $finding->getKey(),
            context: ['metadata' => $metadata],
        );

        return $finding;
    }

    /**
     * @return array<string, mixed>
     */
    private function auditSnapshot(Finding $finding): array
    {
        return [
            'status' => $finding->status,
            'severity' => $finding->severity,
            'due_at' => $finding->due_at?->toIso8601String(),
            'sla_days' => $finding->sla_days,
            'assignee_user_id' => $finding->assignee_user_id,
            'owner_user_id' => $finding->owner_user_id,
            'triaged_at' => $finding->triaged_at?->toIso8601String(),
            'in_progress_at' => $finding->in_progress_at?->toIso8601String(),
            'reopened_at' => $finding->reopened_at?->toIso8601String(),
            'resolved_at' => $finding->resolved_at?->toIso8601String(),
            'resolved_reason' => $finding->resolved_reason,
            'closed_at' => $finding->closed_at?->toIso8601String(),
            'closed_reason' => $finding->closed_reason,
            'closed_by_user_id' => $finding->closed_by_user_id,
        ];
    }
}
