<?php

declare(strict_types=1);

namespace SiteFort\Modules\AuditLog;

use SiteFort\Includes\DataDirectoryProtection;
use SiteFort\Includes\GuardedDataFile;

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

/**
 * File-based audit logging.
 *
 * Writes audit events to a CSV file under /wp-content/sitefort-data/logs/.
 * The filename includes a random token to prevent URL enumeration even if
 * .htaccess is bypassed. Rotation and time-based cleanup are handled here.
 *
 */
class AuditFileLogger {

    const LOG_DIR_NAME    = 'logs';
    const MAX_LOG_SIZE    = 10485760; // 10 MB
    const MAX_ROTATED     = 5;
    const CSV_HEADER      = 'timestamp,event,severity,category,user_id,username,ip_address,details';
    const TOKEN_OPTION    = 'sitefort_audit_log_file_token';
    const FILE_SUFFIX     = '.csv.php';
    const LEGACY_SUFFIX   = '.csv';

    /** @var string|null Cached log file path for the current request. */
    private static ?string $log_path = null;

    /**
     * Absolute path to the logs directory.
     */
    public static function get_log_dir(): string {
        return WP_CONTENT_DIR . '/sitefort-data/' . self::LOG_DIR_NAME;
    }

    /**
     * Ensure the logs directory exists with web-access protection.
     */
    public static function ensure_directory(): bool {
        $dir = self::get_log_dir();

        return DataDirectoryProtection::ensure_local_protection( $dir );
    }

    /**
     * Get the current log file path.
     *
     * Uses a randomised 16-char alphanumeric token stored in a WP option so
     * the filename cannot be guessed from outside.
     */
    public static function get_log_path(): string {
        if ( null !== self::$log_path ) {
            return self::$log_path;
        }

        $token = get_option( self::TOKEN_OPTION, '' );
        if ( empty( $token ) || ! is_string( $token ) ) {
            $token = wp_generate_password( 16, false, false );
            update_option( self::TOKEN_OPTION, $token, false );
        }

        $guarded_path = self::get_log_dir() . '/sitefort-audit-' . $token . self::FILE_SUFFIX;
        GuardedDataFile::migrate_legacy_plaintext(
            self::get_log_dir() . '/sitefort-audit-' . $token . self::LEGACY_SUFFIX,
            $guarded_path
        );

        self::$log_path = $guarded_path;
        return self::$log_path;
    }

    /**
     * Write a single audit log entry as a CSV line.
     *
     * @return bool True on success.
     */
    public static function write(
        string $event,
        array  $details,
        string $severity,
        string $category,
        int    $user_id,
        string $username,
        string $ip
    ): bool {
        if ( ! self::ensure_directory() ) {
            return false;
        }

        $log_path = self::get_log_path();

        // Rotate if the current file exceeds the size limit.
        if ( @file_exists( $log_path ) ) {
            $size = @filesize( $log_path );
            if ( false !== $size && $size > self::MAX_LOG_SIZE ) {
                self::rotate( $log_path );
            }
        }

        // Prepare the CSV line.
        $payload_size = 0;
        if ( @file_exists( $log_path ) ) {
            $file_size = @filesize( $log_path );
            if ( false !== $file_size ) {
                $payload_size = max( 0, (int) $file_size - GuardedDataFile::payload_offset_for_path( $log_path ) );
            }
        }
        $is_new = $payload_size === 0;

        $details_json = wp_json_encode( $details, JSON_UNESCAPED_SLASHES );

        $fields = [
            gmdate( 'Y-m-d\TH:i:s\Z' ),
            $event,
            $severity,
            $category,
            (string) $user_id,
            mb_substr( $username, 0, 255, 'UTF-8' ),
            mb_substr( $ip, 0, 45, 'UTF-8' ),
            mb_substr( is_string( $details_json ) ? $details_json : '', 0, 4096, 'UTF-8' ),
        ];

        $line = self::csv_encode( $fields );

        if ( $is_new ) {
            $line = self::CSV_HEADER . PHP_EOL . $line;
        }

        return GuardedDataFile::append_payload( $log_path, $line . PHP_EOL );
    }

    /**
     * Delete rotated log files older than the given number of days.
     * Called by the daily audit-log cleanup cron.
     */
    public static function cleanup_old_files( int $days ): void {
        if ( $days < 1 ) {
            return;
        }

        $log_path = self::get_log_path();
        $cutoff   = time() - ( $days * DAY_IN_SECONDS );

        // Enforce retention in the active file too; rotation-only cleanup
        // leaves stale rows behind when traffic is steady.
        self::trim_active_file( $log_path, $cutoff );

        for ( $i = 1; $i <= self::MAX_ROTATED; $i++ ) {
            $rotated = GuardedDataFile::rotated_path( $log_path, '.' . $i );
            if ( @file_exists( $rotated ) ) {
                $mtime = @filemtime( $rotated );
                if ( false !== $mtime && $mtime < $cutoff ) {
                    wp_delete_file( $rotated );
                }
            }
        }
    }

