<?php

/**
 * User and customer audit hooks.
 *
 * @package ArraySubs\Features\ProfileFields\Services
 */

namespace ArraySubs\Features\ProfileFields\Services;

defined('ABSPATH') || exit;

/**
 * Audits user and customer profile changes.
 */
class AuditHooks
{
    /**
     * Enable auto-loading.
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Pending user-meta snapshots keyed by user+meta key.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $pending_user_meta_changes = [];

    /**
     * Register hooks.
     */
    public function __construct()
    {
        add_filter('update_user_metadata', [$this, 'captureUserMetaBeforeUpdate'], 10, 5);
        add_filter('add_user_metadata', [$this, 'captureUserMetaBeforeAdd'], 10, 4);

        add_action('updated_user_meta', [$this, 'logUpdatedUserMeta'], 10, 4);
        add_action('added_user_meta', [$this, 'logAddedUserMeta'], 10, 4);
        add_action('profile_update', [$this, 'logProfileUpdate'], 20, 3);
    }

    /**
     * Capture auditable user-meta values before an update.
     *
     * @param mixed  $check      Short-circuit value.
     * @param int    $user_id    User ID.
     * @param string $meta_key   Meta key.
     * @param mixed  $meta_value New value.
     * @param mixed  $prev_value Previous value filter constraint.
     * @return mixed
     */
    public function captureUserMetaBeforeUpdate($check, int $user_id, string $meta_key, $meta_value, $prev_value)
    {
        if (!$this->isAuditableUserMetaKey($meta_key)) {
            return $check;
        }

        $snapshot_key = $this->getSnapshotKey($user_id, $meta_key);

        $this->pending_user_meta_changes[$snapshot_key] = [
            'old'      => get_user_meta($user_id, $meta_key, true),
            'new'      => $meta_value,
            'actor_id' => get_current_user_id(),
            'meta_key' => $meta_key,
            'user_id'  => $user_id,
        ];

        return $check;
    }

    /**
     * Capture auditable user-meta values before a new insert.
     *
     * @param mixed  $check      Short-circuit value.
     * @param int    $user_id    User ID.
     * @param string $meta_key   Meta key.
     * @param mixed  $meta_value New value.
     * @return mixed
     */
    public function captureUserMetaBeforeAdd($check, int $user_id, string $meta_key, $meta_value)
    {
        if (!$this->isAuditableUserMetaKey($meta_key)) {
            return $check;
        }

        $snapshot_key = $this->getSnapshotKey($user_id, $meta_key);

        $this->pending_user_meta_changes[$snapshot_key] = [
            'old'      => '',
            'new'      => $meta_value,
            'actor_id' => get_current_user_id(),
            'meta_key' => $meta_key,
            'user_id'  => $user_id,
        ];

        return $check;
    }

    /**
     * Log updated auditable user meta.
     *
     * @param int    $meta_id    Meta ID.
     * @param int    $user_id    User ID.
     * @param string $meta_key   Meta key.
     * @param mixed  $meta_value Stored value.
     * @return void
     */
    public function logUpdatedUserMeta(int $meta_id, int $user_id, string $meta_key, $meta_value): void
    {
        $this->logAuditableUserMetaChange($user_id, $meta_key, $meta_value);
    }

    /**
     * Log added auditable user meta.
     *
     * @param int    $meta_id    Meta ID.
     * @param int    $user_id    User ID.
     * @param string $meta_key   Meta key.
     * @param mixed  $meta_value Stored value.
     * @return void
     */
    public function logAddedUserMeta(int $meta_id, int $user_id, string $meta_key, $meta_value): void
    {
        $this->logAuditableUserMetaChange($user_id, $meta_key, $meta_value);
    }

