<?php
/**
 * TitanCart Logging Service.
 *
 * Provides a static API for logging events throughout TitanCart.
 * Respects the granular logging toggles in Settings → Logging.
 * Logs are stored in tc_logs and viewable in the admin System Logs page.
 *
 * Usage:
 *   LogService::info('orders', 'create', 'Order #42 created', ['order_id' => 42]);
 *   LogService::warning('products', 'low_stock', 'Widget stock at 2', ['product_id' => 7, 'amount' => 2]);
 *   LogService::error('database', 'errors', $exception->getMessage(), ['query' => $sql]);
 *
 * @package TitanCart\Core
 */

namespace TitanCart\Core;

defined( 'ABSPATH' ) || exit;

class LogService {

    /**
     * Cached logging settings (loaded once per request).
     * null = not loaded yet.
     * @var array|null
     */
    private static ?array $settings = null;

    /* ── Public severity methods ─────────────────────────────────────── */

    public static function debug( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'debug', $type, $action, $message, $context );
    }

    public static function info( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'info', $type, $action, $message, $context );
    }

    public static function notice( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'notice', $type, $action, $message, $context );
    }

    public static function warning( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'warning', $type, $action, $message, $context );
    }

    public static function error( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'error', $type, $action, $message, $context );
    }

    public static function critical( string $type, string $action, string $message, array $context = [] ): bool {
        return self::log( 'critical', $type, $action, $message, $context );
    }

    /* ── Core logging method ─────────────────────────────────────────── */

    /**
     * Severity level hierarchy (higher number = more severe).
     */
    private static array $levels = [
        'debug'    => 0,
        'info'     => 1,
        'notice'   => 2,
        'warning'  => 3,
        'error'    => 4,
        'critical' => 5,
    ];

    /**
     * Write a log entry to tc_logs.
     *
     * @param string $level    One of: debug, info, notice, warning, error, critical.
     * @param string $type     Log category matching Settings toggles: orders, users, products,
     *                         categories, database, requests, general, payments, checkout, security.
     * @param string $action   Sub-action: create, update, delete, status, session, failed_login,
     *                         low_stock, errors, restore, backup, optimize, http, runtime, deprecated, etc.
     * @param string $message  Human-readable description.
     * @param array  $context  Arbitrary data — object IDs, before/after values, request/response, etc.
     *                         Special keys:
     *                           'object_type'  — e.g. 'product', 'order', 'category', 'user'
     *                           'object_id'    — numeric ID of the affected entity
     *                           'user_id'      — override auto-detected user (e.g. for cron/webhook)
     *                           'backtrace'    — bool, set true to force capture call stack
     *                           'skip_check'   — bool, skip the settings toggle check (for errors that must always log)
     *
     * @return bool True if logged, false if skipped by settings or failed.
     */
    public static function log( string $level, string $type, string $action, string $message, array $context = [] ): bool {
        global $wpdb;

        // Always log errors and critical regardless of settings.
        $force = in_array( $level, [ 'error', 'critical' ], true )
              || ! empty( $context['skip_check'] );

        if ( ! $force ) {
            // Check minimum log level.
            if ( ! self::meets_min_level( $level ) ) {
                return false;
            }
            // Check per-type/action toggle.
            if ( ! self::is_enabled( $type, $action ) ) {
                return false;
            }
        }

        // Extract special context keys.
        $object_type = $context['object_type'] ?? '';
        $object_id   = (int) ( $context['object_id'] ?? 0 );
        $user_id     = (int) ( $context['user_id'] ?? self::get_current_user_id() );
        $want_bt     = ! empty( $context['backtrace'] );

        // Clean special keys from context JSON.
        unset( $context['object_type'], $context['object_id'], $context['user_id'], $context['skip_check'] );

        // Auto-backtrace on error/critical if setting is enabled.
        if ( ! $want_bt && in_array( $level, [ 'error', 'critical' ], true ) ) {
            $settings = self::load_settings();
            if ( ( $settings['log_backtrace_on_errors'] ?? '1' ) === '1' ) {
                $want_bt = true;
            }
        }

        // Capture backtrace if requested or auto-enabled.
        if ( $want_bt ) {
            unset( $context['backtrace'] ); // remove the bool flag
            $context['backtrace'] = self::capture_backtrace();
        }

        // If rich context is disabled, strip everything except backtrace.
        $settings = self::load_settings();
        if ( ( $settings['log_rich_context'] ?? '1' ) !== '1' ) {
            $bt = $context['backtrace'] ?? null;
            $context = [];
            if ( $bt ) {
                $context['backtrace'] = $bt;
            }
        }

        // Capture IP and user agent.
        $ip         = self::get_ip();
        $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 512 ) : ''; // WP.org R1 🅱2: unslash before sanitize.

        // Truncate extremely large context to 64KB.
        $context_json = wp_json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
        if ( strlen( $context_json ) > 65536 ) {
            $context['_truncated'] = true;
            $context_json = wp_json_encode( [ '_truncated' => true, 'message' => 'Context exceeded 64KB and was trimmed.' ] );
        }

        $table = $wpdb->prefix . 'tc_logs';

        $result = $wpdb->insert( $table, [
            'level'       => $level,
            'channel'     => $type,
            'action'      => $action,
            'message'     => mb_substr( $message, 0, 65535 ),
            'context'     => $context_json,
            'object_type' => $object_type,
            'object_id'   => $object_id,
            'user_id'     => $user_id,
            'ip_address'  => $ip,
            'user_agent'  => $user_agent,
            'created_at'  => current_time( 'mysql', true ),
        ] );

        return $result !== false;
    }

    /* ── Minimum level check ────────────────────────────────────────── */

    /**
     * Check if the given level meets or exceeds the configured minimum.
     */
    private static function meets_min_level( string $level ): bool {
        $settings  = self::load_settings();
        $min_level = $settings['log_min_level'] ?? 'info';
        $min_val   = self::$levels[ $min_level ] ?? 1;
        $cur_val   = self::$levels[ $level ] ?? 1;
        return $cur_val >= $min_val;
    }

    /* ── Settings check ──────────────────────────────────────────────── */

    /**
     * Check whether logging is enabled for a given type + action.
     *
     * Maps type/action to the settings keys defined in SettingsController::schema_logging().
     * Example: type='orders', action='create' → checks setting key 'log_orders_create'.
     */
    private static function is_enabled( string $type, string $action ): bool {
        $settings = self::load_settings();

        // Build the settings key.
        $key = 'log_' . $type . '_' . $action;

        // If the specific toggle exists, respect it.
        if ( isset( $settings[ $key ] ) ) {
            return $settings[ $key ] === '1';
        }

        // Some actions map to parent toggles (e.g. 'status' on orders maps to 'log_orders_change').
        $alias_map = [
            'orders_status'             => 'log_orders_change',
            'orders_note'               => 'log_orders_note',
            'users_login'               => 'log_users_session',
            'users_logout'              => 'log_users_session',
            'users_register'            => 'log_users_create',
            'payments_charge'           => 'log_payments_success',
            'payments_capture'          => 'log_payments_success',
            'payments_decline'          => 'log_payments_failed',
            'payments_error'            => 'log_payments_failed',
            'payments_webhook_received' => 'log_payments_webhook',
            'checkout_validation'       => 'log_checkout_failed',
            'checkout_stock_error'      => 'log_checkout_cart_errors',
            'checkout_add_failed'       => 'log_checkout_cart_errors',
            'security_ip_blocked'       => 'log_security_blocked',
            'security_unauthorized'     => 'log_security_permission',
            'requests_webhook'          => 'log_requests_http',
            'requests_payment'          => 'log_requests_http',
            'general_system'            => 'log_general_runtime',
            'general_php_error'         => 'log_php_errors',
        ];
        $alias_key = $type . '_' . $action;
        if ( isset( $alias_map[ $alias_key ] ) && isset( $settings[ $alias_map[ $alias_key ] ] ) ) {
            return $settings[ $alias_map[ $alias_key ] ] === '1';
        }

        // Default: log if no explicit toggle found (catch unknown event types).
        return true;
    }

    /**
     * Load all logging settings once and cache for the request.
     */
    private static function load_settings(): array {
        if ( self::$settings !== null ) {
            return self::$settings;
        }

        global $wpdb;
        $table = $wpdb->prefix . 'tc_settings';

        $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
        if ( ! $exists ) {
            self::$settings = [];
            return self::$settings;
        }

        $rows = $wpdb->get_results(
            "SELECT `key`, value FROM {$table} WHERE section = 'logging'",
            ARRAY_A
        );

        self::$settings = [];
        foreach ( $rows ?: [] as $row ) {
            self::$settings[ $row['key'] ] = $row['value'];
        }

        return self::$settings;
    }

    /**
     * Flush cached settings (call after Settings → Logging is saved).
     */
    public static function flush_cache(): void {
        self::$settings = null;
    }

    /* ── Cleanup ─────────────────────────────────────────────────────── */

    /**
     * Delete log entries older than N days.
     *
     * @param int $days Number of days to retain. 0 = keep forever.
     * @return int Number of rows deleted.
     */
    public static function cleanup_old( int $days = 0 ): int {
        if ( $days <= 0 ) {
            $settings = self::load_settings();
            $days     = (int) ( $settings['log_retention_days'] ?? 90 );
        }
        if ( $days <= 0 ) {
            return 0;
        }

        global $wpdb;
        $table    = $wpdb->prefix . 'tc_logs';
        $cutoff   = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );

        return (int) $wpdb->query(
            $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff )
        );
    }

    /**
     * Delete ALL log entries.
     *
     * @return int Number of rows deleted.
     */
    public static function cleanup_all(): int {
        global $wpdb;
        $table = $wpdb->prefix . 'tc_logs';
        $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
        $wpdb->query( "TRUNCATE TABLE {$table}" );
        return $count;
    }

    /* ── Helpers ──────────────────────────────────────────────────────── */

    private static function get_current_user_id(): int {
        if ( function_exists( 'get_current_user_id' ) ) {
            return (int) get_current_user_id();
        }
        return 0;
    }

    private static function get_ip(): string {
        // WP.org R1 🅱2: delegates to titancart_client_ip() (validated REMOTE_ADDR
        // only). Previously walked CF-Connecting-IP / X-Forwarded-For /
        // X-Real-IP first — client-forgeable, so a system-log row's IP could
        // be spoofed. Proxy setups rewrite REMOTE_ADDR at the server layer.
        return function_exists( 'titancart_client_ip' ) ? titancart_client_ip() : '';
    }

    /**
     * Capture a simplified backtrace (file + line + function, no args).
     */
    private static function capture_backtrace(): array {
        $raw   = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 15 ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- LogService IS the logging system: captures the trace into tc_ system-log rows for the admin System Logs page, not for debug output.
        $trace = [];

        // Skip the first 2 frames (this method + the log() caller).
        foreach ( array_slice( $raw, 2 ) as $frame ) {
            $file = $frame['file'] ?? '';
            // Make paths relative to plugin dir.
            if ( defined( 'TITANCART_PATH' ) && $file ) {
                $file = str_replace( TITANCART_PATH, '', $file );
            }
            $trace[] = [
                'file'     => $file,
                'line'     => $frame['line'] ?? 0,
                'function' => ( isset( $frame['class'] ) ? $frame['class'] . '::' : '' ) . ( $frame['function'] ?? '' ),
            ];
        }

        return $trace;
    }

    /* ── PHP Error Handler ────────────────────────────────────────── */

    /**
     * Register PHP error and exception handlers.
     * Call this once during plugin bootstrap (plugins_loaded).
     */
    public static function register_error_handlers(): void {
        $settings = self::load_settings();
        if ( ( $settings['log_php_errors'] ?? '1' ) !== '1' ) {
            return;
        }

        // PHP errors (warnings, notices, deprecations).
        set_error_handler( [ self::class, 'handle_php_error' ], E_ALL ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- deliberate: LogService records PHP warnings/notices into tc_ system-log rows (System Logs feature); the handler returns false so normal error handling still runs.

        // Uncaught exceptions.
        $prev = set_exception_handler( [ self::class, 'handle_exception' ] );
        // Store previous handler to chain if needed.
        self::$prev_exception_handler = $prev;
    }

    /** @var callable|null Previous exception handler to chain. */
    private static $prev_exception_handler = null;

    /**
     * Custom PHP error handler — only logs errors originating from TitanCart files.
     *
     * @return bool False to let PHP's default handler continue.
     */
    public static function handle_php_error( int $errno, string $errstr, string $errfile, int $errline ): bool {
        // Only capture errors from TitanCart plugin files.
        if ( defined( 'TITANCART_PATH' ) && strpos( $errfile, TITANCART_PATH ) === false ) {
            return false; // Not our code, let default handler deal with it.
        }

        $level_map = [
            E_ERROR             => 'critical',
            E_WARNING           => 'warning',
            E_NOTICE            => 'notice',
            E_DEPRECATED        => 'notice',
            E_USER_ERROR        => 'error',
            E_USER_WARNING      => 'warning',
            E_USER_NOTICE       => 'notice',
            E_USER_DEPRECATED   => 'notice',
            E_STRICT            => 'debug',
            E_RECOVERABLE_ERROR => 'error',
        ];

        $level = $level_map[ $errno ] ?? 'warning';
        $type_map = [
            E_ERROR             => 'E_ERROR',
            E_WARNING           => 'E_WARNING',
            E_NOTICE            => 'E_NOTICE',
            E_DEPRECATED        => 'E_DEPRECATED',
            E_USER_ERROR        => 'E_USER_ERROR',
            E_USER_WARNING      => 'E_USER_WARNING',
            E_USER_NOTICE       => 'E_USER_NOTICE',
            E_USER_DEPRECATED   => 'E_USER_DEPRECATED',
            E_STRICT            => 'E_STRICT',
            E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
        ];
        $error_type = $type_map[ $errno ] ?? 'E_UNKNOWN(' . $errno . ')';

        // Make file path relative.
        $rel_file = defined( 'TITANCART_PATH' ) ? str_replace( TITANCART_PATH, '', $errfile ) : $errfile;

        self::log( $level, 'system', 'php_error', "[{$error_type}] {$errstr}", [
            'file'      => $rel_file,
            'line'      => $errline,
            'php_errno' => $errno,
            'backtrace' => true,
        ] );

        return false; // Let default handler continue.
    }

    /**
     * Uncaught exception handler — only logs exceptions from TitanCart code.
     */
    public static function handle_exception( \Throwable $e ): void {
        $file = $e->getFile();

        // Only log if it originated from TitanCart.
        if ( defined( 'TITANCART_PATH' ) && strpos( $file, TITANCART_PATH ) !== false ) {
            $rel_file = str_replace( TITANCART_PATH, '', $file );

            self::log( 'critical', 'system', 'php_error', get_class( $e ) . ': ' . $e->getMessage(), [
                'file'       => $rel_file,
                'line'       => $e->getLine(),
                'exception'  => get_class( $e ),
                'code'       => $e->getCode(),
                'backtrace'  => true,
                'skip_check' => true, // Always log uncaught exceptions.
            ] );
        }

        // Chain to previous handler.
        if ( self::$prev_exception_handler ) {
            call_user_func( self::$prev_exception_handler, $e );
        }
    }
}