    /**
     * Delete ALL log files (main + rotated) and remove the token option.
     * Used by "Clear All Logs" and plugin uninstall.
     */
    public static function purge_all(): void {
        $log_path = self::get_log_path();
        $legacy_path = str_replace( self::FILE_SUFFIX, self::LEGACY_SUFFIX, $log_path );

        wp_delete_file( $log_path );
        wp_delete_file( $legacy_path );
        for ( $i = 1; $i <= self::MAX_ROTATED; $i++ ) {
            wp_delete_file( GuardedDataFile::rotated_path( $log_path, '.' . $i ) );
        }

        delete_option( self::TOKEN_OPTION );
        self::$log_path = null;
    }

    // -----------------------------------------------------------------
    // Internal helpers
    // -----------------------------------------------------------------

    /**
     * CSV-encode a single row with proper escaping.
     */
    private static function csv_encode( array $fields ): string {
        $encoded = [];
        foreach ( $fields as $field ) {
            $field = (string) $field;
            if ( preg_match( '/^\s*[=+\-@]/', $field ) ) {
                $field = "'" . $field;
            }
            if ( false !== strpos( $field, ',' )
                 || false !== strpos( $field, '"' )
                 || false !== strpos( $field, "\n" )
                 || false !== strpos( $field, "\r" ) ) {
                $field = '"' . str_replace( '"', '""', $field ) . '"';
            }
            $encoded[] = $field;
        }
        return implode( ',', $encoded );
    }

    /**
     * Rotate log files following the ScanLogger pattern.
     *
     * Shifts existing rotated files upward (.1→.2, .2→.3, etc.),
     * deletes the oldest if at the cap, and starts a fresh main file.
     */
    private static function rotate( string $log_path ): void {
        for ( $i = self::MAX_ROTATED; $i >= 1; $i-- ) {
            $rotated = GuardedDataFile::rotated_path( $log_path, '.' . $i );
            if ( ! @file_exists( $rotated ) ) {
                continue;
            }
            if ( $i === self::MAX_ROTATED ) {
                wp_delete_file( $rotated );
            } else {
                self::move_file( $rotated, GuardedDataFile::rotated_path( $log_path, '.' . ( $i + 1 ) ) );
            }
        }

        $renamed = self::move_file( $log_path, GuardedDataFile::rotated_path( $log_path, '.1' ) );
        if ( $renamed ) {
            // Start fresh; header will be re-added on the next write.
            GuardedDataFile::clear_payload( $log_path );
            // Flush PHP's stat cache so the subsequent filesize() check sees the new empty file.
            clearstatcache( true, $log_path );
        }
    }

    /**
     * Remove rows older than the retention cutoff from the active CSV file.
     */
    private static function trim_active_file( string $log_path, int $cutoff ): void {
        if ( ! @file_exists( $log_path ) ) {
            return;
        }

        $contents = GuardedDataFile::read_payload( $log_path );
        if ( false === $contents || '' === $contents ) {
            return;
        }

        $kept_rows = [];
        $lines     = preg_split( "/\r\n|\n|\r/", $contents );

        foreach ( $lines as $index => $line ) {
            if ( ! is_string( $line ) || '' === trim( $line ) ) {
                continue;
            }

            $fields = str_getcsv( $line );
            if ( ! is_array( $fields ) || [] === $fields ) {
                continue;
            }

            if ( 0 === $index ) {
                $first = trim( (string) ( $fields[0] ?? '' ) );
                $second = trim( (string) ( $fields[1] ?? '' ) );
                if ( 'timestamp' === strtolower( $first ) && 'event' === strtolower( $second ) ) {
                    continue; // Skip existing header row; we write canonical header below.
                }
            }

            $timestamp_raw = trim( (string) ( $fields[0] ?? '' ) );
            if ( '' === $timestamp_raw ) {
                continue;
            }

            $timestamp = self::parse_utc_timestamp( $timestamp_raw );
            if ( false === $timestamp || $timestamp >= $cutoff ) {
                $kept_rows[] = $fields;
            }
        }

        $rebuilt = self::CSV_HEADER . PHP_EOL;
        foreach ( $kept_rows as $row ) {
            $row = array_map(
                static function ( $v ): string {
                    return is_scalar( $v ) ? (string) $v : wp_json_encode( $v );
                },
                $row
            );
            $rebuilt .= self::csv_encode( $row ) . PHP_EOL;
        }

        GuardedDataFile::write_payload_atomic( $log_path, $rebuilt );
    }

    /**
     * Move a rotated log without using raw rename().
     */
    private static function move_file( string $source, string $destination ): bool {
        if ( ! @file_exists( $source ) ) {
            return false;
        }

        if ( @copy( $source, $destination ) ) {
            wp_delete_file( $source );
            return ! @file_exists( $source );
        }
        return false;
    }

    /** @return int|false */
    private static function parse_utc_timestamp( string $timestamp ) {
        $timestamp = trim( $timestamp );
        if ( '' === $timestamp ) {
            return false;
        }

        if ( ctype_digit( $timestamp ) ) {
            return (int) $timestamp;
        }

        $normalized = (string) preg_replace( '/\s+/', 'T', $timestamp, 1 );
        $normalized = (string) preg_replace( '/\s+(Z|[+-]\d{2}:?\d{2})$/i', '$1', $normalized );
        if ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/', $normalized ) ) {
            $normalized .= 'Z';
        } elseif ( ! preg_match( '/(?:Z|[+-]\d{2}:?\d{2})$/i', $normalized ) ) {
            return false;
        }

        return strtotime( $normalized );
    }
}