    /**
     * Log user-core field changes.
     *
     * @param int      $user_id        User ID.
     * @param \WP_User $old_user_data Previous user object.
     * @param array    $userdata       Raw update payload.
     * @return void
     */
    public function logProfileUpdate(int $user_id, \WP_User $old_user_data, array $userdata = []): void
    {
        if (!function_exists('arraysubs_add_global_note')) {
            return;
        }

        $user = get_userdata($user_id);
        if (!$user instanceof \WP_User) {
            return;
        }

        $field_labels = [
            'display_name' => __('Display name', 'arraysubs'),
            'user_email'   => __('Account email', 'arraysubs'),
            'user_url'     => __('Website', 'arraysubs'),
        ];

        $changes = [];
        $audit_changes = [];

        foreach ($field_labels as $field => $label) {
            $old_value = isset($old_user_data->{$field}) ? (string) $old_user_data->{$field} : '';
            $new_value = isset($user->{$field}) ? (string) $user->{$field} : '';

            if ($old_value === $new_value) {
                continue;
            }

            $changes[] = arraysubs_format_audit_change_sentence(
                $label,
                arraysubs_format_audit_value($old_value),
                arraysubs_format_audit_value($new_value)
            );

            $audit_changes[] = arraysubs_create_audit_change_entry(
                $label,
                arraysubs_format_audit_value($old_value),
                arraysubs_format_audit_value($new_value)
            );
        }

        if (empty($changes)) {
            return;
        }

        arraysubs_add_global_note(
            $this->buildMemberAuditMessage(
                $user_id,
                __('User data updated', 'arraysubs'),
                $changes
            ),
            'private',
            get_current_user_id() ?: 0,
            $audit_changes,
            'member'
        );
    }

    /**
     * Log one auditable user-meta change.
     *
     * @param int    $user_id    User ID.
     * @param string $meta_key   Meta key.
     * @param mixed  $meta_value Stored value.
     * @return void
     */
    private function logAuditableUserMetaChange(int $user_id, string $meta_key, $meta_value): void
    {
        if (!function_exists('arraysubs_add_global_note') || !$this->isAuditableUserMetaKey($meta_key)) {
            return;
        }

        $snapshot_key = $this->getSnapshotKey($user_id, $meta_key);
        $snapshot     = $this->pending_user_meta_changes[$snapshot_key] ?? null;

        unset($this->pending_user_meta_changes[$snapshot_key]);

        if (!is_array($snapshot)) {
            return;
        }

        $old_value = $this->normalizeAuditableUserMetaValue($meta_key, $snapshot['old'] ?? '');
        $new_value = $this->normalizeAuditableUserMetaValue($meta_key, $meta_value);

        if ($old_value === $new_value) {
            return;
        }

        $context = $meta_key === 'arraysubs_avatar' ? 'file' : 'text';
        $label   = $this->getAuditableUserMetaLabel($meta_key);
        $group   = str_starts_with($meta_key, 'billing_') || str_starts_with($meta_key, 'shipping_')
            ? __('Customer data updated', 'arraysubs')
            : __('Profile field changed', 'arraysubs');

        $previous_display = arraysubs_format_audit_value($old_value, $context);
        $changed_display  = arraysubs_format_audit_value($new_value, $context);

        arraysubs_add_global_note(
            $this->buildMemberAuditMessage(
                $user_id,
                $group,
                [
                    arraysubs_format_audit_change_sentence(
                        $label,
                        $previous_display,
                        $changed_display
                    ),
                ]
            ),
            'private',
            (int) ($snapshot['actor_id'] ?? 0),
            [
                arraysubs_create_audit_change_entry($label, $previous_display, $changed_display),
            ],
            'member'
        );
    }

    /**
     * Determine whether a meta key should be audited.
     *
     * @param string $meta_key Meta key.
     * @return bool
     */
    private function isAuditableUserMetaKey(string $meta_key): bool
    {
        $allowed_keys = [
            'billing_first_name',
            'billing_last_name',
            'billing_company',
            'billing_address_1',
            'billing_address_2',
            'billing_city',
            'billing_state',
            'billing_postcode',
            'billing_country',
            'billing_email',
            'billing_phone',
            'shipping_first_name',
            'shipping_last_name',
            'shipping_company',
            'shipping_address_1',
            'shipping_address_2',
            'shipping_city',
            'shipping_state',
            'shipping_postcode',
            'shipping_country',
            'shipping_phone',
            'arraysubs_avatar',
        ];

        return in_array($meta_key, $allowed_keys, true)
            || str_starts_with($meta_key, 'arraysubs_pf_');
    }

