<?php

namespace Modules\SmartStorage\Entities\Services;

use Illuminate\Http\Request;
use Modules\SmartStorage\Entities\SsSetting;

/**
 * Decides whether the current request may read a stored file, applying — in
 * order — authentication, the local backend's network/device sub-mode, and the
 * global OTP gate. The verdict (and its reason) is recorded by the caller in
 * ss_access_logs, so every grant *and* denial is auditable.
 *
 * Sub-modes (local backend only):
 *   lan_only          — caller IP must be inside a configured private CIDR
 *   specific_devices  — caller's device cookie must match an approved ss_devices row
 *   tunnel            — LAN callers allowed; internet callers must present the tunnel secret
 *   this_device_only  — only the single bound device fingerprint is allowed
 */
class AccessGuard
{
    public function __construct(
        protected DeviceManager $devices,
        protected OtpService $otp
    ) {
    }

    /**
     * @return array{allowed:bool, reason:string, challenge:?string}
     */
    public function check(SsSetting $settings, Request $request): array
    {
        // The safe default backend imposes no access control at all.
        if ($settings->active_backend === 'laravel-default') {
            return $this->allow('default-backend');
        }

        // Guarded backends are for internal ERP files: require an authenticated user.
        if (!auth()->check()) {
            return $this->deny('unauthenticated');
        }

        // Network / device sub-mode only applies to the local backend.
        if ($settings->active_backend === 'local') {
            $verdict = $this->checkLocalSubmode($settings, $request);
            if (!$verdict['allowed']) {
                return $verdict;
            }
        }

        // Global OTP gate.
        if ($settings->otp_enabled && !$this->otp->hasLiveSession($request)) {
            return [
                'allowed'   => false,
                'reason'    => 'otp_required',
                'challenge' => 'otp',
            ];
        }

        return $this->allow('granted');
    }

    protected function checkLocalSubmode(SsSetting $settings, Request $request): array
    {
        $ip = $request->ip();

        switch ($settings->local_submode) {
            case 'lan_only':
                return $this->ipInAnyCidr($ip, $settings->lanCidrList())
                    ? $this->allow('lan_ok')
                    : $this->deny('not_on_lan');

            case 'specific_devices':
                $this->devices->touch($settings->business_id);
                return $this->devices->isApproved()
                    ? $this->allow('device_approved')
                    : $this->deny('device_not_approved');

            case 'tunnel':
                if ($this->ipInAnyCidr($ip, $settings->lanCidrList())) {
                    return $this->allow('lan_ok');
                }
                return $this->tunnelOk($settings, $request)
                    ? $this->allow('tunnel_ok')
                    : $this->deny('tunnel_secret_invalid');

            case 'this_device_only':
            default:
                return $this->checkBoundDevice($settings);
        }
    }

    protected function checkBoundDevice(SsSetting $settings): array
    {
        $current = $this->devices->fingerprintHash();
        $bound = $settings->bound_device_fingerprint;

        if (empty($bound)) {
            // Auto-bind to the first authenticated device that accesses storage.
            try {
                $settings->bound_device_fingerprint = $current;
                $settings->save();
                SsSetting::flushCache();
            } catch (\Throwable $e) {
                // if we cannot persist, fail closed
                return $this->deny('bind_failed');
            }
            return $this->allow('device_bound');
        }

        return hash_equals($bound, $current)
            ? $this->allow('bound_device')
            : $this->deny('not_bound_device');
    }

    protected function tunnelOk(SsSetting $settings, Request $request): bool
    {
        $secret = (string) $settings->tunnel_secret;
        if ($secret === '') {
            return false;
        }
        // The tunnel/reverse-proxy is expected to inject this header.
        $presented = (string) $request->header('X-Smart-Storage-Tunnel', '');
        return $presented !== '' && hash_equals($secret, $presented);
    }

    public function ipInAnyCidr(?string $ip, array $cidrs): bool
    {
        foreach ($cidrs as $cidr) {
            if ($this->ipInCidr($ip, $cidr)) {
                return true;
            }
        }
        return false;
    }

    public function ipInCidr(?string $ip, string $cidr): bool
    {
        if (empty($ip)) {
            return false;
        }
        if (!str_contains($cidr, '/')) {
            return $ip === $cidr;
        }

        [$subnet, $bits] = explode('/', $cidr, 2);
        $bits = (int) $bits;

        // IPv4 only (sufficient for LAN ranges); IPv6 falls through to exact match.
        $ipLong = ip2long($ip);
        $subnetLong = ip2long($subnet);
        if ($ipLong === false || $subnetLong === false) {
            return $ip === $subnet;
        }

        $mask = $bits === 0 ? 0 : (-1 << (32 - $bits)) & 0xFFFFFFFF;
        return ($ipLong & $mask) === ($subnetLong & $mask);
    }

    protected function allow(string $reason): array
    {
        return ['allowed' => true, 'reason' => $reason, 'challenge' => null];
    }

    protected function deny(string $reason): array
    {
        return ['allowed' => false, 'reason' => $reason, 'challenge' => null];
    }
}
