<?php

declare(strict_types=1);

namespace ShieldX\Storage;

defined('ABSPATH') || exit;

// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_is_writable,WordPress.WP.AlternativeFunctions.file_system_operations_mkdir,WordPress.WP.AlternativeFunctions.unlink_unlink,WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- SHIELD-X manages its own local storage tree under uploads for logs, cache, quarantine, and restore data.

use ShieldX\Support\Json;
use ShieldX\Support\WordPressPaths;

final class FileStorage
{
    private const MARKER_FILE = '.shield-x-storage.json';
    private const MARKER_VERSION = 2;

    /**
     * Per-request guard. Most SHIELD-X request paths need storage paths, but
     * only the first call should touch every directory/protection file.
     *
     * @var array<string,bool>
     */
    private static $ensured = [];

    /** @var string|null */
    private static $wordpressBaseDir;

    /** @var string */
    private $baseDir;

    /**
     * @var string[]
     */
    private $children = ['cache', 'logs', 'profiles', 'jobs', 'restore', 'restore-points', 'lockdown', 'quarantine', 'db-backups', 'shadow', 'remote-storage', 'addons', 'tmp'];

    public function __construct(string $baseDir)
    {
        $this->baseDir = rtrim($baseDir, '/\\');
    }

    public static function fromWordPress(): self
    {
        if (self::$wordpressBaseDir !== null) {
            return new self(self::$wordpressBaseDir);
        }

        $configured = defined('SHIELDX_STORAGE_DIR') ? (string) constant('SHIELDX_STORAGE_DIR') : '';
        $configured = self::safeConfiguredStoragePath($configured);
        if ($configured !== '') {
            self::$wordpressBaseDir = $configured;

            return new self($configured);
        }

        $uploadsStorage = self::uploadsStoragePath();
        $outsideWebRoot = self::outsideWebRootStoragePath();

        if ($outsideWebRoot !== '' && $uploadsStorage !== '' && self::isRecognizedStorageRootAt($outsideWebRoot) && self::isRecognizedStorageRootAt($uploadsStorage)) {
            if (!self::storagePathHasUserData($outsideWebRoot) && self::storagePathHasUserData($uploadsStorage)) {
                self::$wordpressBaseDir = $uploadsStorage;

                return new self($uploadsStorage);
            }

            self::$wordpressBaseDir = $outsideWebRoot;

            return new self($outsideWebRoot);
        }

        if ($outsideWebRoot !== '' && self::isRecognizedStorageRootAt($outsideWebRoot)) {
            self::$wordpressBaseDir = $outsideWebRoot;

            return new self($outsideWebRoot);
        }

        if ($uploadsStorage !== '' && self::isRecognizedStorageRootAt($uploadsStorage)) {
            self::$wordpressBaseDir = $uploadsStorage;

            return new self($uploadsStorage);
        }

        if ($outsideWebRoot !== '') {
            self::$wordpressBaseDir = $outsideWebRoot;

            return new self($outsideWebRoot);
        }

        self::$wordpressBaseDir = $uploadsStorage !== '' ? $uploadsStorage : sys_get_temp_dir() . '/.shield-x';

        return new self(self::$wordpressBaseDir);
    }

    public function ensure(): void
    {
        $key = $this->ensureKey();
        if (isset(self::$ensured[$key])) {
            return;
        }

        $this->makeDirectory($this->baseDir);

        foreach ($this->expectedDirectories() as $directory) {
            $this->makeDirectory($directory);
        }

        $this->writeProtectionFiles();
        $this->writeMarkerFile();
        self::$ensured[$key] = true;
    }

    public function baseDir(): string
    {
        return $this->baseDir;
    }

    /**
     * @return string[]
     */
    public function expectedDirectories(): array
    {
        return array_map(
            function (string $child): string {
                return $this->baseDir . '/' . $child;
            },
            $this->children
        );
    }

    public function isWritable(): bool
    {
        return is_dir($this->baseDir) && is_writable($this->baseDir);
    }

