<?php

namespace MintyPHP\Service\Audit;

use MintyPHP\Http\RequestContext;

class SystemAuditRedactionService
{
    private const MAX_STRING_LENGTH = 500;
    private const MAX_ARRAY_ITEMS = 50;

    // email/to_email are PII, not credentials, but still redacted to avoid leaking addresses in audit logs.
    /** @var array<int, string> */
    private const SENSITIVE_KEYS = [
        'password',
        'password2',
        'secret',
        'token',
        'access_token',
        'refresh_token',
        'id_token',
        'authorization',
        'api_key',
        'client_secret',
        'smtp_password',
        'microsoft_shared_client_secret',
        'email',
        'to_email',
    ];

    // Only fields on this list show actual before/after values in change sets.
    // Everything else is recorded as '[REDACTED]' — callers must explicitly opt in to expose change values.
    /** @var array<int, string> */
    private const CHANGE_VALUE_ALLOWLIST = [
        'active',
        'status',
        'default_tenant_id',
        'default_role_id',
        'default_department_id',
        'app_locale',
        'app_theme',
        'app_theme_user',
        'app_registration',
        'app_primary_color',
        'api_token_default_ttl_days',
        'api_token_max_ttl_days',
        'user_inactivity_deactivate_days',
        'user_inactivity_delete_days',
        'system_audit_enabled',
        'system_audit_retention_days',
        'frontend_telemetry_enabled',
        'frontend_telemetry_sample_rate',
        'frontend_telemetry_allowed_events',
    ];

    public function redactMetadata(array $metadata): array
    {
        return $this->redactArray($metadata);
    }

    /**
     * @return array{changed_fields: list<string>, changes: array<string, array{before:mixed, after:mixed}>}
     */
    public function buildChangeSet(array $before, array $after): array
    {
        $changedFields = [];
        $changes = [];

        $allFields = array_values(array_unique(array_merge(array_keys($before), array_keys($after))));
        sort($allFields, SORT_STRING);

        foreach ($allFields as $field) {
            if (!is_string($field) || trim($field) === '') {
                continue;
            }

            $beforeValue = $before[$field] ?? null;
            $afterValue = $after[$field] ?? null;

            if ($this->valuesEqual($beforeValue, $afterValue)) {
                continue;
            }

            $changedFields[] = $field;
            if ($this->isAllowlistedChangeField($field)) {
                $changes[$field] = [
                    'before' => $this->normalizeValue($beforeValue),
                    'after' => $this->normalizeValue($afterValue),
                ];
                continue;
            }

            $changes[$field] = [
                'before' => '[REDACTED]',
                'after' => '[REDACTED]',
            ];
        }

        return [
            'changed_fields' => $changedFields,
            'changes' => $changes,
        ];
    }

    public function hashValue(string $value): string
    {
        return RequestContext::hashValue($value);
    }

    private function redactArray(array $data): array
    {
        $result = [];
        $index = 0;

        foreach ($data as $rawKey => $value) {
            if ($index >= self::MAX_ARRAY_ITEMS) {
                break;
            }

            $key = trim((string) $rawKey);
            if ($key === '') {
                continue;
            }

            if ($this->isSensitiveKey($key)) {
                $result[$key] = '[REDACTED]';
                $index++;
                continue;
            }

            if (is_array($value)) {
                $result[$key] = $this->redactArray($value);
                $index++;
                continue;
            }

            $result[$key] = $this->normalizeValue($value);
            $index++;
        }

        return $result;
    }

    private function normalizeValue(mixed $value): mixed
    {
        if (is_null($value) || is_bool($value) || is_int($value) || is_float($value)) {
            return $value;
        }

        if (is_array($value)) {
            return $this->redactArray($value);
        }

        $string = trim((string) $value);
        if ($string === '') {
            return '';
        }

        if (strlen($string) > self::MAX_STRING_LENGTH) {
            return substr($string, 0, self::MAX_STRING_LENGTH);
        }

        return $string;
    }

    // Use json_encode for arrays to get consistent comparison regardless of type coercion.
    private function valuesEqual(mixed $left, mixed $right): bool
    {
        if (is_array($left) || is_array($right)) {
            return json_encode($left) === json_encode($right);
        }

        return (string) $left === (string) $right;
    }

    private function isSensitiveKey(string $key): bool
    {
        $key = strtolower(trim($key));
        if ($key === '') {
            return false;
        }

        if (in_array($key, self::SENSITIVE_KEYS, true)) {
            return true;
        }

        // Heuristic fallback catches new sensitive keys that weren't explicitly listed.
        return str_contains($key, 'password')
            || str_contains($key, 'secret')
            || str_contains($key, 'token')
            || str_contains($key, 'authorization')
            || str_ends_with($key, '_key');
    }

    private function isAllowlistedChangeField(string $field): bool
    {
        return in_array(strtolower(trim($field)), self::CHANGE_VALUE_ALLOWLIST, true);
    }
}
