<?php
/**
 * ACC Security Hardening — OWASP-aligned controls for shared hosting
 * Does NOT rotate secrets/passwords; enforces runtime posture.
 */
declare(strict_types=1);

if ( defined( 'ACC_SECURITY_LOADED' ) ) {
    return;
}
define( 'ACC_SECURITY_LOADED', true );

/** True for local/dev hosts (XAMPP). Production hosts → false. */
function acc_is_local_environment(): bool {
    if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) {
        return true;
    }
    $host = strtolower( (string) ( $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '' ) );
    $host = preg_replace( '/:\d+$/', '', $host ) ?: '';
    if ( $host === '' || $host === 'localhost' || $host === '127.0.0.1' || $host === '::1' ) {
        return true;
    }
    if ( str_ends_with( $host, '.local' ) || str_ends_with( $host, '.test' ) ) {
        return true;
    }
    // Explicit override
    $force = getenv( 'ACC_FORCE_LOCAL' );
    if ( $force === '1' ) {
        return true;
    }
    $force_prod = getenv( 'ACC_FORCE_PRODUCTION' );
    if ( $force_prod === '1' ) {
        return false;
    }
    return false;
}

function acc_client_ip(): string {
    $candidates = [];
    // Only trust proxy headers if explicitly enabled (shared hosting often has reverse proxy)
    if ( getenv( 'ACC_TRUST_PROXY' ) === '1' ) {
        foreach ( [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP' ] as $h ) {
            if ( ! empty( $_SERVER[ $h ] ) ) {
                $candidates[] = (string) $_SERVER[ $h ];
            }
        }
    }
    $candidates[] = (string) ( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' );
    foreach ( $candidates as $raw ) {
        $ip = trim( explode( ',', $raw )[0] );
        if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            return $ip;
        }
    }
    return '0.0.0.0';
}

function acc_security_storage_dir(): string {
    $dir = ( defined( 'ACC_BASE_PATH' ) ? ACC_BASE_PATH : dirname( __DIR__ ) ) . '/storage/security';
    if ( ! is_dir( $dir ) ) {
        @mkdir( $dir, 0750, true );
    }
    $rate = $dir . '/rate';
    if ( ! is_dir( $rate ) ) {
        @mkdir( $rate, 0750, true );
    }
    return $dir;
}

/**
 * Sliding-window rate limit (file-based, shared-hosting safe).
 * @return true if allowed, false if blocked
 */
function acc_rate_limit( string $bucket, int $max, int $window_seconds ): bool {
    $dir  = acc_security_storage_dir() . '/rate';
    $safe = preg_replace( '/[^a-zA-Z0-9_\-\.]/', '_', $bucket ) ?: 'x';
    $file = $dir . '/' . $safe . '.json';
    $now  = time();
    $hits = [];
    if ( is_file( $file ) ) {
        $raw = @file_get_contents( $file );
        $data = json_decode( (string) $raw, true );
        if ( is_array( $data ) ) {
            $hits = array_values( array_filter( $data, static fn( $t ) => is_int( $t ) && $t > $now - $window_seconds ) );
        }
    }
    if ( count( $hits ) >= $max ) {
        return false;
    }
    $hits[] = $now;
    @file_put_contents( $file, json_encode( $hits ), LOCK_EX );
    return true;
}

function acc_rate_limit_or_fail( string $bucket, int $max, int $window_seconds, string $message = 'تعداد درخواست‌ها زیاد است. کمی بعد تلاش کنید.' ): void {
    if ( ! acc_rate_limit( $bucket, $max, $window_seconds ) ) {
        if ( ! headers_sent() ) {
            http_response_code( 429 );
            header( 'Content-Type: application/json; charset=utf-8' );
            header( 'Retry-After: ' . max( 30, (int) ceil( $window_seconds / 4 ) ) );
        }
        echo json_encode( [ 'success' => false, 'message' => $message, 'code' => 'rate_limited' ], JSON_UNESCAPED_UNICODE );
        exit;
    }
}

/** Browser security headers (HTML + API). */
function acc_send_security_headers( bool $is_api = false ): void {
    if ( headers_sent() ) {
        return;
    }

    header( 'X-Content-Type-Options: nosniff' );
    header( 'X-Frame-Options: SAMEORIGIN' );
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );
    header( 'Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()' );
    header( 'Cross-Origin-Opener-Policy: same-origin' );
    header( 'Cross-Origin-Resource-Policy: same-site' );
    header_remove( 'X-Powered-By' );
    header( 'X-Powered-By: ' ); // some SAPIs ignore remove

    $https = ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' )
        || ( (int) ( $_SERVER['SERVER_PORT'] ?? 0 ) === 443 )
        || ( strtolower( (string) ( $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '' ) ) === 'https' );

    if ( $https && ! acc_is_local_environment() ) {
        header( 'Strict-Transport-Security: max-age=31536000; includeSubDomains' );
    }

    if ( ! $is_api ) {
        // Moderate CSP — allow same-origin assets + inline needed by current templates
        $csp = [
            "default-src 'self'",
            "base-uri 'self'",
            "form-action 'self'",
            "frame-ancestors 'self'",
            "object-src 'none'",
            "img-src 'self' data: blob: https:",
            "font-src 'self' data:",
            "style-src 'self' 'unsafe-inline'",
            "script-src 'self' 'unsafe-inline'",
            "connect-src 'self'",
            "upgrade-insecure-requests",
        ];
        if ( acc_is_local_environment() ) {
            // drop upgrade-insecure-requests on localhost http
            array_pop( $csp );
        }
        header( 'Content-Security-Policy: ' . implode( '; ', $csp ) );
    }
}

