<?php
declare(strict_types=1);

namespace Ekesto\CacheAutopilot\Support;

/**
 * File management and option handling for the support debug log system.
 *
 * Coordinates a shared WP option (`ekesto_ca_debug`) that both ECI and ECW
 * read independently to write structured log lines to a single file.
 */
final class SupportLog
{
    /** @var string Shared WP option key. */
    public const OPTION_KEY = 'ekesto_ca_debug';

    /** @var string Product-level debug constant name. */
    private const PRODUCT_CONSTANT = 'EKESTO_CA_DEBUG';

    /** @var string Product-level verbose constant name. */
    private const PRODUCT_VERBOSE_CONSTANT = 'EKESTO_CA_DEBUG_VERBOSE';

    /** @var string Log subdirectory name under uploads. */
    private const LOG_DIR_NAME = 'cache-autopilot-logs';

    /** @var int Maximum log file size in bytes (10 MB). */
    private const MAX_FILE_SIZE = 10 * 1024 * 1024;

    /** @var int Maximum age for old log files in seconds (7 days). */
    private const MAX_LOG_AGE = 7 * 24 * 3600;

    /** @var int Minimum interval between cleanup runs in seconds (4 hours). */
    private const CLEANUP_THROTTLE = 4 * 3600;

    /** @var string Transient key for cleanup throttling. */
    private const CLEANUP_TRANSIENT = 'ekesto_ca_log_cleanup_last';

    /** @var string Strict regex for valid log filenames. */
    private const FILENAME_PATTERN = '/^cache-autopilot-[a-f0-9]{8}-\d{8}-\d{6}-[a-f0-9]{12}\.(?:log|txt)$/';

    /** @var string Strict regex for plugin-managed log files, including rotations. */
    private const MANAGED_FILENAME_PATTERN = '/^cache-autopilot-[a-f0-9]{8}-\d{8}-\d{6}-[a-f0-9]{12}\.(?:log|txt)(?:\.rotated)?$/';

    /** @var bool Request-local flag to prevent repeated cleanup attempts. */
    private static bool $cleanupRanThisRequest = false;

    /**
     * Check whether support mode is enabled via the shared option.
     *
     * @return bool
     */
    public static function isSupportModeEnabled(): bool
    {
        $opt = self::getOption();
        return !empty($opt['enabled']);
    }

    /**
     * Check whether the product-level debug constant is active.
     *
     * @return bool
     */
    public static function isProductConstantEnabled(): bool
    {
        return defined(self::PRODUCT_CONSTANT) && constant(self::PRODUCT_CONSTANT);
    }

    /**
     * Check whether the product-level verbose constant is active.
     *
     * @return bool
     */
    public static function isProductVerboseEnabled(): bool
    {
        return self::isProductConstantEnabled()
            && defined(self::PRODUCT_VERBOSE_CONSTANT)
            && constant(self::PRODUCT_VERBOSE_CONSTANT) === true;
    }

    /**
     * Enable support debug mode. Generates a fresh log filename and stores the option.
     *
     * @return array{enabled: bool, log_file: string} Updated option value.
     */
    public static function enable(): array
    {
        $filename = self::generateLogFilename();
        $opt = [
            'enabled' => true,
            'log_file' => $filename,
        ];
        update_option(self::OPTION_KEY, $opt, false);

        return $opt;
    }

    /**
     * Disable support debug mode. Deletes the active log file and updates the option.
     *
     * @return array{enabled: bool, log_file: string} Updated option value.
     */
    public static function disable(): array
    {
        self::deleteLogFile();

        $opt = self::normalizeOption([
            'enabled' => false,
            'log_file' => '',
        ]);
        update_option(self::OPTION_KEY, $opt, false);

        return $opt;
    }

    /**
     * Write a formatted log line to the support log file.
     *
     * Only writes when support mode or the product constant is active.
     *
     * @param string               $prefix  Product prefix (e.g. '[ECI]' or '[ECW]').
     * @param string               $level   Log level (INFO, WARNING, ERROR).
     * @param string               $message Log message.
     * @param array<string, mixed> $context Optional structured context.
     * @return void
     */
    public static function write(string $prefix, string $level, string $message, array $context = []): void
    {
        $filePath = self::resolveActiveLogPath();
        if ($filePath === null) {
            return;
        }

        if (!self::ensureLogDir()) {
            return;
        }

        $timestamp = gmdate('Y-m-d H:i:s') . ' UTC (' . wp_date('Y-m-d H:i:s') . ' local)';
        $line = sprintf('%s %s %s %s', $timestamp, $prefix, $level, $message);

        if ($context !== []) {
            $encoded = wp_json_encode($context);
            if (is_string($encoded)) {
                $line .= ' ' . $encoded;
            }
        }

        $line .= "\n";

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
        file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX);

