<?php
/**
 * Post meta synchronization helpers.
 *
 * This file is responsible for keeping post meta values aligned across
 * translation siblings when a post is saved. Only custom fields configured
 * with behavior `copy` are actively synchronized here.
 *
 * What this file does:
 * - watches post save events,
 * - finds translation siblings for the edited post,
 * - mirrors `copy` meta keys from the edited post to those siblings.
 *
 * What this file does NOT do:
 * - it does not translate values (that is handled by translation payload flows),
 * - it does not sync taxonomy term meta (`wpr_original_term_id`, etc.),
 * - it does not apply `copy-once` logic during save; only `copy` is enforced here.
 *
 * High-level flow:
 * - listen to `save_post`,
 * - validate request context,
 * - resolve translation siblings (meta-first, TRID fallback),
 * - copy selected meta from edited post to sibling posts.
 *
 * @package WPR_Translate
 */

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

/**
 * Respond to post save events and trigger translation meta synchronization.
 *
 * This is the entry point attached to `save_post`.
 * It performs request safety checks and delegates the actual synchronization
 * to `wpr_sync_meta_fields()`.
 *
 * Why these checks matter:
 * - autosaves/revisions can fire often and should not overwrite translation meta,
 * - invalid post types or missing post objects should fail fast,
 * - a filter hook allows integrators to disable sync for specific workflows.
 *
 * @param int     $post_id Post identifier being saved.
 * @param WP_Post $post    Post object (may be missing in some save contexts).
 * @param bool    $update  Whether this is an existing post being updated.
 */
function wpr_translate_handle_save_post_meta_sync( $post_id, $post, $update ) {
    // Kept for signature compatibility with the `save_post` action callback.
    unset( $update );

    // Normalize the post ID for safe processing.
    $post_id = absint( $post_id );

    if ( ! $post_id ) {
        // No valid post ID means nothing to sync.
        return;
    }

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        // Autosave snapshots are transient and should never become sync sources.
        return;
    }

    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
        // Revisions/autosaves are not canonical content and would produce bad sync state.
        return;
    }

    if ( ! $post instanceof WP_Post ) {
        // Attempt to load the post if not provided.
        $post = get_post( $post_id );
    }

    if ( ! $post instanceof WP_Post ) {
        // Abort when the post cannot be loaded.
        return;
    }

    if ( 'auto-draft' === $post->post_status ) {
        // Ignore placeholder records created by editor bootstrapping.
        return;
    }

    $post_type = (string) $post->post_type;

    if ( '' === $post_type ) {
        // Invalid post type.
        return;
    }

    if ( ! post_type_exists( $post_type ) ) {
        // Defensive guard for malformed or deleted post-type registrations.
        return;
    }

    // Allow external code to disable sync for specific posts/post types.
    $should_sync = apply_filters( 'wpr_translate_should_sync_post_meta', true, $post_id, $post_type, $post );

    if ( ! $should_sync ) {
        // Allow filters to disable synchronization for this post.
        return;
    }

    // Sync meta fields based on configured behaviors.
    wpr_sync_meta_fields( $post_id, $post_type );
}

/**
 * Synchronize a single meta key when post meta is added/updated/deleted.
 *
 * This handler complements `save_post` synchronization for flows where code
 * writes meta directly via `update_post_meta()`/`delete_post_meta()` and never
 * saves the post object itself (for example booking availability updates).
 *
 * @param mixed  $meta_ids   Meta row ID(s) associated with the change event.
 * @param int    $object_id  Object (post) ID whose meta changed.
 * @param string $meta_key   Meta key that changed.
 * @param mixed  $meta_value Meta value payload supplied by the hook.
 */
function wpr_translate_handle_post_meta_change_sync( $meta_ids, $object_id, $meta_key, $meta_value ) {
    // Kept for hook signature compatibility.
    unset( $meta_ids, $meta_value );

    if ( wpr_translate_meta_sync_is_internal_write() ) {
        // Ignore meta changes caused by our own synchronization writes.
        return;
    }

    $post_id  = absint( $object_id );
    $meta_key = (string) $meta_key;

    if ( ! $post_id || '' === $meta_key ) {
        // Invalid event payload.
        return;
    }

    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
        // Skip autosaves/revisions.
        return;
    }

    $post = get_post( $post_id );

    if ( ! $post instanceof WP_Post ) {
        // Ignore non-post objects.
        return;
    }

    if ( 'auto-draft' === $post->post_status ) {
        // Ignore placeholder editor posts.
        return;
    }

    $post_type = (string) $post->post_type;

    if ( '' === $post_type || ! post_type_exists( $post_type ) ) {
        // Unsupported post type.
        return;
    }

    if ( ! wpr_translate_is_copy_meta_field( $meta_key ) ) {
        // Only `copy` fields are synchronized by this flow.
        return;
    }

    // Respect the same sync opt-out filter used by save_post synchronization.
    $should_sync = apply_filters( 'wpr_translate_should_sync_post_meta', true, $post_id, $post_type, $post );

    if ( ! $should_sync ) {
        return;
    }

    wpr_sync_single_meta_field( $post_id, $post_type, $meta_key );
}

