<?php
namespace Bookaro\Utils;

defined('ABSPATH') || exit;

use function bookaro_get_option;

class Logger
{
    private const FALLBACK_EMAIL = '';
    private const REDACTED = '[redacted]';
    private const DEFAULT_MAX_BYTES = 5242880; // 5MB

    /**
     * Log an error or exception.
     *
     * @param string $message  Error message
     * @param array  $context  Additional context for debugging
     * @param bool   $email    Whether to send the log via email
     */
    public static function log(string $message, array $context = [], bool $email = true): void
    {
        $email_to   = sanitize_email( bookaro_get_option( 'log_email', self::FALLBACK_EMAIL ) );

        $time     = gmdate( 'Y-m-d H:i:s' );
        $user     = is_user_logged_in() ? 'User ID: ' . get_current_user_id() : 'Guest';
        $request  = isset( $_SERVER['REQUEST_URI'] )
            ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
            : 'CLI';
        $request  = self::redact_string( $request );
        $message  = self::redact_string( $message );
        $context  = wp_json_encode( self::sanitize_context( $context ), JSON_PRETTY_PRINT );
        $logEntry = "[{$time}] {$user} @ {$request}\n{$message}\n{$context}\n\n";

        self::write_to_file( $logEntry );

        if ($email && ! empty($email_to)) {
            wp_mail(
                $email_to,
                '[MyBookaro] Error Log',
                $logEntry,
                [ 'Content-Type: text/plain; charset=' . get_bloginfo('charset') ]
            );
        }

        if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log( $logEntry );
        }
    }

    public static function get_log_file_path(): string
    {
        $upload = wp_upload_dir();
        $base = $upload['basedir'] ?? '';
        if ( $base === '' ) {
            return '';
        }
        $dir = trailingslashit( $base ) . 'bookaro';
        // Unguessable, per-site filename so the log can't be fetched at a
        // predictable URL on servers that ignore .htaccess (e.g. nginx). The
        // suffix derives from the site's auth salt: stable per-site, but not
        // externally guessable. The directory is additionally locked down by
        // .htaccess / web.config / index.html via protect_log_dir().
        $secret = ( defined( 'AUTH_SALT' ) && AUTH_SALT )
            ? AUTH_SALT
            : ( ( defined( 'SECURE_AUTH_SALT' ) && SECURE_AUTH_SALT ) ? SECURE_AUTH_SALT : 'bookaro-log' );
        $suffix = substr( hash( 'sha256', 'bookaro-log-file|' . $secret ), 0, 16 );
        $path = trailingslashit( $dir ) . 'bookaro-' . $suffix . '.log';
        /**
         * Filter the log file path for the admin log viewer.
         */
        return (string) apply_filters( 'bookaro_log_file_path', $path );
    }

    /**
     * Drop directory-protection files so log files cannot be fetched directly
     * over HTTP. Defense-in-depth alongside the unguessable filename: covers
     * Apache (.htaccess), IIS (web.config) and directory listing (index.html).
     * Idempotent and cheap — only writes guard files that are missing.
     */
    public static function protect_log_dir( string $dir ): void
    {
        if ( $dir === '' || ! is_dir( $dir ) ) {
            return;
        }
        $guards = [
            '.htaccess'  => "# MyBookaro: deny direct web access to log files.\n<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nOrder allow,deny\nDeny from all\n</IfModule>\n",
            'web.config' => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration><system.webServer><authorization><deny users=\"*\" /></authorization></system.webServer></configuration>\n",
            'index.html' => '',
        ];
        foreach ( $guards as $name => $contents ) {
            $file = trailingslashit( $dir ) . $name;
            if ( ! file_exists( $file ) ) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- One-time write of tiny static directory-guard files; WP_Filesystem would force credential prompts and is unnecessary for fixed-content guards.
                file_put_contents( $file, $contents, LOCK_EX );
            }
        }
    }

    public static function is_file_logging_enabled(): bool
    {
        return function_exists( 'bookaro_get_option' ) && ! empty( bookaro_get_option( 'log_file_enabled', 0 ) );
    }

    /**
     * Ensure the log file exists on disk.
     *
     * @param bool $force Create even when file logging is disabled.
     */
    public static function ensure_log_file( bool $force = false ): bool
    {
        if ( ! $force && ! self::is_file_logging_enabled() ) {
            return false;
        }
        $path = self::get_log_file_path();
        if ( $path === '' ) {
            return false;
        }
        $dir = dirname( $path );
        if ( ! is_dir( $dir ) ) {
            wp_mkdir_p( $dir );
        }
        self::protect_log_dir( $dir );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- One-shot directory permission probe for the log writer. WP_Filesystem would force credential prompts in some hosting environments and adds async/file-system overhead on a hot path (every log line).
        if ( ! is_dir( $dir ) || ! is_writable( $dir ) ) {
            return false;
        }
        if ( ! file_exists( $path ) ) {
            file_put_contents( $path, '', LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Direct write to Bookaro's own log file on a hot path; WP_Filesystem would force credential prompts and is unsuited to per-line logging.
        }
        return file_exists( $path );
    }

    private static function write_to_file( string $entry ): void
    {
        if ( ! self::is_file_logging_enabled() ) {
            return;
        }
        $path = self::get_log_file_path();
        if ( $path === '' ) {
            return;
        }
        $dir = dirname( $path );
        if ( ! is_dir( $dir ) ) {
            wp_mkdir_p( $dir );
        }
        self::protect_log_dir( $dir );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- See above; same hot-path log-write probe.
        if ( ! is_dir( $dir ) || ! is_writable( $dir ) ) {
            return;
        }

        $max_bytes = (int) apply_filters( 'bookaro_log_file_max_bytes', self::DEFAULT_MAX_BYTES );
        if ( file_exists( $path ) && $max_bytes > 0 ) {
            $size = filesize( $path );
            if ( $size !== false && $size > $max_bytes ) {
                $stamp = gmdate( 'Y-m-d H:i:s' );
                $truncate_notice = "[{$stamp}] Log truncated (exceeded size limit).\n\n";
                file_put_contents( $path, $truncate_notice, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Direct write to Bookaro's own log file on a hot path; WP_Filesystem would force credential prompts and is unsuited to per-line logging.
            }
        }

        file_put_contents( $path, $entry, FILE_APPEND | LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Direct append to Bookaro's own log file on a hot path; WP_Filesystem would force credential prompts and is unsuited to per-line logging.
    }

    /**
     * Purge log file if it's older than the given number of days.
     */
    public static function purge_file_older_than( int $days ): bool
    {
        $days = absint( $days );
        if ( $days < 1 ) {
            return false;
        }
        $path = self::get_log_file_path();
        if ( $path === '' || ! file_exists( $path ) ) {
            return false;
        }
        $cutoff = time() - ( $days * DAY_IN_SECONDS );
        $mtime = filemtime( $path );
        if ( $mtime !== false && $mtime < $cutoff ) {
            wp_delete_file( $path );
            return true;
        }
        return false;
    }

    private static function sanitize_context( array $context ): array
    {
        $clean = [];
        foreach ( $context as $key => $value ) {
            $key_str = is_string( $key ) ? $key : (string) $key;
            $key_lc = strtolower( $key_str );
            if ( self::is_sensitive_key( $key_lc ) ) {
                $clean[ $key_str ] = self::REDACTED;
                continue;
            }

            if ( is_array( $value ) ) {
                $clean[ $key_str ] = self::sanitize_context( $value );
                continue;
            }
            if ( is_object( $value ) ) {
                if ( method_exists( $value, '__toString' ) ) {
                    $value = (string) $value;
                } else {
                    $clean[ $key_str ] = '[object]';
                    continue;
                }
            }

            if ( is_string( $value ) ) {
                $clean[ $key_str ] = self::redact_string( $value, $key_lc );
            } else {
                $clean[ $key_str ] = $value;
            }
        }

        return $clean;
    }

    private static function is_sensitive_key( string $key ): bool
    {
        $blocked = [
            'token',
            'key',
            'license',
            'secret',
            'password',
            'nonce',
            'authorization',
            'api_key',
            'access_token',
            'refresh_token',
            'client_secret',
            'signature',
        ];
        foreach ( $blocked as $needle ) {
            if ( $key === $needle || strpos( $key, $needle ) !== false ) {
                return true;
            }
        }
        return false;
    }

    private static function redact_string( string $value, string $key_hint = '' ): string
    {
        $value = (string) $value;
        if ( $value === '' ) {
            return $value;
        }

        if ( $key_hint !== '' ) {
            if ( self::is_sensitive_key( $key_hint ) ) {
                return self::REDACTED;
            }
            if ( strpos( $key_hint, 'email' ) !== false ) {
                return self::mask_email( $value );
            }
            if ( strpos( $key_hint, 'phone' ) !== false ) {
                return self::mask_phone( $value );
            }
        }

        $value = preg_replace( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i', '[redacted-email]', $value );
        $value = preg_replace( '/Bearer\\s+[A-Za-z0-9\\-._~+\/]+=*/i', 'Bearer [redacted]', $value );
        $value = preg_replace( '/(token|key|license|secret|signature|password|nonce|authorization|api_key|access_token|refresh_token)=([A-Za-z0-9._~\\-]+)/i', '$1=' . self::REDACTED, $value );
        $value = preg_replace_callback(
            '/\\+?\\d[\\d\\s\\-()]{6,}\\d/',
            static fn( $m ) => self::mask_phone( (string) $m[0] ),
            $value
        );
        $value = preg_replace( '/(?<![A-Za-z0-9])[A-Za-z0-9._\\-]{20,}(?![A-Za-z0-9])/', '[redacted-token]', $value );

        return $value;
    }

    private static function mask_email( string $email ): string
    {
        $email = trim( $email );
        if ( $email === '' ) {
            return '';
        }
        $parts = explode( '@', $email, 2 );
        if ( count( $parts ) !== 2 ) {
            return self::REDACTED;
        }
        $local = $parts[0];
        $domain = $parts[1];
        $mask = $local !== '' ? substr( $local, 0, 1 ) . '***' : '***';
        return $mask . '@' . $domain;
    }

    private static function mask_phone( string $phone ): string
    {
        $phone = trim( $phone );
        if ( $phone === '' ) {
            return '';
        }
        $digits = preg_replace( '/\\D+/', '', $phone );
        if ( ! is_string( $digits ) || $digits === '' ) {
            return self::REDACTED;
        }
        $last4 = substr( $digits, -4 );
        $prefix = strpos( $phone, '+' ) !== false ? '+' : '';
        return $prefix . '***' . $last4;
    }
}