    public function isInsideWebRoot(): bool
    {
        $root = WordPressPaths::root();
        if ($root === '') {
            return false;
        }

        $baseReal = realpath($this->baseDir);
        $rootReal = realpath($root);
        $base = is_string($baseReal) ? $baseReal : $this->baseDir;
        $root = is_string($rootReal) ? $rootReal : $root;

        return self::pathStartsWith($base, $root);
    }

    public function logFile(?int $timestamp = null): string
    {
        $timestamp = $timestamp ?: time();
        $date = function_exists('wp_date') ? wp_date('Y-m-d', $timestamp) : gmdate('Y-m-d', $timestamp);

        return $this->logDirectory() . '/' . $date . '.log';
    }

    public function logDirectory(): string
    {
        return $this->baseDir . '/logs';
    }

    public function remove(): void
    {
        if (!$this->isManagedStorageRoot()) {
            return;
        }

        $this->removePath($this->baseDir);
        unset(self::$ensured[$this->ensureKey()]);
        if (self::$wordpressBaseDir === $this->baseDir) {
            self::$wordpressBaseDir = null;
        }
    }

    public static function clearRuntimeCache(): void
    {
        self::$ensured = [];
        self::$wordpressBaseDir = null;
    }

    public function isManagedStorageRoot(): bool
    {
        $marker = $this->markerPath();
        if ($marker === '') {
            return false;
        }

        $raw = file_get_contents($marker);
        $decoded = Json::decodeArray($raw);

        return is_array($decoded) && $this->markerIsValid($decoded);
    }

    private function makeDirectory(string $path): void
    {
        if (is_dir($path)) {
            return;
        }

        if (function_exists('wp_mkdir_p')) {
            wp_mkdir_p($path);
            return;
        }

        mkdir($path, 0755, true);
    }

    private function writeProtectionFiles(): void
    {
        $htaccess = $this->baseDir . '/.htaccess';
        $apacheRules = $this->apacheDenyRules();
        $existingHtaccess = is_file($htaccess) ? file_get_contents($htaccess) : '';
        if (!is_string($existingHtaccess) || strpos($existingHtaccess, 'SHIELD-X private storage') === false || strpos($existingHtaccess, 'Require all denied') === false) {
            // Storage can be pointed at a pre-existing private directory.
            // Keep unrelated host directives intact and append only the
            // SHIELD-X protection block.
            $existingHtaccess = is_string($existingHtaccess) ? rtrim($existingHtaccess) : '';
            file_put_contents($htaccess, ($existingHtaccess !== '' ? $existingHtaccess . "\n\n" : '') . $apacheRules);
        }

        $webConfig = $this->baseDir . '/web.config';
        $iisRules = $this->iisDenyRules();
        $existingWebConfig = is_file($webConfig) ? file_get_contents($webConfig) : '';
        if (!is_string($existingWebConfig) || strpos($existingWebConfig, '<authorization>') === false) {
            file_put_contents($webConfig, $iisRules);
        }

        $index = $this->baseDir . '/index.php';
        if (!file_exists($index)) {
            file_put_contents($index, "<?php\n// Silence is golden.\n");
        }
    }

    private function writeMarkerFile(): void
    {
        $markerPath = $this->baseDir . '/' . self::MARKER_FILE;
        $createdAt = gmdate('c');
        if (is_file($markerPath)) {
            $existing = file_get_contents($markerPath);
            $decoded = Json::decodeArray($existing);
            if (is_array($decoded) && $this->markerIsValid($decoded) && isset($decoded['created_at']) && is_scalar($decoded['created_at'])) {
                $createdAt = (string) $decoded['created_at'];
            }
        }

        $marker = [
            'product' => 'SHIELD-X',
            'purpose' => 'private-plugin-storage',
            'base_dir' => $this->normalizedBaseDir(),
            'marker_version' => self::MARKER_VERSION,
            'created_at' => $createdAt,
            'updated_at' => gmdate('c'),
        ];
        $hmac = $this->markerHmac($marker);
        if ($hmac !== '') {
            $marker['hmac'] = $hmac;
        }
        $json = wp_json_encode($marker, JSON_PRETTY_PRINT);
        if (is_string($json)) {
            file_put_contents($markerPath, $json . "\n");
        }
    }

