<?php
/**
 * Logger — Lightweight logging service.
 *
 * Writes entries to the WordPress error log and, optionally, to the
 * security_logs database table (when available).
 *
 * Implements a singleton so any part of the plugin can call:
 *   Logger::get_instance()->info( 'message' );
 *   Logger::cleanup_old_logs( [ 'days' => 30 ] );
 *
 * @package UltraguardPro\core
 */

namespace UltraguardPro\core;

use UltraguardPro\core\DatabaseCrud;

if ( ! defined( 'ABSPATH' ) ) { exit; }

// CacheManager is needed to invalidate cached queries after log pruning.
// The class is always available at this point because Logger is only called
// after the plugin has booted (plugins_loaded priority 10).

class Logger {

    private static ?Logger $instance = null;
    private static bool $db_write_in_progress = false;

    /** Minimum level to write. Set via ultraguard_pro_log_level filter. */
    private string $min_level = 'info';

    private const LEVELS = [ 'debug' => 0, 'info' => 1, 'warning' => 2, 'error' => 3, 'critical' => 4 ];

    private function __construct() {
        $this->min_level = apply_filters( 'ultraguard_pro_log_level', 'info' );
    }

    public static function get_instance(): self {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // ── Convenience static aliases ────────────────────────────────────────────

    public static function debug( string $message, array $context = [] ): void {
        self::get_instance()->log( 'debug', $message, $context );
    }

    public static function info( string $message, array $context = [] ): void {
        self::get_instance()->log( 'info', $message, $context );
    }

    public static function warning( string $message, array $context = [] ): void {
        self::get_instance()->log( 'warning', $message, $context );
    }

    public static function error( string $message, array $context = [] ): void {
        self::get_instance()->log( 'error', $message, $context );
    }

    public static function critical( string $message, array $context = [] ): void {
        self::get_instance()->log( 'critical', $message, $context );
    }

    // ── Maintenance ───────────────────────────────────────────────────────────

    /**
     * Delete old log files from the uploads log directory.
     *
     * @param array{ days: int } $options
     */
    public static function cleanup_old_logs( array $options = [] ): void {
        $days      = max( 1, (int) ( $options['days'] ?? 30 ) );
        $cutoff    = time() - ( $days * DAY_IN_SECONDS );
        $upload    = wp_upload_dir();
        $log_dir   = trailingslashit( $upload['basedir'] ) . 'ultraguard-security-suite/logs/';

        if ( ! is_dir( $log_dir ) ) { return; }

        foreach ( glob( $log_dir . '*.log' ) ?: [] as $file ) {
            if ( filemtime( $file ) < $cutoff ) {
                wp_delete_file( $file );
            }
        }
    }

    /**
     * Purge old rows from the security_logs DB table.
     *
     * @param array{ days: int } $options
     */
    public static function cleanup_database_logs( array $options = [] ): void {
        $days = max( 1, (int) ( $options['days'] ?? 90 ) );

        // Route through DatabaseCrud so the cache is invalidated automatically
        // and all DB access goes through a single controlled path.
        $deleted = DatabaseCrud::delete_old_records( 'ultraguard_security_logs', 'created_at', $days );

        if ( false !== $deleted ) {
            // delete_old_records() already calls clear_table_cache(); belt-and-suspenders
            // explicit invalidation ensures the dashboard count refreshes immediately.
            CacheManager::get_instance()->invalidate_db( 'ultraguard_security_logs' );
        }
    }

    // ── Core write ────────────────────────────────────────────────────────────

    public function log( string $level, string $message, array $context = [] ): void {
        if ( ( self::LEVELS[ $level ] ?? 1 ) < ( self::LEVELS[ $this->min_level ] ?? 1 ) ) {
            return;
        }

        $prefix  = strtoupper( $level );
        $plugin  = 'UltraGuard Pro';
        $context = empty( $context ) ? '' : ' ' . wp_json_encode( $context );

        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Centralized plugin logger intentionally writes to the PHP error log.
        error_log( "[{$plugin}][{$prefix}] {$message}{$context}" );

        // Optionally persist to DB (best-effort; silently ignored if table missing)
        $this->maybe_write_to_db( $level, $message, $context );
    }

    private function maybe_write_to_db( string $level, string $message, string $context ): void {
        if ( self::$db_write_in_progress || ! did_action( 'plugins_loaded' ) ) {
            return;
        }

        // Verify table exists before attempting an insert.
        $table = DatabaseSchema::get_table_name( 'security_logs' );
        if ( ! $table ) { return; }

        self::$db_write_in_progress = true;

        try {
            // $internal = true bypasses the manage_options capability check.
            // Logger is a trusted infrastructure class that must write events from
            // ANY context — public pages (firewall block), cron, admin, login page.
            DatabaseCrud::insert( 'ultraguard_security_logs', [
                'event_type'  => 'plugin_log',
                'severity'    => $level,
                'message'     => substr( $message, 0, 65535 ),
                'ip_address'  => Utils::get_client_ip(),
                'context'     => $context ?: null,
                'created_at'  => current_time( 'mysql', true ),
            ], null, true ); // $internal = true: no cap check needed for logging
        } catch ( \Throwable $e ) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Last-resort logger fallback must not recurse through the DB layer.
            error_log( '[UltraGuard Pro][ERROR] Logger DB write skipped: ' . $e->getMessage() );
        } finally {
            self::$db_write_in_progress = false;
        }
    }
}
