<?php
declare(strict_types=1);

namespace Ekesto\CacheAutopilot\Support;

/**
 * Shared debug logger used by the invalidator and warmup engine facades.
 */
final class DebugLogger
{
    /** @var string Debug level: minimal signal. */
    public const LOG_LEVEL_MINIMAL = 'minimal';

    /** @var string Debug level: normal signal for day-to-day diagnostics. */
    public const LOG_LEVEL_NORMAL = 'normal';

    /** @var string Debug level: verbose diagnostics. */
    public const LOG_LEVEL_VERBOSE = 'verbose';

    /** @var string Log level: debug. */
    public const LEVEL_DEBUG = 'DEBUG';

    /** @var string Log level: informational. */
    public const LEVEL_INFO = 'INFO';

    /** @var string Log level: warning. */
    public const LEVEL_WARNING = 'WARNING';

    /** @var string Log level: error. */
    public const LEVEL_ERROR = 'ERROR';

    /** @var int Maximum number of request-local verbose dedupe keys. */
    private const VERBOSE_DEDUPE_MAX = 500;

    /** @var array<string,bool> Request-local map for duplicate verbose trace suppression. */
    private static array $verboseSeen = [];

    /**
     * Whether any debug destination is active.
     *
     * @param string $developerConstant Engine developer debug constant name.
     * @return bool
     */
    public static function enabled(string $developerConstant): bool
    {
        return SupportLog::isSupportModeEnabled()
            || SupportLog::isProductConstantEnabled()
            || self::isConstantTruthy($developerConstant);
    }

    /**
     * Whether verbose debug logging is enabled.
     *
     * @param string $developerConstant        Engine developer debug constant name.
     * @param string $developerVerboseConstant Engine developer verbose constant name.
     * @return bool
     */
    public static function verboseEnabled(string $developerConstant, string $developerVerboseConstant): bool
    {
        $developerActive = self::isConstantTruthy($developerConstant)
            || SupportLog::isProductConstantEnabled();

        if (!$developerActive) {
            return false;
        }

        return self::isConstantStrictTrue($developerVerboseConstant)
            || SupportLog::isProductVerboseEnabled();
    }

    /**
     * Return active debug signal level.
     *
     * @param string   $developerConstant        Engine developer debug constant name.
     * @param string   $developerVerboseConstant Engine developer verbose constant name.
     * @param string[] $levelConstants           Ordered level constant names.
     * @return string One of: minimal|normal|verbose.
     */
    public static function level(string $developerConstant, string $developerVerboseConstant, array $levelConstants): string
    {
        if (!self::enabled($developerConstant)) {
            return self::LOG_LEVEL_MINIMAL;
        }

        if (self::verboseEnabled($developerConstant, $developerVerboseConstant)) {
            return self::LOG_LEVEL_VERBOSE;
        }

        $configured = self::resolveConfiguredLevel($levelConstants);
        if ($configured !== '') {
            return $configured;
        }

        return self::LOG_LEVEL_NORMAL;
    }

    /**
     * Check whether the active debug level meets or exceeds the required level.
     *
     * @param string   $developerConstant        Engine developer debug constant name.
     * @param string   $developerVerboseConstant Engine developer verbose constant name.
     * @param string[] $levelConstants           Ordered level constant names.
     * @param string   $requiredLevel            Required level: minimal|normal|verbose.
     * @return bool
     */
    public static function allowsLevel(
        string $developerConstant,
        string $developerVerboseConstant,
        array $levelConstants,
        string $requiredLevel
    ): bool {
        if (!self::enabled($developerConstant)) {
            return false;
        }

        $required = self::normalizeLevel($requiredLevel);
        if ($required === '') {
            $required = self::LOG_LEVEL_NORMAL;
        }

        $rank = [
            self::LOG_LEVEL_MINIMAL => 0,
            self::LOG_LEVEL_NORMAL => 1,
            self::LOG_LEVEL_VERBOSE => 2,
        ];

        return $rank[self::level($developerConstant, $developerVerboseConstant, $levelConstants)] >= $rank[$required];
    }