/**
 * Determine whether a meta key is configured with `copy` behavior.
 *
 * @param string $meta_key Meta key to inspect.
 *
 * @return bool
 */
function wpr_translate_is_copy_meta_field( $meta_key ) {
    $meta_key = (string) $meta_key;

    if ( '' === $meta_key ) {
        return false;
    }

    $behaviors = wpr_translate_get_meta_field_behaviors();

    if ( empty( $behaviors ) || ! isset( $behaviors[ $meta_key ] ) ) {
        return false;
    }

    return 'copy' === (string) $behaviors[ $meta_key ];
}

/**
 * Enter an internal meta synchronization write scope.
 *
 * @return void
 */
function wpr_translate_meta_sync_enter_internal_write() {
    if ( ! isset( $GLOBALS['wpr_translate_meta_sync_write_depth'] ) ) {
        $GLOBALS['wpr_translate_meta_sync_write_depth'] = 0;
    }

    $GLOBALS['wpr_translate_meta_sync_write_depth'] = (int) $GLOBALS['wpr_translate_meta_sync_write_depth'] + 1;
}

/**
 * Leave an internal meta synchronization write scope.
 *
 * @return void
 */
function wpr_translate_meta_sync_exit_internal_write() {
    if ( ! isset( $GLOBALS['wpr_translate_meta_sync_write_depth'] ) ) {
        $GLOBALS['wpr_translate_meta_sync_write_depth'] = 0;
        return;
    }

    $depth = (int) $GLOBALS['wpr_translate_meta_sync_write_depth'] - 1;

    if ( $depth < 0 ) {
        $depth = 0;
    }

    $GLOBALS['wpr_translate_meta_sync_write_depth'] = $depth;
}

/**
 * Check whether the current request is writing meta via sync internals.
 *
 * @return bool
 */
function wpr_translate_meta_sync_is_internal_write() {
    return ! empty( $GLOBALS['wpr_translate_meta_sync_write_depth'] );
}

/**
 * Synchronize one meta key from the edited post to translation siblings.
 *
 * @param int    $post_id   Source post ID.
 * @param string $post_type Source post type.
 * @param string $meta_key  Meta key to propagate.
 */
function wpr_sync_single_meta_field( $post_id, $post_type, $meta_key ) {
    static $in_progress = array();

    $post_id   = absint( $post_id );
    $post_type = (string) $post_type;
    $meta_key  = (string) $meta_key;

    if ( ! $post_id || '' === $post_type || '' === $meta_key ) {
        return;
    }

    $guard_key = $post_type . ':' . $post_id . ':' . $meta_key;

    if ( isset( $in_progress[ $guard_key ] ) ) {
        // Prevent same-key recursion on the same source post.
        return;
    }

    $in_progress[ $guard_key ] = true;

    $translation_post_ids = wpr_translate_resolve_meta_sync_translation_post_ids( $post_id, $post_type );

    if ( count( $translation_post_ids ) < 2 ) {
        unset( $in_progress[ $guard_key ] );
        return;
    }

    $source_post_id = $post_id;

    if ( function_exists( 'wpr_translation_is_translation_post_available' ) && ! wpr_translation_is_translation_post_available( $source_post_id ) ) {
        unset( $in_progress[ $guard_key ] );
        return;
    }

    $source_values      = get_post_meta( $source_post_id, $meta_key, false );
    $normalized_source  = array_map( 'maybe_serialize', array_map( 'maybe_unserialize', (array) $source_values ) );

    foreach ( $translation_post_ids as $translated_post_id ) {
        $translated_post_id = absint( $translated_post_id );

        if ( ! $translated_post_id || $translated_post_id === $source_post_id ) {
            continue;
        }

        if ( function_exists( 'wpr_translation_is_translation_post_available' ) && ! wpr_translation_is_translation_post_available( $translated_post_id ) ) {
            continue;
        }

        $target_values     = get_post_meta( $translated_post_id, $meta_key, false );
        $normalized_target = array_map( 'maybe_serialize', array_map( 'maybe_unserialize', (array) $target_values ) );

        if ( $normalized_source === $normalized_target ) {
            continue;
        }

        wpr_translate_meta_sync_enter_internal_write();
        delete_post_meta( $translated_post_id, $meta_key );

        if ( ! empty( $source_values ) ) {
            foreach ( $source_values as $value ) {
                add_post_meta( $translated_post_id, $meta_key, maybe_unserialize( $value ) );
            }
        }
        wpr_translate_meta_sync_exit_internal_write();
    }

    unset( $in_progress[ $guard_key ] );
}

