<?php

declare(strict_types=1);

namespace App\Services\Findings;

use App\Models\AuditLog;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;

final class FindingAssignmentHygieneService
{
    public const string FILTER_ALL = 'all';

    public const string REASON_BROKEN_ASSIGNMENT = 'broken_assignment';

    public const string REASON_STALE_IN_PROGRESS = 'stale_in_progress';

    private const string HYGIENE_BASELINE_TIMESTAMP = '1970-01-01 00:00:00';

    private const int STALE_IN_PROGRESS_WINDOW_DAYS = 7;

    public function __construct(
        private readonly CapabilityResolver $capabilityResolver,
        private readonly FindingWorkflowService $findingWorkflowService,
    ) {}

    /**
     * @return array<int, ManagedEnvironment>
     */
    public function visibleTenants(Workspace $workspace, User $user): array
    {
        $authorizedTenants = ManagedEnvironment::query()
            ->where('managed_environments.workspace_id', (int) $workspace->getKey())
            ->where('managed_environments.lifecycle_status', 'active')
            ->orderBy('managed_environments.name')
            ->get(['managed_environments.id', 'managed_environments.name', 'managed_environments.slug', 'managed_environments.workspace_id'])
            ->all();

        if ($authorizedTenants === []) {
            return [];
        }

        $this->capabilityResolver->primeMemberships(
            $user,
            array_map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(), $authorizedTenants),
        );

