<?php

namespace App\Support\Verification;

use App\Services\Intune\SecretClassificationService;

final class VerificationReportSanitizer
{
    /**
     * Evidence pointers must remain pointer-only. This allowlist is intentionally strict.
     *
     * @var array<int, string>
     */
    private const ALLOWED_EVIDENCE_KINDS = [
        'provider_connection_id',
        'target_scope_identifier',
        'entra_tenant_id',
        'connection_type',
        'credential_source',
        'organization_id',
        'http_status',
        'app_id',
        'observed_permissions_count',
        'missing_permission',
        'permission_type',
        'feature',
    ];

    /**
     * @return array<string, mixed>
     */
    public static function sanitizeReport(array $report): array
    {
        $sanitized = [];

        $schemaVersion = self::sanitizeShortString($report['schema_version'] ?? null, fallback: null);
        if ($schemaVersion !== null) {
            $sanitized['schema_version'] = $schemaVersion;
        }

        $flow = self::sanitizeShortString($report['flow'] ?? null, fallback: null);
        if ($flow !== null) {
            $sanitized['flow'] = $flow;
        }

        $generatedAt = self::sanitizeShortString($report['generated_at'] ?? null, fallback: null);
        if ($generatedAt !== null) {
            $sanitized['generated_at'] = $generatedAt;
        }

        if (array_key_exists('fingerprint', $report)) {
            $fingerprint = $report['fingerprint'];

            if (is_string($fingerprint)) {
                $fingerprint = strtolower(trim($fingerprint));

                if (preg_match('/^[a-f0-9]{64}$/', $fingerprint)) {
                    $sanitized['fingerprint'] = $fingerprint;
                }
            }
        }

        if (array_key_exists('previous_report_id', $report)) {
            $previousReportId = $report['previous_report_id'];

            if ($previousReportId === null || is_int($previousReportId)) {
                $sanitized['previous_report_id'] = $previousReportId;
            } elseif (is_string($previousReportId)) {
                $previousReportId = trim($previousReportId);

                if ($previousReportId === '') {
                    $sanitized['previous_report_id'] = null;
                } elseif (ctype_digit($previousReportId)) {
                    $sanitized['previous_report_id'] = (int) $previousReportId;
                } else {
                    $previousReportId = self::sanitizeShortString($previousReportId, fallback: null);

                    if ($previousReportId !== null) {
                        $sanitized['previous_report_id'] = $previousReportId;
                    }
                }
            }
        }

        if (is_array($report['identity'] ?? null)) {
            $identity = self::sanitizeIdentity((array) $report['identity']);

            if ($identity !== []) {
                $sanitized['identity'] = $identity;
            }
        }

        $summary = is_array($report['summary'] ?? null) ? (array) $report['summary'] : [];
        $summary = self::sanitizeSummary($summary);

        if ($summary !== null) {
            $sanitized['summary'] = $summary;
        }

        $checks = is_array($report['checks'] ?? null) ? (array) $report['checks'] : [];
        $checks = self::sanitizeChecks($checks);

        if ($checks !== null) {
            $sanitized['checks'] = $checks;
        }

        return $sanitized;
    }

    /**
     * @param  array<string, mixed>  $identity
     * @return array<string, mixed>
     */
    private static function sanitizeIdentity(array $identity): array
    {
        $sanitized = [];

        foreach ($identity as $key => $value) {
            if (! is_string($key) || trim($key) === '') {
                continue;
            }

            if (self::classifier()->protectsField('verification', $key)) {
                continue;
            }

            if ($key === 'target_scope' && is_array($value)) {
                $targetScope = self::sanitizeIdentityTargetScope($value);

                if ($targetScope !== []) {
                    $sanitized[$key] = $targetScope;
                }

                continue;
            }

            if (is_int($value)) {
                $sanitized[$key] = $value;

                continue;
            }

            if (! is_string($value)) {
                continue;
            }

            $value = self::sanitizeValueString($value);

            if ($value !== null) {
                $sanitized[$key] = $value;
            }
        }

        return $sanitized;
    }