/**
 * Synchronize `copy` configured meta fields across translations for a post.
 *
 * Important behavior:
 * - the currently edited post is treated as source of truth,
 * - translation siblings are resolved using post meta first,
 * - TRID lookup is used only as a fallback,
 * - only fields with behavior `copy` are propagated.
 *
 * Example:
 * - FR property is edited and `_price` is marked `copy`,
 * - this function reads `_price` from FR post,
 * - then writes that same value to EN/RO sibling posts in the same translation set.
 *
 * @param int    $post_id   Post identifier that triggered the save event.
 * @param string $post_type Post type slug.
 */
function wpr_sync_meta_fields( $post_id, $post_type ) {
    // Per-request re-entry guard keyed by `post_type:post_id`.
    // Why: writing meta inside this function can trigger additional hooks in the same
    // request; without this lock, the same post could re-enter sync and loop.
    static $in_progress = array();

    // Step 1: validate inputs and guard against recursive save/update loops.
    // Normalize inputs.
    $post_id   = absint( $post_id );
    $post_type = (string) $post_type;

    if ( ! $post_id || '' === $post_type ) {
        // Invalid inputs mean no sync.
        return;
    }

    // Build a unique lock key for this post/type sync scope (example: estate_property:38650).
    $guard_key = $post_type . ':' . $post_id;

    if ( isset( $in_progress[ $guard_key ] ) ) {
        // The same post sync is already running in this request; avoid infinite loops.
        return;
    }

    $in_progress[ $guard_key ] = true;

    // Step 2: load configured custom-field behaviors and keep only `copy` keys.
    // This function is intentionally strict: `translate`, `copy-once`, and `ignore`
    // are excluded to avoid conflicting semantics during post-save synchronization.
    $behaviors = wpr_translate_get_meta_field_behaviors();

    if ( empty( $behaviors ) ) {
        // Nothing configured to sync.
        unset( $in_progress[ $guard_key ] );
        return;
    }

    $copy_keys = array();

    foreach ( $behaviors as $meta_key => $behavior ) {
        // Ignore non-copy behaviors in this synchronizer.
        if ( 'copy' !== $behavior ) {
            continue;
        }

        $meta_key = (string) $meta_key;

        if ( '' === $meta_key ) {
            // Skip invalid meta keys.
            continue;
        }

        $copy_keys[] = $meta_key;
    }

    // Remove duplicates in case multiple rules resolve to the same meta key.
    $copy_keys = array_values( array_unique( $copy_keys ) );

    if ( empty( $copy_keys ) ) {
        // No meta keys require copy behavior.
        unset( $in_progress[ $guard_key ] );
        return;
    }

    // Step 3: resolve the translation post set using post-meta links first,
    // then fall back to TRID group lookups only when needed.
    // Meta-first avoids relying solely on language engine state when direct linkage
    // (`wpr_translated_original_post_id`) already provides the translation family.
    $translation_post_ids = wpr_translate_resolve_meta_sync_translation_post_ids( $post_id, $post_type );

    if ( count( $translation_post_ids ) < 2 ) {
        // Nothing to sync when there are no sibling translations.
        unset( $in_progress[ $guard_key ] );
        return;
    }

    // Step 4: the edited post is the source of truth for this sync run.
    // This allows edits in any language (FR/RO/EN) to propagate outward.
    $source_post_id = $post_id;

    if ( function_exists( 'wpr_translation_is_translation_post_available' ) && ! wpr_translation_is_translation_post_available( $source_post_id ) ) {
        // Abort when source post cannot be read as a translation element.
        unset( $in_progress[ $guard_key ] );
        return;
    }

    // Cache source values once per key to avoid re-querying for every target post.
    // `get_post_meta(..., false)` returns all rows for multi-value keys.
    $source_meta = array();

    // Step 5: snapshot all source values for each copy-configured meta key.
    foreach ( $copy_keys as $meta_key ) {
        // Load all values for the source post meta key.
        $source_meta[ $meta_key ] = get_post_meta( $source_post_id, $meta_key, false );
    }

    // Step 6: push source values into every sibling post in the translation set.
    foreach ( $translation_post_ids as $translated_post_id ) {
        $translated_post_id = absint( $translated_post_id );

        if ( ! $translated_post_id || $translated_post_id === $source_post_id ) {
            // Skip the source post or invalid translations.
            continue;
        }

        if ( function_exists( 'wpr_translation_is_translation_post_available' ) && ! wpr_translation_is_translation_post_available( $translated_post_id ) ) {
            // Skip unavailable translated posts.
            continue;
        }

        foreach ( $copy_keys as $meta_key ) {
            $source_values = isset( $source_meta[ $meta_key ] ) ? $source_meta[ $meta_key ] : array();
            $target_values = get_post_meta( $translated_post_id, $meta_key, false );

            // Compare normalized values to avoid false diffs from serialization shape.
            // We normalize both sides through maybe_unserialize + maybe_serialize so
            // semantically identical values are not rewritten unnecessarily.
            $normalized_source = array_map( 'maybe_serialize', array_map( 'maybe_unserialize', (array) $source_values ) );
            $normalized_target = array_map( 'maybe_serialize', array_map( 'maybe_unserialize', (array) $target_values ) );

            if ( $normalized_source === $normalized_target ) {
                // No changes required when values match.
                continue;
            }

            // Replace all target values with the source values.
            // Full replace model: clear all existing values for this meta key first.
            // This avoids stale extra rows remaining on target posts.
            wpr_translate_meta_sync_enter_internal_write();
            delete_post_meta( $translated_post_id, $meta_key );

            if ( ! empty( $source_values ) ) {
                // Recreate the full meta key set in source order.
                // Order can matter for some repeated meta consumers.
                foreach ( $source_values as $value ) {
                    // Copy each source value to the translated post.
                    add_post_meta( $translated_post_id, $meta_key, maybe_unserialize( $value ) );
                }
            }
            wpr_translate_meta_sync_exit_internal_write();
        }
    }

    // Release the guard key now that syncing is complete.
    unset( $in_progress[ $guard_key ] );
}

