<?php
/**
 * HtaccessChangeLogTable — Immutable audit trail for every .htaccess write.
 *
 * Columns:
 *   id            — auto-increment PK
 *   user_id       — WP user ID (0 = system)
 *   user_login    — snapshot of login name at write time
 *   change_reason — e.g. manual_save, apply_rules, import, reset, toggle_feature
 *   feature_key   — populated only for toggle_feature changes
 *   file_size     — byte size after the write
 *   content_hash  — SHA-256 of written content for integrity verification
 *   backup_path   — absolute path of pre-write backup (if taken)
 *   ip_address    — admin's IP at time of change
 *   created_at    — UTC datetime
 *
 * All method signatures that override BaseTable methods exactly match the
 * parent signatures to avoid PHP declaration incompatibility errors.
 *
 * @package UltraguardPro\modules\htaccess\db
 */

namespace UltraguardPro\modules\htaccess\db;

use UltraguardPro\db\BaseTable;

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

class HtaccessChangeLogTable extends BaseTable {

	protected $table_name  = 'ultraguard_htaccess_change_log';
	protected $cache_group = 'ultraguard_htaccess_change_log';

	// ── BaseTable contract ─────────────────────────────────────────────────────

	public function get_defaults() {
		return [
			'id'            => 0,
			'user_id'       => 0,
			'user_login'    => '',
			'change_reason' => 'manual_save',
			'feature_key'   => null,
			'file_size'     => 0,
			'content_hash'  => '',
			'backup_path'   => null,
			'ip_address'    => '',
			'created_at'    => '',
		];
	}

	/**
	 * @param mixed $data
	 * @return mixed  Validated data array, or false to abort.
	 */
	protected function validate_data( $data ) {
		if ( empty( $data['change_reason'] ) ) {
			$data['change_reason'] = 'manual_save';
		}

		$valid_reasons = [
			'manual_save', 'apply_rules', 'import', 'restore', 'reset',
			'toggle_feature', 'auto_backup', 'custom_rules_saved', 'optimize',
		];

		if (
			! in_array( $data['change_reason'], $valid_reasons, true ) &&
			! str_starts_with( (string) $data['change_reason'], 'before_' )
		) {
			$data['change_reason'] = 'manual_save';
		}

		return $data;
	}

	/**
	 * @param mixed $data
	 * @return array
	 */
	protected function sanitize_data( $data ) {
		$out = [];
		foreach ( (array) $data as $k => $v ) {
			$out[ $k ] = match ( $k ) {
				'user_id'       => absint( $v ),
				'file_size'     => absint( $v ),
				'backup_path',
				'feature_key'   => is_null( $v ) ? null : sanitize_text_field( (string) $v ),
				'content_hash'  => preg_replace( '/[^a-f0-9]/i', '', (string) $v ),
				'ip_address'    => sanitize_text_field( (string) $v ),
				'user_login'    => sanitize_user( (string) $v ),
				'change_reason' => sanitize_key( (string) $v ),
				default         => is_string( $v ) ? sanitize_text_field( $v ) : $v,
			};
		}
		return $out;
	}

	/**
	 * No return type — must match parent signature exactly.
	 */
	protected function get_searchable_fields() {
		return [ 'user_login', 'change_reason', 'feature_key', 'ip_address' ];
	}

	// ── Overrides: must match BaseTable signatures exactly ────────────────────

	/**
	 * Get recent rows ordered by created_at DESC.
	 * Signature matches BaseTable::get_recent(int, string, array, bool).
	 *
	 * @param int    $limit
	 * @param string $date_field
	 * @param array  $conditions
	 * @param bool   $use_cache
	 * @return array
	 */
	public function get_recent(
		int    $limit      = 50,
		string $date_field = 'created_at',
		array  $conditions = [],
		bool   $use_cache  = true
	): array {
		return $this->get_by( $conditions, $limit, $date_field, 'DESC' );
	}

	/**
	 * Get rows for a specific WP user.
	 * Signature matches BaseTable::get_by_user(int, int, string).
	 *
	 * @param int    $user_id
	 * @param int    $limit
	 * @param string $orderby
	 * @return array
	 */
	public function get_by_user( int $user_id, int $limit = 100, string $orderby = 'created_at' ): array {
		return $this->get_by( [ 'user_id' => $user_id ], $limit, $orderby, 'DESC' );
	}

	// ── Module-specific helpers ────────────────────────────────────────────────

	/**
	 * Record a .htaccess write event.
	 *
	 * Call this immediately after a successful file_put_contents() so the
	 * audit trail is always in sync with the actual file on disk.
	 *
	 * @param string      $reason       One of the valid change_reason values.
	 * @param string      $content      The content that was written.
	 * @param string|null $backup_path  Absolute path of the backup taken before writing, or null.
	 * @param string|null $feature_key  For toggle_feature changes: the feature slug.
	 * @return int|false  Inserted row ID, or false on failure.
	 */
	public function record(
		string  $reason,
		string  $content,
		?string $backup_path = null,
		?string $feature_key = null
	) {
		$current_user = wp_get_current_user();

		return $this->insert( [
			'user_id'       => $current_user->ID       ?? 0,
			'user_login'    => $current_user->user_login ?? '',
			'change_reason' => $reason,
			'feature_key'   => $feature_key,
			'file_size'     => strlen( $content ),
			'content_hash'  => hash( 'sha256', $content ),
			'backup_path'   => $backup_path,
			'ip_address'    => $this->get_client_ip(),
			'created_at'    => gmdate( 'Y-m-d H:i:s' ),
		] );
	}

	/**
	 * Count changes in the last N days.
	 *
	 * @param int $days
	 * @return int
	 */
	public function count_recent( int $days = 7 ): int {
		$since = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
		return $this->count( [ 'created_at >=' => $since ] );
	}

	/**
	 * Verify the current .htaccess file against the most recent log entry.
	 *
	 * @return array{match: bool, stored_hash: string, actual_hash: string}
	 */
	public function verify_integrity(): array {
		$path = ABSPATH . '.htaccess';
		if ( ! file_exists( $path ) ) {
			return [ 'match' => false, 'stored_hash' => '', 'actual_hash' => '' ];
		}

		$actual = hash( 'sha256', file_get_contents( $path ) );

		// get_recent() matches the BaseTable signature (limit, date_field, conditions, use_cache).
		$rows   = $this->get_recent( 1 );
		$stored = ! empty( $rows ) ? ( $rows[0]['content_hash'] ?? '' ) : '';

		return [
			'match'       => $stored !== '' && hash_equals( $stored, $actual ),
			'stored_hash' => $stored,
			'actual_hash' => $actual,
		];
	}

	// ── Private ────────────────────────────────────────────────────────────────

	private function get_client_ip(): string {
		$headers = [
			'HTTP_CF_CONNECTING_IP',
			'HTTP_X_REAL_IP',
			'HTTP_X_FORWARDED_FOR',
			'REMOTE_ADDR',
		];
        foreach ( $headers as $h ) {
            if ( ! empty( $_SERVER[ $h ] ) ) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- dynamic trusted header is unslashed, split, and validated as an IP immediately below.
                $ip = trim( explode( ',', wp_unslash( $_SERVER[ $h ] ) )[0] );
                if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                    return $ip;
                }
			}
		}
		return '0.0.0.0';
	}
}
