<?php
/**
 * Wrenio Logger Class
 *
 * File logger with rotation. ERROR and WARNING always write regardless of
 * the debug setting — INFO and DEBUG are gated on it.
 *
 * @package Wrenio_Media_Offload
 * @since 1.1.0
 */

namespace Wrenio_Media_Offload;

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

/**
 * Class Wrenio_Logger
 */
class Wrenio_Logger {

    /** Maximum log file size in bytes (1MB) */
    const MAX_LOG_SIZE = 1048576;

    /** Maximum rotated log files to keep */
    const MAX_LOG_FILES = 5;

    /** Levels that always log regardless of debug setting */
    const ALWAYS_LOG_LEVELS = array( 'ERROR', 'WARNING' );

    /** @var string */
    private $log_dir;

    /** @var string */
    private $log_file;

    /** @var bool */
    private $debug_enabled;

    /** @var self|null */
    private static $instance = null;

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

    /**
     * Constructor
     */
    private function __construct() {
        $upload_dir     = wp_upload_dir();
        $this->log_dir  = $upload_dir['basedir'] . '/wrenio-media-offload-logs';
        $this->log_file = $this->log_dir . '/debug.log';

        $this->ensure_log_directory();
        $this->reload_settings();
    }

    /**
     * Re-read the debug setting. Call this after settings are updated.
     */
    public function reload_settings() {
        $settings            = get_option( 'wrenio_media_offload_settings', array() );
        $this->debug_enabled = ! empty( $settings['enable_debug_logging'] );
    }

    /**
     * Ensure the log directory exists and is protected against direct access.
     *
     * Protection files (.htaccess, index.php, index.html) are created on a
     * one-time basis via a persistent option gate, not on every request.
     * Each file is created independently so upgrades from earlier versions
     * receive any missing protection files.
     */
    private function ensure_log_directory() {
        if ( ! file_exists( $this->log_dir ) ) {
            wp_mkdir_p( $this->log_dir );
        }

        // Gate protection-file creation behind a persistent option
        $protection_version = get_option( 'wrenio_media_offload_log_protection_version', '0' );
        if ( version_compare( $protection_version, '4.0.0', '>=' ) ) {
            return;
        }

        global $wp_filesystem;
        if ( ! $wp_filesystem ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }

        $protection_files = array(
            '.htaccess'  => "Deny from all\n",
            'index.php'  => "<?php\n// Silence is golden.\n",
            'index.html' => '',
            'web.config' => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <system.webServer>\n    <security>\n      <authorization>\n        <remove users=\"*\" roles=\"\" verbs=\"\" />\n        <add accessType=\"Deny\" users=\"*\" />\n      </authorization>\n    </security>\n  </system.webServer>\n</configuration>\n",
        );

        foreach ( $protection_files as $filename => $contents ) {
            $path = $this->log_dir . '/' . $filename;
            if ( file_exists( $path ) ) {
                continue;
            }
            if ( $wp_filesystem ) {
                $wp_filesystem->put_contents( $path, $contents, FS_CHMOD_FILE );
            } else {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
                @file_put_contents( $path, $contents );
            }
        }

        update_option( 'wrenio_media_offload_log_protection_version', '4.0.0' );
    }

    /**
     * Log a message.
     *
     * @param string $message Message to log
     * @param string $level   Log level (INFO, ERROR, WARNING, DEBUG)
     */
    public function log( $message, $level = 'INFO' ) {
        $level = strtoupper( $level );

        // ERROR and WARNING always log. INFO/DEBUG gated on debug setting.
        if ( ! $this->debug_enabled && ! in_array( $level, self::ALWAYS_LOG_LEVELS, true ) ) {
            return;
        }

        $this->rotate_if_needed();

        $timestamp = current_time( 'Y-m-d H:i:s' );
        $formatted = sprintf( "[%s] [%s] %s\n", $timestamp, $level, $message );

        // error_log with mode 3 writes to the specified file (not system log)
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
        @error_log( $formatted, 3, $this->log_file );
    }