    /**
     * Write a verbose log entry after optional level and dedupe checks.
     *
     * @param string        $prefix                   Prefix shown in log lines.
     * @param string        $developerConstant        Engine developer debug constant name.
     * @param string        $developerVerboseConstant Engine developer verbose constant name.
     * @param string        $message                  Log message.
     * @param array<string,mixed> $context            Optional structured context.
     * @param bool          $dedupe                   Whether to suppress duplicate verbose lines.
     * @param string[]|null $levelConstants           Level constants when level gating is required.
     * @return void
     */
    public static function verbose(
        string $prefix,
        string $developerConstant,
        string $developerVerboseConstant,
        string $message,
        array $context = [],
        bool $dedupe = false,
        ?array $levelConstants = null
    ): void {
        if ($levelConstants !== null) {
            if (!self::allowsLevel(
                $developerConstant,
                $developerVerboseConstant,
                $levelConstants,
                self::LOG_LEVEL_VERBOSE
            )) {
                return;
            }
        } elseif (!self::verboseEnabled($developerConstant, $developerVerboseConstant)) {
            return;
        }

        if ($dedupe && self::isDuplicateVerbose($prefix, $message, $context)) {
            return;
        }

        self::writeToErrorLog($prefix, $message, $context);
    }

    /**
     * Write a debug log entry with optional normal-level gating.
     *
     * @param string        $prefix                   Prefix shown in log lines.
     * @param string        $developerConstant        Engine developer debug constant name.
     * @param string        $developerVerboseConstant Engine developer verbose constant name.
     * @param string        $message                  Log message.
     * @param array<string,mixed> $context            Optional structured context.
     * @param string[]|null $levelConstants           Level constants when level gating is required.
     * @return void
     */
    public static function log(
        string $prefix,
        string $developerConstant,
        string $developerVerboseConstant,
        string $message,
        array $context = [],
        ?array $levelConstants = null
    ): void {
        if ($levelConstants !== null) {
            if (!self::allowsLevel(
                $developerConstant,
                $developerVerboseConstant,
                $levelConstants,
                self::LOG_LEVEL_NORMAL
            )) {
                return;
            }
        } elseif (!self::enabled($developerConstant)) {
            return;
        }

        self::writeWithLevel($prefix, $developerConstant, self::LEVEL_DEBUG, $message, $context);
    }

    /**
     * Write an informational log entry.
     *
     * @param string               $prefix            Prefix shown in log lines.
     * @param string               $developerConstant Engine developer debug constant name.
     * @param string               $message           Log message.
     * @param array<string, mixed> $context           Optional structured context.
     * @return void
     */
    public static function info(string $prefix, string $developerConstant, string $message, array $context = []): void
    {
        self::writeIfEnabled($prefix, $developerConstant, self::LEVEL_INFO, $message, $context);
    }

    /**
     * Write a warning log entry.
     *
     * @param string               $prefix            Prefix shown in log lines.
     * @param string               $developerConstant Engine developer debug constant name.
     * @param string               $message           Log message.
     * @param array<string, mixed> $context           Optional structured context.
     * @return void
     */
    public static function warn(string $prefix, string $developerConstant, string $message, array $context = []): void
    {
        self::writeIfEnabled($prefix, $developerConstant, self::LEVEL_WARNING, $message, $context);
    }

    /**
     * Write an error log entry.
     *
     * @param string               $prefix            Prefix shown in log lines.
     * @param string               $developerConstant Engine developer debug constant name.
     * @param string               $message           Log message.
     * @param array<string, mixed> $context           Optional structured context.
     * @return void
     */
    public static function error(string $prefix, string $developerConstant, string $message, array $context = []): void
    {
        self::writeIfEnabled($prefix, $developerConstant, self::LEVEL_ERROR, $message, $context);
    }

    /**
     * Write a log entry when any destination is enabled.
     *
     * @param string               $prefix            Prefix shown in log lines.
     * @param string               $developerConstant Engine developer debug constant name.
     * @param string               $level             Log level.
     * @param string               $message           Log message.
     * @param array<string, mixed> $context           Optional structured context.
     * @return void
     */
    private static function writeIfEnabled(
        string $prefix,
        string $developerConstant,
        string $level,
        string $message,
        array $context
    ): void {
        if (!self::enabled($developerConstant)) {
            return;
        }

        self::writeWithLevel($prefix, $developerConstant, $level, $message, $context);
    }

    /**
     * Route a log entry to active destinations.
     *
     * @param string               $prefix            Prefix shown in log lines.
     * @param string               $developerConstant Engine developer debug constant name.
     * @param string               $level             Log level.
     * @param string               $message           Log message.
     * @param array<string, mixed> $context           Optional structured context.
     * @return void
     */
    private static function writeWithLevel(
        string $prefix,
        string $developerConstant,
        string $level,
        string $message,
        array $context
    ): void {
        $developerActive = self::isConstantTruthy($developerConstant)
            || SupportLog::isProductConstantEnabled();

        if ($developerActive) {
            $logMessage = ($level !== self::LEVEL_DEBUG)
                ? $level . ': ' . $message
                : $message;
            self::writeToErrorLog($prefix, $logMessage, $context);
        }

        if (SupportLog::isSupportModeEnabled() || SupportLog::isProductConstantEnabled()) {
            SupportLog::write($prefix, $level, self::clamp($message), self::sanitizeContext($context));
        }
    }