/** CORS: never reflect * in production; allow same-origin + optional allowlist. */
function acc_send_cors_headers(): void {
    if ( headers_sent() ) {
        return;
    }
    $origin = (string) ( $_SERVER['HTTP_ORIGIN'] ?? '' );
    $allow  = [];
    $env    = getenv( 'ACC_CORS_ORIGINS' );
    if ( is_string( $env ) && $env !== '' ) {
        foreach ( explode( ',', $env ) as $o ) {
            $o = trim( $o );
            if ( $o !== '' ) {
                $allow[] = $o;
            }
        }
    }

    $host = (string) ( $_SERVER['HTTP_HOST'] ?? '' );
    $https = ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' )
        || ( strtolower( (string) ( $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '' ) ) === 'https' );
    $scheme = $https ? 'https' : 'http';
    if ( $host !== '' ) {
        $allow[] = $scheme . '://' . $host;
    }
    if ( acc_is_local_environment() ) {
        $allow = array_merge( $allow, [
            'http://localhost',
            'http://127.0.0.1',
            'http://localhost:8080',
            'http://127.0.0.1:8080',
        ] );
        // keep wildcard only for local API convenience
        if ( $origin === '' || acc_is_local_environment() ) {
            // Prefer explicit origin echo when present
        }
    }

    $allow = array_values( array_unique( $allow ) );
    if ( $origin !== '' && in_array( $origin, $allow, true ) ) {
        header( 'Access-Control-Allow-Origin: ' . $origin );
        header( 'Vary: Origin' );
        header( 'Access-Control-Allow-Credentials: true' );
    } elseif ( acc_is_local_environment() && $origin === '' ) {
        // same-origin browser calls don't send Origin
    } elseif ( acc_is_local_environment() ) {
        // local tools / file:// sometimes — still no *
        if ( $origin !== '' && ( str_starts_with( $origin, 'http://localhost' ) || str_starts_with( $origin, 'http://127.0.0.1' ) ) ) {
            header( 'Access-Control-Allow-Origin: ' . $origin );
            header( 'Vary: Origin' );
        }
    }

    header( 'Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS' );
    header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Cart-Session' );
    header( 'Access-Control-Max-Age: 600' );

    if ( ( $_SERVER['REQUEST_METHOD'] ?? '' ) === 'OPTIONS' ) {
        http_response_code( 204 );
        exit;
    }
}