    public function error( $message )   { $this->log( $message, 'ERROR' ); }
    public function warning( $message ) { $this->log( $message, 'WARNING' ); }
    public function info( $message )    { $this->log( $message, 'INFO' ); }
    public function debug( $message )   { $this->log( $message, 'DEBUG' ); }

    /**
     * Rotate log file if it exceeds size limit.
     */
    private function rotate_if_needed() {
        if ( ! file_exists( $this->log_file ) ) {
            return;
        }

        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        $size = @filesize( $this->log_file );
        if ( $size === false || $size < self::MAX_LOG_SIZE ) {
            return;
        }

        $this->rotate_logs();
    }

    /**
     * Rotate log files.
     */
    private function rotate_logs() {
        global $wp_filesystem;
        if ( ! $wp_filesystem ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }

        // Delete oldest log
        $oldest = $this->log_file . '.' . self::MAX_LOG_FILES;
        if ( file_exists( $oldest ) ) {
            wp_delete_file( $oldest );
        }

        // Rotate existing logs: .1 -> .2, .2 -> .3, etc.
        for ( $i = self::MAX_LOG_FILES - 1; $i >= 1; $i-- ) {
            $old = $this->log_file . '.' . $i;
            $new = $this->log_file . '.' . ( $i + 1 );
            if ( file_exists( $old ) ) {
                if ( $wp_filesystem ) {
                    $wp_filesystem->move( $old, $new );
                } else {
                    // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
                    @rename( $old, $new );
                }
            }
        }

        // Move current log to .1
        if ( file_exists( $this->log_file ) ) {
            if ( $wp_filesystem ) {
                $wp_filesystem->move( $this->log_file, $this->log_file . '.1' );
            } else {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
                @rename( $this->log_file, $this->log_file . '.1' );
            }
        }
    }

    /**
     * Clear all log files.
     */
    public function clear_logs() {
        if ( file_exists( $this->log_file ) ) {
            wp_delete_file( $this->log_file );
        }

        for ( $i = 1; $i <= self::MAX_LOG_FILES; $i++ ) {
            $file = $this->log_file . '.' . $i;
            if ( file_exists( $file ) ) {
                wp_delete_file( $file );
            }
        }
    }

    /**
     * Get current log file content.
     *
     * @param int $lines Number of lines to retrieve (0 = all)
     * @return string
     */
    public function get_log_content( $lines = 100 ) {
        if ( ! file_exists( $this->log_file ) ) {
            return '';
        }

        if ( $lines <= 0 ) {
            // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
            $content = @file_get_contents( $this->log_file );
            return $content !== false ? $content : '';
        }

        // Try tail-style read
        try {
            $file = new \SplFileObject( $this->log_file, 'r' );
            $file->seek( PHP_INT_MAX );
            $total_lines = $file->key();

            $start   = max( 0, $total_lines - $lines );
            $content = '';

            $file->seek( $start );
            while ( ! $file->eof() ) {
                $line = $file->current();
                if ( $line !== false ) {
                    $content .= $line;
                }
                $file->next();
            }

            return $content;
        } catch ( \Throwable $e ) {
            // Properly namespaced catch — falls back to simple read on any error
            // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
            $content = @file_get_contents( $this->log_file );
            return $content !== false ? $content : '';
        }
    }

    /**
     * Get log file size in bytes.
     */
    public function get_log_size() {
        if ( ! file_exists( $this->log_file ) ) {
            return 0;
        }
        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        $size = @filesize( $this->log_file );
        return $size !== false ? $size : 0;
    }

    /**
     * Get formatted log size.
     */
    public function get_log_size_formatted() {
        $size = $this->get_log_size();
        if ( $size < 1024 ) {
            return $size . ' B';
        } elseif ( $size < 1048576 ) {
            return round( $size / 1024, 2 ) . ' KB';
        }
        return round( $size / 1048576, 2 ) . ' MB';
    }
}