    /**
     * @param  array<string, mixed>  $targetScope
     * @return array<string, string>
     */
    private static function sanitizeIdentityTargetScope(array $targetScope): array
    {
        $sanitized = [];

        foreach (['provider', 'scope_kind', 'scope_identifier', 'scope_display_name'] as $key) {
            $value = $targetScope[$key] ?? null;

            if (! is_string($value)) {
                continue;
            }

            $value = self::sanitizeValueString($value);

            if ($value !== null) {
                $sanitized[$key] = $value;
            }
        }

        return $sanitized;
    }

    /**
     * @param  array<string, mixed>  $summary
     * @return array{overall: string, counts: array{total: int, pass: int, fail: int, warn: int, skip: int, running: int}}|null
     */
    private static function sanitizeSummary(array $summary): ?array
    {
        $overall = $summary['overall'] ?? null;

        if (! is_string($overall) || ! in_array($overall, VerificationReportOverall::values(), true)) {
            return null;
        }

        $counts = is_array($summary['counts'] ?? null) ? (array) $summary['counts'] : [];

        foreach (['total', 'pass', 'fail', 'warn', 'skip', 'running'] as $key) {
            if (! is_int($counts[$key] ?? null) || $counts[$key] < 0) {
                return null;
            }
        }

        return [
            'overall' => $overall,
            'counts' => [
                'total' => $counts['total'],
                'pass' => $counts['pass'],
                'fail' => $counts['fail'],
                'warn' => $counts['warn'],
                'skip' => $counts['skip'],
                'running' => $counts['running'],
            ],
        ];
    }

    /**
     * @param  array<int, mixed>  $checks
     * @return array<int, array<string, mixed>>|null
     */
    private static function sanitizeChecks(array $checks): ?array
    {
        if ($checks === []) {
            return [];
        }

        $sanitized = [];

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

            $key = self::sanitizeShortString($check['key'] ?? null, fallback: null);
            $title = self::sanitizeShortString($check['title'] ?? null, fallback: null);
            $reasonCode = self::sanitizeShortString($check['reason_code'] ?? null, fallback: null);

            if ($key === null || $title === null || $reasonCode === null) {
                continue;
            }

            $status = $check['status'] ?? null;
            if (! is_string($status) || ! in_array($status, VerificationCheckStatus::values(), true)) {
                continue;
            }

            $severityRaw = $check['severity'] ?? null;
            if (! is_string($severityRaw)) {
                continue;
            }

            $severity = strtolower(trim($severityRaw));

            if ($severity !== '' && ! in_array($severity, VerificationCheckSeverity::values(), true)) {
                continue;
            }

            $messageRaw = $check['message'] ?? null;
            if (! is_string($messageRaw) || trim($messageRaw) === '') {
                continue;
            }

            $blocking = is_bool($check['blocking'] ?? null) ? (bool) $check['blocking'] : false;

            $sanitized[] = [
                'key' => $key,
                'title' => self::normalizeCheckTitle($key, $title),
                'status' => $status,
                'severity' => $severity,
                'blocking' => $blocking,
                'reason_code' => $reasonCode,
                'message' => self::sanitizeMessage($messageRaw),
                'evidence' => self::sanitizeEvidence(is_array($check['evidence'] ?? null) ? (array) $check['evidence'] : []),
                'next_steps' => self::sanitizeNextStepsPayload($check['next_steps'] ?? null),
            ];
        }