        self::maybeCleanup();
        self::maybeRotate($filePath);
    }

    /**
     * Return the resolved log directory path.
     *
     * @return string Absolute directory path (no trailing slash).
     */
    public static function getLogDir(): string
    {
        $uploads = wp_upload_dir(null, false);
        $baseDir = $uploads['basedir'] ?? '';

        return rtrim($baseDir, '/\\') . '/' . self::LOG_DIR_NAME;
    }

    /**
     * Build UI status payload for the settings boot data and REST responses.
     *
     * @return array{enabled: bool, has_log: bool, file_size: int, file_size_human: string, filename: string}
     */
    public static function getStatusForUi(): array
    {
        $opt = self::getOption();
        $enabled = !empty($opt['enabled']);
        $filename = $opt['log_file'] ?? '';
        $filePath = self::resolveLogPath($filename);
        $hasLog = $filePath !== null && is_file($filePath);
        $fileSize = $hasLog ? (int) filesize($filePath) : 0;

        return [
            'enabled' => $enabled,
            'has_log' => $hasLog,
            'file_size' => $fileSize,
            'file_size_human' => $hasLog ? size_format($fileSize) : '',
            'filename' => $hasLog ? $filename : '',
        ];
    }

    /**
     * Delete the active log file. Does not change the enabled state.
     * Assigns a fresh filename so subsequent writes go to a new file.
     *
     * @return bool True if file was deleted or did not exist.
     */
    public static function deleteLogFile(): bool
    {
        $opt = self::getOption();
        $filename = $opt['log_file'] ?? '';
        $filePath = self::resolveLogPath($filename);
        $deleted = true;

        if ($filePath !== null && is_file($filePath)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            $deleted = unlink($filePath);
        }

        // If still enabled, assign a fresh filename for subsequent writes.
        if (!empty($opt['enabled']) && $deleted) {
            $opt['log_file'] = self::generateLogFilename();
            update_option(self::OPTION_KEY, $opt, false);
        }

        return $deleted;
    }

    /**
     * Get the absolute path to the active log file, or null if unavailable.
     *
     * @return string|null
     */
    public static function getActiveLogPath(): ?string
    {
        return self::resolveActiveLogPath();
    }

    /**
     * Remove log files older than the configured max age.
     *
     * Throttled to run at most once per CLEANUP_THROTTLE interval.
     *
     * @return int Number of files removed.
     */
    public static function cleanupOldLogs(): int
    {
        $logDir = self::getLogDir();
        if (!is_dir($logDir)) {
            return 0;
        }

        $now = time();
        $removed = 0;
        $opt = self::getOption();
        $activeFilename = $opt['log_file'] ?? '';

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_scandir
        $files = scandir($logDir);
        if (!is_array($files)) {
            return 0;
        }

        foreach ($files as $file) {
            if ($file === '.' || $file === '..' || $file === 'index.php' || $file === '.htaccess') {
                continue;
            }

            // Skip the active log file.
            if ($file === $activeFilename) {
                continue;
            }

            // Only touch plugin-managed support log files, including rotations.
            if (!self::isManagedFilename($file)) {
                continue;
            }

            $filePath = $logDir . '/' . $file;
            if (!is_file($filePath)) {
                continue;
            }

            $mtime = (int) filemtime($filePath);
            if (($now - $mtime) > self::MAX_LOG_AGE) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
                if (unlink($filePath)) {
                    ++$removed;
                }
            }
        }

        return $removed;
    }

    /**
     * Remove all plugin-managed support log files and cleanup state.
     *
     * @return void
     */
    public static function purgeAllLogs(): void
    {
        self::deleteLogFile();
        delete_transient(self::CLEANUP_TRANSIENT);

        $logDir = self::getLogDir();
        if (!is_dir($logDir)) {
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_scandir
        $files = scandir($logDir);
        if (!is_array($files)) {
            return;
        }

        foreach ($files as $file) {
            if (!self::isManagedFilename($file)) {
                continue;
            }

            $filePath = $logDir . '/' . $file;
            if (!is_file($filePath)) {
                continue;
            }

            // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
            unlink($filePath);
        }
    }

    /**
     * Ensure the log directory exists with directory traversal protections.
     *
     * @return bool True if directory is ready for writing.
     */
    public static function ensureLogDir(): bool
    {
        $logDir = self::getLogDir();

        if (!is_dir($logDir)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
            if (!wp_mkdir_p($logDir)) {
                return false;
            }
        }

        // Prevent directory listing.
        $indexPath = $logDir . '/index.php';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
        if (file_put_contents($indexPath, '', LOCK_EX) === false) {
            return false;
        }

        // Block direct HTTP access.
        $htaccessPath = $logDir . '/.htaccess';
        if (!is_file($htaccessPath)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
            if (file_put_contents($htaccessPath, "Deny from all\n", LOCK_EX) === false) {
                return false;
            }
        }

        return is_file($indexPath)
            && filesize($indexPath) === 0
            && is_file($htaccessPath);
    }

    /**
     * Get and normalize the shared option.
     *
     * @return array{enabled: bool, log_file: string}
     */
    private static function getOption(): array
    {
        $raw = get_option(self::OPTION_KEY, []);
        return self::normalizeOption(is_array($raw) ? $raw : []);
    }

    /**
     * Normalize option shape to canonical defaults.
     *
     * @param array<string, mixed> $raw Raw option value.
     * @return array{enabled: bool, log_file: string}
     */
    private static function normalizeOption(array $raw): array
    {
        return [
            'enabled' => !empty($raw['enabled']),
            'log_file' => is_string($raw['log_file'] ?? null) ? ($raw['log_file'] ?? '') : '',
        ];
    }

    /**
     * Generate a unique log filename.
     *
     * Format: cache-autopilot-<sitehash8>-<date>-<time>-<random12>.txt
     *
     * @return string
     */
    private static function generateLogFilename(): string
    {
        $siteHash = substr(md5(get_site_url()), 0, 8);
        $timestamp = gmdate('Ymd-His');
        $random = bin2hex(random_bytes(6));

        return sprintf('cache-autopilot-%s-%s-%s.txt', $siteHash, $timestamp, $random);
    }

    /**
     * Validate a filename against the expected log filename pattern.
     *
     * @param string $filename Filename to validate.
     * @return bool
     */
    private static function isValidFilename(string $filename): bool
    {
        return (bool) preg_match(self::FILENAME_PATTERN, $filename);
    }

    /**
     * Validate a plugin-managed log filename, including rotated files.
     *
     * @param string $filename Filename to validate.
     * @return bool
     */
    private static function isManagedFilename(string $filename): bool
    {
        return (bool) preg_match(self::MANAGED_FILENAME_PATTERN, $filename);
    }

    /**
     * Resolve a filename to an absolute log file path, with validation.
     *
     * @param string $filename Log filename.
     * @return string|null Absolute path or null if invalid.
     */
    private static function resolveLogPath(string $filename): ?string
    {
        if ($filename === '' || !self::isValidFilename($filename)) {
            return null;
        }

        return self::getLogDir() . '/' . $filename;
    }

    /**
     * Resolve the active log file path from the stored option.
     *
     * @return string|null Absolute path or null if support mode is inactive or filename is invalid.
     */
    private static function resolveActiveLogPath(): ?string
    {
        if (!self::isSupportModeEnabled() && !self::isProductConstantEnabled()) {
            return null;
        }

        $opt = self::getOption();
        $filename = $opt['log_file'] ?? '';

        return self::resolveLogPath($filename);
    }

    /**
     * Opportunistically run old-log cleanup if throttle has expired.
     *
     * @return void
     */
    private static function maybeCleanup(): void
    {
        if (self::$cleanupRanThisRequest) {
            return;
        }

        self::$cleanupRanThisRequest = true;

        $lastRun = (int) get_transient(self::CLEANUP_TRANSIENT);
        if ($lastRun > 0 && (time() - $lastRun) < self::CLEANUP_THROTTLE) {
            return;
        }

        set_transient(self::CLEANUP_TRANSIENT, time(), self::CLEANUP_THROTTLE);
        self::cleanupOldLogs();
    }

    /**
     * Rotate the log file if it exceeds the maximum size.
     *
     * Instead of truncating, renames the oversized file and assigns a fresh filename.
     *
     * @param string $filePath Absolute path to the current log file.
     * @return void
     */
    private static function maybeRotate(string $filePath): void
    {
        if (!is_file($filePath)) {
            return;
        }

        $size = (int) filesize($filePath);
        if ($size < self::MAX_FILE_SIZE) {
            return;
        }

        // Rename the oversized file with a .rotated suffix so cleanup can age it out.
        $rotatedPath = $filePath . '.rotated';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
        rename($filePath, $rotatedPath);

        // Assign a fresh filename for subsequent writes.
        $opt = self::getOption();
        if (!empty($opt['enabled'])) {
            $opt['log_file'] = self::generateLogFilename();
            update_option(self::OPTION_KEY, $opt, false);
        }
    }
}