    private function ensureKey(): string
    {
        return strtolower(str_replace('\\', '/', $this->baseDir));
    }

    private static function outsideWebRootStoragePath(): string
    {
        $root = WordPressPaths::root();
        if ($root === '') {
            return '';
        }

        $rootReal = realpath($root);
        $root = is_string($rootReal) ? $rootReal : $root;
        $root = rtrim($root, '/\\');
        if ($root === '') {
            return '';
        }

        $parent = dirname($root);
        if ($parent === '' || $parent === '.' || self::pathStartsWith($parent, $root)) {
            return '';
        }

        $candidate = $parent . DIRECTORY_SEPARATOR . 'shield-x-storage';
        if (is_dir($candidate)) {
            return is_writable($candidate) ? $candidate : '';
        }

        return is_writable($parent) ? $candidate : '';
    }

    private static function uploadsStoragePath(): string
    {
        $base = self::uploadsBaseDir();

        return $base !== '' ? rtrim($base, '/\\') . '/.shield-x' : '';
    }

    private static function isRecognizedStorageRootAt(string $path): bool
    {
        return (new self($path))->hasRecognizedStorageMarker();
    }

    private function hasRecognizedStorageMarker(): bool
    {
        $marker = $this->markerPath();
        if ($marker === '') {
            return false;
        }

        $raw = file_get_contents($marker);
        $decoded = Json::decodeArray($raw);

        return is_array($decoded) && $this->markerIsValid($decoded);
    }

    /**
     * @param array<string,mixed> $marker
     */
    private function markerIsValid(array $marker): bool
    {
        if (
            (string) ($marker['product'] ?? '') !== 'SHIELD-X'
            || (string) ($marker['purpose'] ?? '') !== 'private-plugin-storage'
            || (string) ($marker['base_dir'] ?? '') !== $this->normalizedBaseDir()
        ) {
            return false;
        }

        if ((int) ($marker['marker_version'] ?? 0) !== self::MARKER_VERSION || !isset($marker['hmac']) || !is_scalar($marker['hmac'])) {
            return false;
        }

        $expected = $this->markerHmac($marker);

        return $expected !== '' && hash_equals($expected, (string) $marker['hmac']);
    }

    /**
     * @param array<string,mixed> $marker
     */
    private function markerHmac(array $marker): string
    {
        $key = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        if ($key === '') {
            return '';
        }

        $payload = implode("\n", [
            'v' . self::MARKER_VERSION,
            (string) ($marker['product'] ?? ''),
            (string) ($marker['purpose'] ?? ''),
            (string) ($marker['base_dir'] ?? ''),
            (string) ($marker['created_at'] ?? ''),
            (string) ($marker['updated_at'] ?? ''),
        ]);

        return hash_hmac('sha256', $payload, $key);
    }

    private static function storagePathHasUserData(string $path): bool
    {
        if (!is_dir($path)) {
            return false;
        }

        try {
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::SELF_FIRST
            );
        } catch (\Exception $error) {
            return false;
        }

        $root = rtrim(str_replace('\\', '/', $path), '/');
        foreach ($iterator as $item) {
            if (!$item instanceof \SplFileInfo || !$item->isFile()) {
                continue;
            }

            $itemPath = str_replace('\\', '/', $item->getPathname());
            $relative = ltrim(substr($itemPath, strlen($root)), '/');
            if (in_array($relative, ['.htaccess', 'web.config', 'index.php', self::MARKER_FILE], true)) {
                continue;
            }

            return true;
        }

