<?php

namespace Kanzu\StoreVoiceAI;

// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Lightweight rotating-file logger used by the plugin to record API
 * requests and responses for support diagnostics.
 *
 * Mirrors the small subset of the Monolog API the plugin used previously
 * (info / warning / error), so existing callers do not need to change.
 *
 * Log files are written to the directory passed into the constructor as
 * `storevoiceai-YYYY-MM-DD-<token>.log.php`. The oldest files beyond `max_files` are
 * pruned on each write so the directory never grows unbounded.
 */
class Logger {

    /** @var string Absolute path of the directory log files are written into. */
    private $log_dir;

    /** @var string Channel name written into each log line. */
    private $channel;

    /** @var int Number of dated log files to retain. */
    private $max_files;

    /** @var string Non-guessable suffix used in log filenames. */
    private $file_token;

    public function __construct( string $log_dir, string $channel = 'StoreVoiceAI', int $max_files = 7 ) {
        $this->log_dir    = trailingslashit( $log_dir );
        $this->channel    = $channel;
        $this->max_files  = max( 1, $max_files );
        $salt             = function_exists( 'wp_salt' ) ? wp_salt( 'auth' ) : __FILE__;
        $this->file_token = substr( hash( 'sha256', $salt . '|' . $this->log_dir ), 0, 16 );
    }

    public function info( string $message, array $context = [] ): void {
        $this->write( 'INFO', $message, $context );
    }

    public function warning( string $message, array $context = [] ): void {
        $this->write( 'WARNING', $message, $context );
    }

    public function error( string $message, array $context = [] ): void {
        $this->write( 'ERROR', $message, $context );
    }

    /**
     * Append a single line to today's log file and prune older files.
     * Logging never throws back to the caller.
     */
    private function write( string $level, string $message, array $context ): void {
        try {
            if ( ! is_dir( $this->log_dir ) ) {
                return;
            }

            $line = sprintf(
                "[%s] %s.%s: %s %s\n",
                gmdate( 'Y-m-d H:i:s' ),
                $this->channel,
                $level,
                $message,
                empty( $context ) ? '{}' : wp_json_encode( $this->redact_context( $context ) )
            );

            $file = $this->log_dir . 'storevoiceai-' . gmdate( 'Y-m-d' ) . '-' . $this->file_token . '.log.php';

            $old_umask = umask( 0137 );
            try {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Direct file write is required for high-frequency append-only logging; WP_Filesystem has no efficient append primitive.
                $written = file_put_contents( $file, ( file_exists( $file ) ? '' : "<?php exit; ?>\n" ) . $line, FILE_APPEND | LOCK_EX );
            } finally {
                umask( $old_umask );
            }
            if ( false === $written ) {
                return;
            }
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Log files must not be world-readable or writable.
            chmod( $file, 0640 );

            $this->prune();
        } catch ( \Throwable $e ) {
            // Logging must never break the caller.
        }
    }

    /**
     * Keep only the N most recent dated log files; remove the rest.
     * glob() returns alphabetical order, which for YYYY-MM-DD filenames
     * matches chronological order.
     */
    private function prune(): void {
        try {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_glob -- Logger directory is small and fully owned by the plugin; glob is the natural fit.
            $files = glob( $this->log_dir . 'storevoiceai-*.log.php' );
            if ( ! is_array( $files ) || count( $files ) <= $this->max_files ) {
                return;
            }
            $stale = array_slice( $files, 0, count( $files ) - $this->max_files );
            foreach ( $stale as $f ) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- See file_put_contents rationale; logger cleanup uses native filesystem.
                unlink( $f );
            }
        } catch ( \Throwable $e ) {
            // Pruning failures must not propagate.
        }
    }

    private function redact_context( array $context ): array {
        $sensitive_keys = array(
            'accessToken',
            'access_token',
            'apiKey',
            'api_key',
            'authorization',
            'consumerKey',
            'consumerSecret',
            'email',
            'emailAddress',
            'email_address',
            'externalConsumerKey',
            'externalConsumerSecret',
            'licenseKey',
            'notifyEmail',
            'notify_email',
            'ownerEmail',
            'owner_email',
            'phone',
            'phoneNumber',
            'phone_number',
            'secret',
            'secretKey',
            'token',
            'transcript',
            'transcriptText',
            'transferPhoneNumber',
            'userEmail',
            'user_email',
        );

        foreach ( $context as $key => $value ) {
            if ( in_array( (string) $key, $sensitive_keys, true ) ) {
                $context[ $key ] = '[redacted]';
                continue;
            }

            if ( is_array( $value ) ) {
                $context[ $key ] = $this->redact_context( $value );
            }
        }

        return $context;
    }
}