function acc_block_dangerous_query_flags(): void {
    if ( acc_is_local_environment() ) {
        return;
    }
    foreach ( [ 'debug_routes', 'phpinfo', 'ACC_DEBUG' ] as $k ) {
        if ( isset( $_GET[ $k ] ) || isset( $_POST[ $k ] ) ) {
            http_response_code( 403 );
            header( 'Content-Type: application/json; charset=utf-8' );
            echo json_encode( [ 'success' => false, 'message' => 'Forbidden' ], JSON_UNESCAPED_UNICODE );
            exit;
        }
    }
}

/** Safe path join under a base directory (anti path-traversal). */
function acc_safe_path( string $base, string $relative ): ?string {
    $base = realpath( $base );
    if ( $base === false ) {
        return null;
    }
    $target = realpath( $base . DIRECTORY_SEPARATOR . ltrim( str_replace( [ '\\', "\0" ], [ '/', '' ], $relative ), '/' ) );
    if ( $target === false ) {
        return null;
    }
    if ( ! str_starts_with( $target, $base ) ) {
        return null;
    }
    return $target;
}

function acc_security_boot( bool $is_api = false ): void {
    // Runtime PHP hardening
    @ini_set( 'expose_php', '0' );
    @ini_set( 'allow_url_include', '0' );
    @ini_set( 'session.cookie_httponly', '1' );
    @ini_set( 'session.cookie_samesite', 'Lax' );
    if ( ! acc_is_local_environment() ) {
        @ini_set( 'session.cookie_secure', '1' );
        @ini_set( 'display_errors', '0' );
    }

    acc_block_dangerous_query_flags();
    acc_send_security_headers( $is_api );
    if ( $is_api ) {
        acc_send_cors_headers();
    }
}

/** Opaque session tokens (anti account-takeover). Stored as SHA-256 hashes. */
function acc_issue_opaque_token( string $prefix, int $subject_id, int $ttl_seconds = 86400 ): string {
    $token = $prefix . bin2hex( random_bytes( 32 ) );
    $hash  = hash( 'sha256', $token );
    $dir   = acc_security_storage_dir() . '/sessions';
    if ( ! is_dir( $dir ) ) {
        @mkdir( $dir, 0750, true );
    }
    @file_put_contents( $dir . '/' . $hash . '.json', json_encode( [
        'sid' => $subject_id,
        'exp' => time() + max( 60, $ttl_seconds ),
        'ip'  => acc_client_ip(),
    ], JSON_UNESCAPED_UNICODE ), LOCK_EX );
    return $token;
}

function acc_resolve_opaque_token( string $prefix, string $token ): int {
    $token = trim( $token );
    if ( $token === '' || ! str_starts_with( $token, $prefix ) || strlen( $token ) < strlen( $prefix ) + 32 ) {
        return 0;
    }
    $hash = hash( 'sha256', $token );
    $file = acc_security_storage_dir() . '/sessions/' . $hash . '.json';
    if ( ! is_file( $file ) ) {
        return 0;
    }
    $data = json_decode( (string) @file_get_contents( $file ), true );
    if ( ! is_array( $data ) || empty( $data['sid'] ) || empty( $data['exp'] ) ) {
        return 0;
    }
    if ( (int) $data['exp'] < time() ) {
        @unlink( $file );
        return 0;
    }
    return (int) $data['sid'];
}

function acc_demo_endpoints_allowed(): bool {
    return defined( 'ACC_ENABLE_DEMO_ENDPOINTS' ) && ACC_ENABLE_DEMO_ENDPOINTS;
}

/** Require JWT user with ACL capability (or super_admin). */
function acc_require_capability( string $cap ): bool {
    $uid = function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0;
    if ( $uid <= 0 ) {
        return false;
    }
    if ( function_exists( 'acl_user_is_super_admin' ) && acl_user_is_super_admin( $uid ) ) {
        return true;
    }
    if ( function_exists( 'acl_user_can' ) && acl_user_can( $uid, $cap ) ) {
        return true;
    }
    if ( function_exists( 'acl_can' ) && acl_can( $cap ) ) {
        return true;
    }
    return false;
}