    /**
     * Resolve an audit label for a user-meta key.
     *
     * @param string $meta_key Meta key.
     * @return string
     */
    private function getAuditableUserMetaLabel(string $meta_key): string
    {
        $labels = [
            'billing_first_name' => __('Billing first name', 'arraysubs'),
            'billing_last_name' => __('Billing last name', 'arraysubs'),
            'billing_company' => __('Billing company', 'arraysubs'),
            'billing_address_1' => __('Billing address line 1', 'arraysubs'),
            'billing_address_2' => __('Billing address line 2', 'arraysubs'),
            'billing_city' => __('Billing city', 'arraysubs'),
            'billing_state' => __('Billing state', 'arraysubs'),
            'billing_postcode' => __('Billing postcode', 'arraysubs'),
            'billing_country' => __('Billing country', 'arraysubs'),
            'billing_email' => __('Billing email', 'arraysubs'),
            'billing_phone' => __('Billing phone', 'arraysubs'),
            'shipping_first_name' => __('Shipping first name', 'arraysubs'),
            'shipping_last_name' => __('Shipping last name', 'arraysubs'),
            'shipping_company' => __('Shipping company', 'arraysubs'),
            'shipping_address_1' => __('Shipping address line 1', 'arraysubs'),
            'shipping_address_2' => __('Shipping address line 2', 'arraysubs'),
            'shipping_city' => __('Shipping city', 'arraysubs'),
            'shipping_state' => __('Shipping state', 'arraysubs'),
            'shipping_postcode' => __('Shipping postcode', 'arraysubs'),
            'shipping_country' => __('Shipping country', 'arraysubs'),
            'shipping_phone' => __('Shipping phone', 'arraysubs'),
            'arraysubs_avatar' => __('Avatar file', 'arraysubs'),
        ];

        if (isset($labels[$meta_key])) {
            return $labels[$meta_key];
        }

        if (str_starts_with($meta_key, 'arraysubs_pf_')) {
            $field_key = substr($meta_key, strlen('arraysubs_pf_'));
            $fields    = arraysubs_get_enabled_profile_fields();

            foreach ($fields as $field) {
                if (($field['key'] ?? '') === $field_key) {
                    return sanitize_text_field((string) ($field['label'] ?? $field_key));
                }
            }

            return sprintf(
                /* translators: %s: profile field key. */
                __('Profile field %s', 'arraysubs'),
                sanitize_text_field($field_key)
            );
        }

        return sanitize_text_field(str_replace('_', ' ', $meta_key));
    }

    /**
     * Normalize a user-meta value for comparison.
     *
     * @param string $meta_key Meta key.
     * @param mixed  $value    Raw value.
     * @return string
     */
    private function normalizeAuditableUserMetaValue(string $meta_key, $value): string
    {
        if (is_array($value)) {
            $json = wp_json_encode($value);

            return is_string($json) ? $json : '';
        }

        if ($meta_key === 'billing_email') {
            return sanitize_email((string) $value);
        }

        if ($meta_key === 'arraysubs_avatar') {
            return sanitize_file_name((string) $value);
        }

        if ($value === '1' || $value === '0') {
            return $value === '1' ? __('Yes', 'arraysubs') : __('No', 'arraysubs');
        }

        return sanitize_text_field((string) $value);
    }

    /**
     * Build a consistent member audit note.
     *
     * @param int    $user_id User ID.
     * @param string $title   Short audit group label.
     * @param array  $changes List of change sentences.
     * @return string
     */
    private function buildMemberAuditMessage(int $user_id, string $title, array $changes): string
    {
        $content = sprintf(
            /* translators: 1: member ID, 2: audit title. */
            __('Member #%1$d %2$s:', 'arraysubs'),
            $user_id,
            $title
        );

        $content .= '<br>' . implode(
            '<br>',
            array_map(
                static fn(string $change): string => '• ' . esc_html($change),
                $changes
            )
        );

        return $content;
    }

    /**
     * Build the array key used for pending snapshots.
     *
     * @param int    $user_id  User ID.
     * @param string $meta_key Meta key.
     * @return string
     */
    private function getSnapshotKey(int $user_id, string $meta_key): string
    {
        return $user_id . ':' . $meta_key;
    }
}