        return array_values(array_filter(
            $authorizedTenants,
            fn (ManagedEnvironment $tenant): bool => $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW),
        ));
    }

    /**
     * @return Builder<Finding>
     */
    public function issueQuery(
        Workspace $workspace,
        User $user,
        ?int $tenantId = null,
        string $reasonFilter = self::FILTER_ALL,
        bool $applyOrdering = true,
    ): Builder {
        return $this->issueQueryForVisibleTenantIds(
            $workspace,
            $this->visibleTenantIds($workspace, $user),
            $tenantId,
            $reasonFilter,
            $applyOrdering,
        );

    }

    /**
     * @param  array<int, int>  $visibleTenantIds
     * @return Builder<Finding>
     */
    private function issueQueryForVisibleTenantIds(
        Workspace $workspace,
        array $visibleTenantIds,
        ?int $tenantId = null,
        string $reasonFilter = self::FILTER_ALL,
        bool $applyOrdering = true,
    ): Builder {
        if ($tenantId !== null && ! in_array($tenantId, $visibleTenantIds, true)) {
            $visibleTenantIds = [];
        } elseif ($tenantId !== null) {
            $visibleTenantIds = [$tenantId];
        }

        $brokenAssignmentExpression = $this->brokenAssignmentExpression();
        $lastWorkflowActivityExpression = $this->lastWorkflowActivityExpression();
        $staleBindings = [$this->staleThreshold()->toDateTimeString()];
        $staleInProgressExpression = $this->staleInProgressExpression($lastWorkflowActivityExpression);

        $query = Finding::query()
            ->select('findings.*')
            ->selectRaw(
                "case when {$brokenAssignmentExpression} then 1 else 0 end as hygiene_is_broken_assignment",
            )
            ->selectRaw("{$lastWorkflowActivityExpression} as hygiene_last_workflow_activity_at")
            ->selectRaw(
                "case when {$staleInProgressExpression} then 1 else 0 end as hygiene_is_stale_in_progress",
                $staleBindings,
            )
            ->selectRaw(
                "(case when {$brokenAssignmentExpression} then 1 else 0 end + case when {$staleInProgressExpression} then 1 else 0 end) as hygiene_issue_count",
                $staleBindings,
            )
            ->with([
                'tenant',
                'ownerUser' => static fn ($relation) => $relation->withTrashed(),
                'assigneeUser' => static fn ($relation) => $relation->withTrashed(),
            ])
            ->withSubjectDisplayName()
            ->join('managed_environments', 'managed_environments.id', '=', 'findings.managed_environment_id')
            ->leftJoin('users as hygiene_assignee_lookup', 'hygiene_assignee_lookup.id', '=', 'findings.assignee_user_id')
            ->leftJoin('workspace_memberships as hygiene_assignee_workspace_membership', function ($join): void {
                $join
                    ->on('hygiene_assignee_workspace_membership.workspace_id', '=', 'findings.workspace_id')
                    ->on('hygiene_assignee_workspace_membership.user_id', '=', 'findings.assignee_user_id');
            })
            ->leftJoin('managed_environment_memberships as hygiene_assignee_scope', function ($join): void {
                $join
                    ->on('hygiene_assignee_scope.managed_environment_id', '=', 'findings.managed_environment_id')
                    ->on('hygiene_assignee_scope.user_id', '=', 'findings.assignee_user_id');
            })
            ->leftJoinSub(
                ManagedEnvironmentMembership::query()
                    ->select('managed_environment_id')
                    ->groupBy('managed_environment_id'),
                'hygiene_environment_scope_state',
                function ($join): void {
                    $join->on('hygiene_environment_scope_state.managed_environment_id', '=', 'findings.managed_environment_id');
                },
            )
            ->leftJoinSub(
                $this->latestMeaningfulWorkflowAuditSubquery(),
                'hygiene_workflow_audit',
                function ($join): void {
                    $join
                        ->on('hygiene_workflow_audit.workspace_id', '=', 'findings.workspace_id')
                        ->on('hygiene_workflow_audit.managed_environment_id', '=', 'findings.managed_environment_id')
                        ->whereRaw('hygiene_workflow_audit.resource_id = '.$this->castFindingIdToAuditResourceId());
                },
            )
            ->where('findings.workspace_id', (int) $workspace->getKey())
            ->whereIn('findings.managed_environment_id', $visibleTenantIds === [] ? [-1] : $visibleTenantIds)
            ->whereIn('findings.status', Finding::openStatusesForQuery())
            ->where(function (Builder $builder) use ($brokenAssignmentExpression, $staleInProgressExpression, $staleBindings): void {
                $builder
                    ->whereRaw($brokenAssignmentExpression)
                    ->orWhereRaw($staleInProgressExpression, $staleBindings);
            });

        $this->applyReasonFilter($query, $reasonFilter, $brokenAssignmentExpression, $staleInProgressExpression, $staleBindings);

        if (! $applyOrdering) {
            return $query;
        }

        return $query
            ->orderByRaw(
                "case when {$brokenAssignmentExpression} then 0 when {$staleInProgressExpression} then 1 else 2 end asc",
                $staleBindings,
            )
            ->orderByRaw("case when {$lastWorkflowActivityExpression} is null then 1 else 0 end asc")
            ->orderByRaw("{$lastWorkflowActivityExpression} asc")
            ->orderByRaw('case when findings.due_at is null then 1 else 0 end asc')
            ->orderBy('findings.due_at')
            ->orderBy('managed_environments.name')
            ->orderByDesc('findings.id');
    }

    /**
     * @return array{unique_issue_count: int, broken_assignment_count: int, stale_in_progress_count: int}
     */
    public function summary(Workspace $workspace, User $user, ?int $tenantId = null): array
    {
        return $this->summaryForVisibleTenantIds(
            $workspace,
            $this->visibleTenantIds($workspace, $user),
            $tenantId,
        );
    }

    /**
     * @param  array<int, int>  $visibleTenantIds
     * @return array{unique_issue_count: int, broken_assignment_count: int, stale_in_progress_count: int}
     */
    public function summaryForVisibleTenantIds(Workspace $workspace, array $visibleTenantIds, ?int $tenantId = null): array
    {
        $allIssues = $this->issueQueryForVisibleTenantIds($workspace, $visibleTenantIds, $tenantId, self::FILTER_ALL, applyOrdering: false);
        $brokenAssignments = $this->issueQueryForVisibleTenantIds($workspace, $visibleTenantIds, $tenantId, self::REASON_BROKEN_ASSIGNMENT, applyOrdering: false);
        $staleInProgress = $this->issueQueryForVisibleTenantIds($workspace, $visibleTenantIds, $tenantId, self::REASON_STALE_IN_PROGRESS, applyOrdering: false);

        return [
            'unique_issue_count' => (clone $allIssues)->count(),
            'broken_assignment_count' => (clone $brokenAssignments)->count(),
            'stale_in_progress_count' => (clone $staleInProgress)->count(),
        ];
    }

    /**
     * @return array<int, int>
     */
    public function visibleTenantIds(Workspace $workspace, User $user): array
    {
        return array_map(
            static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(),
            $this->visibleTenants($workspace, $user),
        );
    }

    /**
     * @return array<string, string>
     */
    public function filterOptions(): array
    {
        return [
            self::FILTER_ALL => 'All issues',
            self::REASON_BROKEN_ASSIGNMENT => 'Broken assignment',
            self::REASON_STALE_IN_PROGRESS => 'Stale in progress',
        ];
    }

    public function filterLabel(string $filter): string
    {
        return $this->filterOptions()[$filter] ?? $this->filterOptions()[self::FILTER_ALL];
    }

    /**
     * @return list<string>
     */
    public function reasonLabelsFor(Finding $finding): array
    {
        $labels = [];

        if ($this->recordHasBrokenAssignment($finding)) {
            $labels[] = 'Broken assignment';
        }

        if ($this->recordHasStaleInProgress($finding)) {
            $labels[] = 'Stale in progress';
        }

        return $labels;
    }

    public function lastWorkflowActivityAt(Finding $finding): ?CarbonImmutable
    {
        return $this->findingWorkflowService->lastMeaningfulActivityAt(
            $finding,
            $finding->getAttribute('hygiene_last_workflow_activity_at'),
        );
    }

    public function recordHasBrokenAssignment(Finding $finding): bool
    {
        return (int) ($finding->getAttribute('hygiene_is_broken_assignment') ?? 0) === 1;
    }

    public function recordHasStaleInProgress(Finding $finding): bool
    {
        return (int) ($finding->getAttribute('hygiene_is_stale_in_progress') ?? 0) === 1;
    }

    private function applyReasonFilter(
        Builder $query,
        string $reasonFilter,
        string $brokenAssignmentExpression,
        string $staleInProgressExpression,
        array $staleBindings,
    ): void {
        $resolvedFilter = array_key_exists($reasonFilter, $this->filterOptions())
            ? $reasonFilter
            : self::FILTER_ALL;

        if ($resolvedFilter === self::REASON_BROKEN_ASSIGNMENT) {
            $query->whereRaw($brokenAssignmentExpression);

            return;
        }

        if ($resolvedFilter === self::REASON_STALE_IN_PROGRESS) {
            $query->whereRaw($staleInProgressExpression, $staleBindings);
        }
    }

    /**
     * @return Builder<AuditLog>
     */
    private function latestMeaningfulWorkflowAuditSubquery(): Builder
    {
        return AuditLog::query()
            ->selectRaw('workspace_id, managed_environment_id, resource_id, max(recorded_at) as latest_workflow_activity_at')
            ->where('resource_type', 'finding')
            ->whereIn('action', FindingWorkflowService::meaningfulActivityActionValues())
            ->groupBy('workspace_id', 'managed_environment_id', 'resource_id');
    }

    private function brokenAssignmentExpression(): string
    {
        return '(findings.assignee_user_id is not null and ((hygiene_assignee_lookup.id is not null and hygiene_assignee_lookup.deleted_at is not null) or hygiene_assignee_workspace_membership.id is null or (hygiene_environment_scope_state.managed_environment_id is not null and hygiene_assignee_scope.id is null)))';
    }

    private function staleInProgressExpression(string $lastWorkflowActivityExpression): string
    {
        return sprintf(
            "(findings.status = '%s' and %s is not null and %s < ?)",
            Finding::STATUS_IN_PROGRESS,
            $lastWorkflowActivityExpression,
            $lastWorkflowActivityExpression,
        );
    }

    private function lastWorkflowActivityExpression(): string
    {
        $baseline = "'".self::HYGIENE_BASELINE_TIMESTAMP."'";
        $greatestExpression = match ($this->connectionDriver()) {
            'pgsql', 'mysql' => sprintf(
                'greatest(coalesce(findings.in_progress_at, %1$s), coalesce(findings.reopened_at, %1$s), coalesce(hygiene_workflow_audit.latest_workflow_activity_at, %1$s))',
                $baseline,
            ),
            default => sprintf(
                'max(coalesce(findings.in_progress_at, %1$s), coalesce(findings.reopened_at, %1$s), coalesce(hygiene_workflow_audit.latest_workflow_activity_at, %1$s))',
                $baseline,
            ),
        };

        return sprintf('nullif(%s, %s)', $greatestExpression, $baseline);
    }

    private function castFindingIdToAuditResourceId(): string
    {
        return match ($this->connectionDriver()) {
            'pgsql' => 'findings.id::text',
            'mysql' => 'cast(findings.id as char)',
            default => 'cast(findings.id as text)',
        };
    }

    private function connectionDriver(): string
    {
        return Finding::query()->getConnection()->getDriverName();
    }

    private function staleThreshold(): CarbonImmutable
    {
        return CarbonImmutable::now()->subDays(self::STALE_IN_PROGRESS_WINDOW_DAYS);
    }
}