        return $sanitized;
    }

    private static function normalizeCheckTitle(string $key, string $title): string
    {
        return match ($key) {
            'permissions.admin_consent' => 'Required application permissions',
            default => $title,
        };
    }

    /**
     * @param  array<int, mixed>  $evidence
     * @return array<int, array{kind: string, value: int|string}>
     */
    private static function sanitizeEvidence(array $evidence): array
    {
        $sanitized = [];

        foreach ($evidence as $pointer) {
            if (! is_array($pointer)) {
                continue;
            }

            $kind = $pointer['kind'] ?? null;
            if (! is_string($kind) || trim($kind) === '') {
                continue;
            }

            $kind = trim($kind);

            if (! in_array($kind, self::ALLOWED_EVIDENCE_KINDS, true)) {
                continue;
            }

            $value = $pointer['value'] ?? null;

            if (is_int($value)) {
                $sanitized[] = ['kind' => $kind, 'value' => $value];

                continue;
            }

            if (! is_string($value)) {
                continue;
            }

            $sanitizedValue = self::sanitizeValueString($value);

            if ($sanitizedValue === null) {
                continue;
            }

            $sanitized[] = ['kind' => $kind, 'value' => $sanitizedValue];
        }

        return $sanitized;
    }

    /**
     * @return array<int, array{label: string, url: string}>
     */
    public static function sanitizeNextStepsPayload(mixed $nextSteps): array
    {
        if (! is_array($nextSteps)) {
            return [];
        }

        return self::sanitizeNextSteps($nextSteps);
    }

    /**
     * @param  array<int, mixed>  $nextSteps
     * @return array<int, array{label: string, url: string}>
     */
    private static function sanitizeNextSteps(array $nextSteps): array
    {
        $sanitized = [];

        foreach ($nextSteps as $step) {
            if (! is_array($step)) {
                continue;
            }

            $label = self::sanitizeShortString($step['label'] ?? null, fallback: null);
            $url = self::sanitizeUrlString($step['url'] ?? null, fallback: null);

            if ($label === null || $url === null) {
                continue;
            }

            $sanitized[] = [
                'label' => $label,
                'url' => $url,
            ];
        }

        return $sanitized;
    }

    private static function sanitizeUrlString(mixed $value, ?string $fallback): ?string
    {
        if (! is_string($value)) {
            return $fallback;
        }

        $value = trim($value);

        if ($value === '') {
            return $fallback;
        }

        if (self::containsSecretQueryParameter($value)) {
            return $fallback;
        }

        // URLs can be long (OAuth consent / remediation links). Truncating too aggressively can
        // remove required query parameters (e.g. `scope`) and break the remediation flow.
        $value = substr($value, 0, 2048);

        if (str_starts_with($value, '/')) {
            return $value;
        }

        if (str_starts_with($value, 'https://') || str_starts_with($value, 'http://')) {
            return $value;
        }

        return $fallback;
    }

    private static function sanitizeMessage(mixed $message): string
    {
        if (! is_string($message)) {
            return '—';
        }

        $message = trim(str_replace(["\r", "\n"], ' ', $message));
        $message = self::classifier()->sanitizeAuditString($message);
        $message = preg_replace('/\b[A-Za-z0-9\-\._~\+\/]{64,}\b/', '[REDACTED]', $message) ?? $message;
        $message = preg_replace('/\b(access_token|refresh_token|client_secret|password|authorization)\b\s*[:=]\s*\[REDACTED\]/i', '[REDACTED]', $message) ?? $message;
        $message = preg_replace('/"(access_token|refresh_token|client_secret|password|authorization)"\s*:\s*"\[REDACTED\]"/i', '"[REDACTED]"', $message) ?? $message;

        return $message === '' ? '—' : substr($message, 0, 240);
    }

    private static function sanitizeShortString(mixed $value, ?string $fallback): ?string
    {
        if (! is_string($value)) {
            return $fallback;
        }

        $value = trim($value);

        if ($value === '') {
            return $fallback;
        }

        return substr($value, 0, 200);
    }

    private static function sanitizeValueString(string $value): ?string
    {
        $value = trim($value);

        if ($value === '') {
            return null;
        }

        if (preg_match('/\bBearer\s+[A-Za-z0-9\-\._~\+\/]+=*\b/i', $value)) {
            return null;
        }

        if (strlen($value) > 512) {
            return null;
        }

        if (preg_match('/\b[A-Za-z0-9\-\._~\+\/]{128,}\b/', $value)) {
            return null;
        }

        if (self::containsSecretQueryParameter($value)) {
            return null;
        }

        return $value;
    }

    private static function containsSecretQueryParameter(string $value): bool
    {
        return preg_match('/(?:[?&]|^)(access_token|refresh_token|client_secret|password|authorization)=/i', $value) === 1;
    }

    private static function classifier(): SecretClassificationService
    {
        return app(SecretClassificationService::class);
    }
}