/**
 * Resolve translation-related post IDs for post-meta synchronization.
 *
 * Resolution strategy:
 * 1. Use post meta linkage (`wpr_translated_original_post_id`) first.
 * 2. Fall back to TRID translation group lookups only when meta linkage is incomplete.
 *
 * Why this order:
 * - meta linkage is direct and cheap,
 * - TRID state may be unavailable or lag behind in some import/edit flows,
 * - combining both improves resilience.
 *
 * @param int    $post_id   Current post ID that triggered sync.
 * @param string $post_type Current post type slug.
 *
 * @return array<int,int> Unique post IDs participating in sync.
 */
function wpr_translate_resolve_meta_sync_translation_post_ids( $post_id, $post_type ) {
    $post_id   = absint( $post_id );
    $post_type = sanitize_key( (string) $post_type );

    if ( $post_id <= 0 || '' === $post_type ) {
        return array();
    }

    // Meta-first: resolve the canonical/original post anchor.
    // For translation posts this points to the source/original post.
    // For source posts the meta may be absent, so we fallback to current post ID.
    $original_post_id = absint( get_post_meta( $post_id, 'wpr_translated_original_post_id', true ) );

    if ( $original_post_id <= 0 ) {
        $original_post_id = $post_id;
    }

    // Always include the current post and resolved original anchor.
    $meta_linked_post_ids = array( $post_id, $original_post_id );
    // Collect posts that explicitly reference the same original post through meta.
    // This is the primary family resolution path.
    $linked_posts         = get_posts(
        array(
            'post_type'        => $post_type,
            'post_status'      => 'any',
            'posts_per_page'   => -1,
            'fields'           => 'ids',
            'orderby'          => 'ID',
            'order'            => 'ASC',
            // Keep this query raw and independent from language/query filters.
            // We need physical siblings, not query-layer filtered views.
            'suppress_filters' => true,
            'meta_query'       => array(
                array(
                    'key'     => 'wpr_translated_original_post_id',
                    'value'   => (string) $original_post_id,
                    'compare' => '=',
                ),
            ),
        )
    );

    if ( is_array( $linked_posts ) ) {
        foreach ( $linked_posts as $linked_post_id ) {
            $linked_post_id = absint( $linked_post_id );

            if ( $linked_post_id > 0 ) {
                $meta_linked_post_ids[] = $linked_post_id;
            }
        }
    }

    $meta_linked_post_ids = array_values( array_unique( array_filter( array_map( 'absint', $meta_linked_post_ids ) ) ) );

    if ( count( $meta_linked_post_ids ) > 1 ) {
        // Meta linkage is sufficient; no fallback needed.
        return $meta_linked_post_ids;
    }

    // Fallback: resolve via TRID translation group helpers.
    if ( ! function_exists( 'wpr_translation_get_element_type' ) || ! function_exists( 'wpr_translation_get_translation_group' ) ) {
        return $meta_linked_post_ids;
    }

    $element_type = wpr_translation_get_element_type( $post_type );
    $group        = wpr_translation_get_translation_group( $post_id, $element_type );

    if ( empty( $group['translations'] ) || ! is_array( $group['translations'] ) ) {
        return $meta_linked_post_ids;
    }

    // Start from meta-linked IDs and append any IDs returned by TRID fallback.
    $trid_post_ids = $meta_linked_post_ids;

    foreach ( $group['translations'] as $group_post_id ) {
        $group_post_id = absint( $group_post_id );

        if ( $group_post_id > 0 ) {
            $trid_post_ids[] = $group_post_id;
        }
    }

    return array_values( array_unique( array_filter( array_map( 'absint', $trid_post_ids ) ) ) );
}

