<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\RequestContext;
use EasyChat\Library\SafeRuntime;
use EasyChat\Library\Security\AeadEnvelopeCipher;
use EasyChat\Library\Security\PrivateFileArea;

 
final class MaxPrivateRuntime {
    private const KEY_FORMAT = 1;
    private const STORAGE_PROFILE_FORMAT = 1;
    private const STORAGE_PROFILE_TTL = 86400;

    private static $resolved_root = '';
    private static $resolved_acl_owner = false;
    private static $storage_profile = [];

    public static function root(): string {
        if (self::$resolved_root !== '') return self::$resolved_root;
        $configured = defined('EASYCHAT_PRIVATE_STORAGE_DIR')
            ? trim((string)EASYCHAT_PRIVATE_STORAGE_DIR)
            : trim((string)getenv('EASYCHAT_PRIVATE_STORAGE_DIR'));
        $candidates = [];
        if ($configured !== '') $candidates[] = rtrim($configured, '/\\') . DIRECTORY_SEPARATOR . 'phpmax';
        $site = rtrim((string)SITE_PATH, '/\\');
        $roots = self::publicRoots($site);
        if (defined('DIR_STORAGE') && self::absolute((string)DIR_STORAGE) && !self::insideAny((string)DIR_STORAGE, $roots)) {
            $candidates[] = rtrim((string)DIR_STORAGE, '/\\') . DIRECTORY_SEPARATOR . 'easychat-private' . DIRECTORY_SEPARATOR . 'phpmax';
        }
        $public = $roots ? reset($roots) : '';
        if (is_string($public) && $public !== '') {
            $candidates[] = dirname($public) . DIRECTORY_SEPARATOR . '.easychat-private' . DIRECTORY_SEPARATOR . 'phpmax';
        }
        foreach (array_unique($candidates) as $candidate) {
            if (!self::absolute($candidate) || self::insideAny($candidate, $roots)) continue;
            try {
                [$area, $acl_owner] = self::resolveArea($candidate);
                $real = realpath($candidate);
                if (is_string($real) && !self::insideAny($real, $roots)) {
                    try {
                        self::recordStorageProfile($area, $real, $acl_owner);
                    } catch (\RuntimeException $e) {
                         
                         
                         
                         
                        if ($acl_owner || self::ownerPolicy() === 'strict' || $e->getMessage() !== 'private_storage_owner_invalid') {
                            throw $e;
                        }
                        $area = new PrivateFileArea($candidate, true);
                        $acl_owner = true;
                        self::recordStorageProfile($area, $real, true);
                    }
                    self::$resolved_root = $real;
                    self::$resolved_acl_owner = $acl_owner;
                    return self::$resolved_root;
                }
            } catch (\Throwable $e) {
                continue;
            }
        }
        throw new \RuntimeException('max_private_storage_unavailable');
    }

    public static function sessionStore(int $channel_id, int $generation): MaxEncryptedSessionStore {
        if ($channel_id <= 0 || $generation <= 0) throw new \InvalidArgumentException('max_session_identity_invalid');
        $dir = self::root() . DIRECTORY_SEPARATOR . 'sessions';
        return new MaxEncryptedSessionStore(
            self::fileArea($dir),
            self::cipher(),
            $channel_id,
            $generation
        );
    }