    /**
     * Write a formatted line to the PHP error log.
     *
     * @param string               $prefix  Prefix shown in log lines.
     * @param string               $message Log message.
     * @param array<string, mixed> $context Optional structured context.
     * @return void
     */
    private static function writeToErrorLog(string $prefix, string $message, array $context): void
    {
        $message = self::clamp($message);
        $line = $prefix . ' ' . $message;

        if ($context !== []) {
            $line .= ' ' . wp_json_encode(self::sanitizeContext($context));
        }

        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional support/debug logger sink, gated by project debug configuration before this method is called.
        error_log($line);
    }

    /**
     * Resolve configured debug level from supported constants.
     *
     * @param string[] $levelConstants Ordered level constant names.
     * @return string Normalized level or empty string when unset/invalid.
     */
    private static function resolveConfiguredLevel(array $levelConstants): string
    {
        foreach ($levelConstants as $constantName) {
            if (!defined($constantName)) {
                continue;
            }

            $value = constant($constantName);
            if (!is_string($value)) {
                continue;
            }

            $normalized = self::normalizeLevel($value);
            if ($normalized !== '') {
                return $normalized;
            }
        }

        return '';
    }

    /**
     * Normalize debug level labels.
     *
     * @param string $level Raw level.
     * @return string Normalized known level, or empty string when invalid.
     */
    private static function normalizeLevel(string $level): string
    {
        $key = sanitize_key(strtolower(trim($level)));

        if ($key === self::LOG_LEVEL_MINIMAL || $key === self::LOG_LEVEL_NORMAL || $key === self::LOG_LEVEL_VERBOSE) {
            return $key;
        }

        return '';
    }

    /**
     * Clamp a message to a maximum character length.
     *
     * @param string $message  Message to clamp.
     * @param int    $maxChars Maximum length.
     * @return string
     */
    private static function clamp(string $message, int $maxChars = 2000): string
    {
        if (function_exists('mb_substr')) {
            return mb_substr($message, 0, $maxChars);
        }

        return substr($message, 0, $maxChars);
    }

    /**
     * Sanitize context array for safe logging.
     *
     * @param array<string, mixed> $context Raw context.
     * @return array<string, mixed>
     */
    private static function sanitizeContext(array $context): array
    {
        $out = [];

        foreach ($context as $k => $v) {
            $key = is_string($k) ? $k : (string) $k;

            $lk = strtolower($key);
            if (str_contains($lk, 'cookie') || str_contains($lk, 'authorization') || str_contains($lk, 'nonce')) {
                $out[$key] = '[redacted]';
                continue;
            }

            if (is_null($v) || is_scalar($v)) {
                $out[$key] = $v;
                continue;
            }

            if (is_array($v)) {
                $out[$key] = array_slice($v, 0, 50, true);
                continue;
            }

            $out[$key] = is_object($v) ? ('object:' . get_class($v)) : 'non-scalar';
        }

        return $out;
    }

    /**
     * Return whether a verbose line was already written during this request.
     *
     * @param string               $prefix  Prefix shown in log lines.
     * @param string               $message Log message.
     * @param array<string, mixed> $context Optional structured context.
     * @return bool
     */
    private static function isDuplicateVerbose(string $prefix, string $message, array $context): bool
    {
        $key = $prefix . '|' . self::clamp($message);

        if ($context !== []) {
            $encoded = wp_json_encode(self::sanitizeContext($context));
            if (is_string($encoded)) {
                $key .= '|' . $encoded;
            }
        }

        if (isset(self::$verboseSeen[$key])) {
            return true;
        }

        if (count(self::$verboseSeen) >= self::VERBOSE_DEDUPE_MAX) {
            self::$verboseSeen = [];
        }

        self::$verboseSeen[$key] = true;
        return false;
    }

    /**
     * Check whether a constant exists and has a truthy value.
     *
     * @param string $constantName Constant name.
     * @return bool
     */
    private static function isConstantTruthy(string $constantName): bool
    {
        return defined($constantName) && (bool) constant($constantName);
    }

    /**
     * Check whether a constant exists and is strictly true.
     *
     * @param string $constantName Constant name.
     * @return bool
     */
    private static function isConstantStrictTrue(string $constantName): bool
    {
        return defined($constantName) && constant($constantName) === true;
    }
}