/**
 * Retrieve the configured behaviors for post meta synchronization.
 *
 * Output format:
 * - key:   meta key string
 * - value: normalized behavior (`copy`, `copy-once`, `translate`, `ignore`)
 *
 * Rule shape compatibility:
 * - scalar behavior map (`meta_key => action`)
 * - array rules with `id` and `action`/`file_action`
 *
 * Notes:
 * - unknown/invalid actions are dropped,
 * - returned map is normalized and safe for downstream comparisons.
 *
 * @return array<string, string> Map of meta key to behavior keyword.
 */
function wpr_translate_get_meta_field_behaviors() {
    // Normalize the configured behavior map.
    $normalized = array();

    if ( function_exists( 'wpr_translate_get_custom_field_rules' ) ) {
        $rules = wpr_translate_get_custom_field_rules();

        foreach ( $rules as $meta_key => $rule ) {
            $behavior = '';

            if ( is_array( $rule ) ) {
                // Extract meta key and behavior from array rules.
                if ( isset( $rule['id'] ) ) {
                    $meta_key = $rule['id'];
                }

                if ( isset( $rule['action'] ) ) {
                    $behavior = $rule['action'];
                } elseif ( isset( $rule['file_action'] ) ) {
                    $behavior = $rule['file_action'];
                }
            } else {
                $behavior = $rule;
            }

            $meta_key = is_string( $meta_key ) ? trim( $meta_key ) : '';

            if ( '' === $meta_key ) {
                // Skip invalid meta keys.
                continue;
            }

            // Ensure behavior values are one of the supported internal keywords.
            $normalized_behavior = wpr_translate_normalize_meta_field_behavior_value( $behavior );

            if ( '' === $normalized_behavior ) {
                // Skip unsupported behaviors.
                continue;
            }

            $normalized[ $meta_key ] = $normalized_behavior;
        }
    }

    /**
     * Filter the map of meta field behaviors used when synchronizing translations.
     *
     * @param array<string, string> $normalized Map of meta key to behavior keyword.
     */
    return apply_filters( 'wpr_translate_meta_field_behaviors', $normalized );
}

/**
 * Normalize a raw behavior value to a supported keyword.
 *
 * Accepted outputs:
 * - copy
 * - copy-once
 * - translate
 * - ignore
 *
 * Unsupported or empty values return an empty string so callers can skip them.
 * Legacy alias `copy_once` is normalized to `copy-once`.
 *
 * @param mixed $behavior Raw behavior value.
 *
 * @return string
 */
function wpr_translate_normalize_meta_field_behavior_value( $behavior ) {
    if ( is_array( $behavior ) ) {
        // Support both the legacy `action` key and the updated `value` key
        // when the behavior is provided as an array structure.
        if ( isset( $behavior['action'] ) ) {
            $behavior = $behavior['action'];
        } elseif ( isset( $behavior['value'] ) ) {
            $behavior = $behavior['value'];
        }
    }

    // Normalize the behavior to a lowercase string.
    $behavior = is_string( $behavior ) ? strtolower( trim( $behavior ) ) : '';

    if ( 'copy_once' === $behavior ) {
        // Normalize legacy naming.
        $behavior = 'copy-once';
    }

    $allowed = array( 'copy', 'copy-once', 'translate', 'ignore' );

    if ( ! in_array( $behavior, $allowed, true ) ) {
        // Reject unsupported behaviors.
        return '';
    }

    return $behavior;
}