    public static function cipher(): AeadEnvelopeCipher {
        $root = self::root();
        $area = self::fileArea($root);
        $path = $area->path('keys.json');
        $lock = $area->openLock($area->path('keys.lock'));
        if (!is_resource($lock)) throw new \RuntimeException('max_storage_key_lock_unavailable');
        try {
            $raw = $area->read($path, 4096);
            if ($raw === null) {
                $raw = json_encode([
                    'format' => self::KEY_FORMAT,
                    'active' => 'primary',
                    'keys' => ['primary' => base64_encode(random_bytes(32))],
                    'created_at' => time(),
                ], JSON_UNESCAPED_SLASHES);
                if (!is_string($raw)) throw new \RuntimeException('max_storage_key_encode_failed');
                $area->writeAtomic($path, $raw);
            }
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
        $raw = $area->read($path, 4096);
        $data = is_string($raw) ? json_decode($raw, true) : null;
        $active = is_array($data) ? (string)($data['active'] ?? '') : '';
        $encoded = is_array($data) && is_array($data['keys'] ?? null) ? (string)($data['keys'][$active] ?? '') : '';
        $key = base64_decode($encoded, true);
        if (($data['format'] ?? null) !== self::KEY_FORMAT || $active === '' || !is_string($key) || strlen($key) !== 32 || base64_encode($key) !== $encoded) {
            throw new \RuntimeException('max_storage_key_invalid');
        }
        return new AeadEnvelopeCipher($active, $key);
    }

    public static function fileArea(string $directory): PrivateFileArea {
        if (self::$resolved_root === '') self::root();
        return new PrivateFileArea($directory, self::$resolved_acl_owner);
    }

     
    public static function storageProfile(): array {
        if (self::$resolved_root === '') self::root();
        return self::$storage_profile;
    }

    




    private static function resolveArea(string $directory): array {
        try {
            return [new PrivateFileArea($directory, false), false];
        } catch (\RuntimeException $e) {
            if (self::ownerPolicy() === 'strict' || $e->getMessage() !== 'private_storage_owner_invalid') {
                throw $e;
            }
        }
        return [new PrivateFileArea($directory, true), true];
    }

    private static function ownerPolicy(): string {
        $value = defined('EASYCHAT_PRIVATE_STORAGE_OWNER_POLICY')
            ? EASYCHAT_PRIVATE_STORAGE_OWNER_POLICY
            : getenv('EASYCHAT_PRIVATE_STORAGE_OWNER_POLICY');
        $policy = strtolower(trim((string)$value));
        if (in_array($policy, ['strict', 'auto'], true)) return $policy;

         
         
        if (defined('EASYCHAT_PRIVATE_STORAGE_ALLOW_ACL_OWNER')) {
            return self::truthy(EASYCHAT_PRIVATE_STORAGE_ALLOW_ACL_OWNER) ? 'auto' : 'strict';
        }
        $legacy = getenv('EASYCHAT_PRIVATE_STORAGE_ALLOW_ACL_OWNER');
        if ($legacy !== false && trim((string)$legacy) !== '') {
            return self::truthy($legacy) ? 'auto' : 'strict';
        }
        return 'auto';
    }

    private static function truthy($value): bool {
        if (is_bool($value)) return $value;
        return in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }

    private static function recordStorageProfile(PrivateFileArea $area, string $root, bool $acl_owner): void {
        $profile = [
            'format' => self::STORAGE_PROFILE_FORMAT,
            'ready' => true,
            'ownership' => $acl_owner ? 'acl' : 'strict',
            'location' => 'outside_web_root',
            'path_fingerprint' => hash('sha256', $root),
            'checked_at' => time(),
        ];
        $path = $area->path('storage-profile.json');
        $capability_lock_path = $area->path('storage-capability.lock');
        $capability_lock = $area->openLock($capability_lock_path, true);
        if (!is_resource($capability_lock)) throw new \RuntimeException('max_storage_capability_lock_unavailable');
        try {
            $existing_raw = $area->read($path, 4096);
            $existing = is_string($existing_raw) ? json_decode($existing_raw, true) : null;
            $fresh = is_array($existing)
                && ($existing['format'] ?? null) === self::STORAGE_PROFILE_FORMAT
                && ($existing['ownership'] ?? '') === $profile['ownership']
                && ($existing['path_fingerprint'] ?? '') === $profile['path_fingerprint']
                && (int)($existing['checked_at'] ?? 0) >= time() - self::STORAGE_PROFILE_TTL;
            if ($fresh) $profile['checked_at'] = (int)$existing['checked_at'];
            else {
                $json = json_encode($profile, JSON_UNESCAPED_SLASHES);
                if (!is_string($json)) throw new \RuntimeException('max_storage_profile_encode_failed');
                $area->writeAtomic($path, $json);
                if ($area->read($path, 4096) !== $json) throw new \RuntimeException('max_storage_profile_readback_failed');
            }
        } finally {
            flock($capability_lock, LOCK_UN);
            fclose($capability_lock);
            $area->delete($capability_lock_path);
        }
        unset($profile['path_fingerprint']);
        self::$storage_profile = $profile;
    }

    private static function publicRoots(string $site): array {
        $roots = [];
        $server = RequestContext::server();
        $document_root = trim((string)($server['DOCUMENT_ROOT'] ?? ''));
        if (self::absolute($document_root)) $roots[] = $document_root;
        $cursor = $site;
        foreach (range(0, 12) as $_) {
            if (!self::absolute($cursor) || dirname($cursor) === $cursor) break;
            if (in_array(strtolower(basename($cursor)), ['public_html', 'www', 'htdocs', 'httpdocs', 'html', 'public'], true)) {
                $roots[] = $cursor;
                break;
            }
            $cursor = dirname($cursor);
        }
        $result = [];
        foreach ($roots as $root) {
            $normalized = realpath($root) ?: self::normalize($root);
            if ($normalized !== '') $result[$normalized] = true;
        }
        return array_keys($result);
    }

    private static function insideAny(string $path, array $roots): bool {
        $path = realpath($path) ?: self::normalize($path);
        if ($path === '') return true;
        foreach ($roots as $root) {
            $root = rtrim((string)$root, DIRECTORY_SEPARATOR);
            if ($path === $root || ($root !== '' && strpos($path, $root . DIRECTORY_SEPARATOR) === 0)) return true;
        }
        return false;
    }

    private static function normalize(string $path): string {
        if (!self::absolute($path)) return '';
        $parts = [];
        foreach (preg_split('#[\\\\/]#', $path) ?: [] as $part) {
            if ($part === '' || $part === '.') continue;
            if ($part === '..') array_pop($parts); else $parts[] = $part;
        }
        return DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts);
    }
    private static function absolute(string $path): bool {
        return $path !== '' && strpos($path, "\0") === false
            && ($path[0] === DIRECTORY_SEPARATOR || preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1);
    }
}