        return false;
    }

    private static function safeConfiguredStoragePath(string $path): string
    {
        $path = trim($path);
        if ($path === '' || strpos($path, "\0") !== false) {
            return '';
        }

        $path = rtrim(str_replace('\\', '/', $path), '/');
        if ($path === '' || preg_match('~^(?:[A-Za-z]:)?/$~', $path) === 1) {
            return '';
        }
        if (preg_match('~(?:^|/)\.\.(?:/|$)~', $path) === 1) {
            return '';
        }

        $basename = strtolower(basename($path));
        if (strpos($basename, 'shield-x') === false) {
            return '';
        }

        $blocked = array_filter([
            WordPressPaths::root(),
            WordPressPaths::contentDir(),
            self::uploadsBaseDir(),
            sys_get_temp_dir(),
        ]);

        foreach ($blocked as $blockedPath) {
            if (self::sameNormalizedPath($path, (string) $blockedPath)) {
                return '';
            }
        }

        // Never let an opt-in override turn the plugin directory (or the
        // WordPress plugins directory that contains it) into runtime storage.
        // Unlike the exact-match root/temp guards above, descendants are also
        // unsafe here because plugin updates replace plugin files.
        $pluginsDirectory = WordPressPaths::pluginParentDir();
        if ($pluginsDirectory !== '' && self::pathStartsWith($path, $pluginsDirectory)) {
            return '';
        }

        return $path;
    }

    private static function uploadsBaseDir(): string
    {
        $uploads = function_exists('wp_upload_dir') ? wp_upload_dir(null, false) : [];

        return isset($uploads['basedir']) && is_string($uploads['basedir']) ? $uploads['basedir'] : '';
    }

    private static function sameNormalizedPath(string $left, string $right): bool
    {
        $leftReal = realpath($left);
        $rightReal = realpath($right);
        $left = is_string($leftReal) ? $leftReal : $left;
        $right = is_string($rightReal) ? $rightReal : $right;

        return rtrim(str_replace('\\', '/', $left), '/') === rtrim(str_replace('\\', '/', $right), '/');
    }

    private static function pathStartsWith(string $path, string $prefix): bool
    {
        $path = rtrim(str_replace('\\', '/', $path), '/');
        $prefix = rtrim(str_replace('\\', '/', $prefix), '/');

        return $path === $prefix || strpos($path, $prefix . '/') === 0;
    }

    private function markerPath(): string
    {
        $marker = $this->baseDir . '/' . self::MARKER_FILE;

        return is_file($marker) ? $marker : '';
    }

    private function apacheDenyRules(): string
    {
        return "# SHIELD-X private storage\n"
            . "<IfModule mod_authz_core.c>\n"
            . "    Require all denied\n"
            . "</IfModule>\n"
            . "<IfModule !mod_authz_core.c>\n"
            . "    Order allow,deny\n"
            . "    Deny from all\n"
            . "</IfModule>\n";
    }

    private function iisDenyRules(): string
    {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            . "<configuration>\n"
            . "  <system.webServer>\n"
            . "    <directoryBrowse enabled=\"false\" />\n"
            . "    <security>\n"
            . "      <authorization>\n"
            . "        <remove users=\"*\" roles=\"\" verbs=\"\" />\n"
            . "        <add accessType=\"Deny\" users=\"*\" />\n"
            . "      </authorization>\n"
            . "    </security>\n"
            . "  </system.webServer>\n"
            . "</configuration>\n";
    }

    private function removePath(string $path): void
    {
        if (!file_exists($path)) {
            return;
        }

        if (is_file($path) || is_link($path)) {
            if (!$this->canDeletePath($path)) {
                return;
            }

            if (function_exists('wp_delete_file')) {
                wp_delete_file($path);
            } else {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Unit tests can run without WordPress; production uses wp_delete_file() above.
                unlink($path);
            }
            return;
        }

        $items = scandir($path);
        if ($items === false) {
            return;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $this->removePath($path . DIRECTORY_SEPARATOR . $item);
        }

        if ($this->canDeletePath($path) && $this->directoryIsEmpty($path)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Storage cleanup removes SHIELD-X-owned directories after child files are removed.
            rmdir($path);
        }
    }

    private function canDeletePath(string $path): bool
    {
        $parent = dirname($path);

        return is_writable($path) || ($parent !== '' && is_writable($parent));
    }

    private function normalizedBaseDir(): string
    {
        $real = realpath($this->baseDir);
        $path = is_string($real) && $real !== '' ? $real : $this->baseDir;

        return rtrim(str_replace('\\', '/', $path), '/');
    }

    private function directoryIsEmpty(string $path): bool
    {
        $items = scandir($path);
        if ($items === false) {
            return false;
        }

        foreach ($items as $item) {
            if ($item !== '.' && $item !== '..') {
                return false;
            }
        }

        return true;
    }
}
