<?php
/**
 * Storage and discovery helpers for translation strings.
 *
 * @package WPR_Translate
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
require_once WPR_TRANSLATE_PATH . 'includes/admin/string-database.php';
/**
 * Build lookup maps for locales and codes.
 *
 * @param array $languages Configured languages.
 *
 * @return array
 */
function wpr_translate_admin_build_language_maps( $languages ) {
    // Build maps for locale and language code lookups.
    $locale_map = array(); $code_map = array();
    foreach ( $languages as $language ) {
        // Normalize language codes for map keys.
        if ( isset( $language['code'] ) && '' !== $language['code'] ) {
            $code = strtolower( (string) $language['code'] );
            $code_map[ $code ] = (string) $language['code'];
        }
        // Normalize locale values for map keys.
        if ( isset( $language['locale'] ) && '' !== $language['locale'] ) {
            $locale = strtolower( str_replace( '-', '_', (string) $language['locale'] ) );
            $locale_map[ $locale ] = isset( $language['code'] ) ? (string) $language['code'] : '';
        }
    }
    return array(
        'locale' => $locale_map,
        'code'   => $code_map,
    );
}
/**
 * Merge newly detected strings into the accumulator.
 *
 * @param array $existing Previously detected strings.
 * @param array $new      Newly detected strings.
 *
 * @return array
 */
function wpr_translate_admin_merge_detected_strings( $existing, $new ) {
    foreach ( $new as $key => $string ) {
        // Merge translations for already-known strings.
        if ( isset( $existing[ $key ] ) ) {
            if ( isset( $string['translations'] ) && is_array( $string['translations'] ) ) {
                if ( ! isset( $existing[ $key ]['translations'] ) || ! is_array( $existing[ $key ]['translations'] ) ) {
                    $existing[ $key ]['translations'] = array();
                }

                $existing[ $key ]['translations'] = array_merge(
                    $existing[ $key ]['translations'],
                    $string['translations']
                );
            }

            $existing[ $key ]['value'] = $string['value'];
            $existing[ $key ]['md5']   = $string['md5'];
            continue;
        }

        // Add newly detected string entries.
        $existing[ $key ] = $string;
    }
    return $existing;
}
/**
 * Persist detected strings into the storage table.
 *
 * @param array  $strings      Normalized strings keyed by hash.
 * @param array  $languages    Configured languages.
 * @param string $default_code Default language code.
 *
 * @return int Number of newly created default-language entries.
 */
function wpr_translate_admin_persist_detected_strings( $strings, $languages, $default_code ) {
    global $wpdb;

    // Abort if the storage table is unavailable.
    if ( ! wpr_translate_admin_strings_table_exists() ) {
        return 0;
    }

    $table = $wpdb->prefix . 'wpestate_translation_strings';
    // Prepare language code list and processed flag availability.
    $count              = 0; $language_codes = array();
    $has_processed_flag = wpr_translate_admin_ensure_processed_column();
    foreach ( $languages as $language ) {
        if ( isset( $language['code'] ) && '' !== $language['code'] ) {
            $language_codes[] = (string) $language['code'];
        }
    }
    if ( empty( $language_codes ) ) {
        return 0;
    }
    // Load existing string rows for comparison.
    $existing_map    = wpr_translate_admin_get_existing_strings_map( $table, $strings, $language_codes );
    $pending_rows    = array();
    $now             = current_time( 'mysql' );
    $in_transaction  = ( false !== $wpdb->query( 'START TRANSACTION' ) );
    foreach ( $strings as $string ) {
        // Apply translations for each configured language.
        $translations = isset( $string['translations'] ) && is_array( $string['translations'] ) ? $string['translations'] : array();
        foreach ( $language_codes as $code ) {
            $key               = $string['context'] . '|' . $string['name'] . '|' . $code;
            $translation_value = ( $code === $default_code ) ? (string) $string['value'] : ( isset( $translations[ $code ] ) ? (string) $translations[ $code ] : '' );
            $has_translation   = '' !== trim( wp_strip_all_tags( $translation_value ) );
            $status_value      = ( $code === $default_code ) ? 1 : ( $has_translation ? 1 : 0 );
            $requires_export   = ( 1 === $status_value );
            $data              = array(
                'context'       => (string) $string['context'],
                'name'          => (string) $string['name'],
                'value'         => (string) $string['value'],
                'language_code' => (string) $code,
                'md5'           => (string) $string['md5'],
                'updated_at'    => $now,
                'translation'   => $translation_value,
                'status'        => $status_value,
            );
            if ( $has_processed_flag ) {
                $data['processed'] = $requires_export ? 0 : 1;
            }
            if ( isset( $existing_map[ $key ] ) ) {
                // Update existing rows.
                $update_data = array(
                    'value'       => $data['value'],
                    'md5'         => $data['md5'],
                    'updated_at'  => $data['updated_at'],
                );

                $update_format = array( '%s', '%s', '%s' );

                if ( $code === $default_code || $has_translation ) {
                    // Persist non-empty translations, but never overwrite existing
                    // non-default translations with empty values from import.
                    $update_data['translation'] = $data['translation'];
                    $update_data['status']      = $data['status'];
                    $update_format[]            = '%s';
                    $update_format[]            = '%d';

                    if ( $has_processed_flag ) {
                        $update_data['processed'] = $data['processed'];
                        $update_format[]          = '%d';
                    }
                }

                $wpdb->update(
                    $table,
                    $update_data,
                    array( 'string_id' => (int) $existing_map[ $key ] ),
                    $update_format,
                    array( '%d' )
                );
                continue;
            }
            // Queue inserts for new rows.
            $pending_rows[] = array(
                'data'       => $data,
                'is_default' => ( $code === $default_code ),
            );
        }
    }
    // Insert queued rows in batches.
    $count += wpr_translate_admin_insert_string_rows_batch( $table, $pending_rows, $has_processed_flag );
    if ( $in_transaction ) {
        $wpdb->query( 'COMMIT' );
    }
    return $count;
}

/**
 * Build a lookup map of existing strings for the provided contexts and languages.
 *
 * @param string $table          Table name including prefix.
 * @param array  $strings        Normalized strings keyed by hash.
 * @param array  $language_codes Language codes to check.
 *
 * @return array
 */
function wpr_translate_admin_get_existing_strings_map( $table, $strings, $language_codes ) {
    global $wpdb;

    // Build a lookup list of contexts and names.
    $contexts = array(); $name_lookup = array();
    foreach ( $strings as $string ) {
        $context = (string) $string['context'];
        $name    = (string) $string['name'];
        $contexts[ $context ] = true;
        if ( ! isset( $name_lookup[ $context ] ) ) {
            $name_lookup[ $context ] = array();
        }
        $name_lookup[ $context ][ $name ] = true;
    }
    $contexts = array_keys( $contexts );
    if ( empty( $contexts ) ) {
        return array();
    }
    $existing_map = array();
    foreach ( array_chunk( $contexts, 25 ) as $context_chunk ) {
        // Query existing rows for the current chunk.
        $context_placeholders   = implode( ',', array_fill( 0, count( $context_chunk ), '%s' ) );
        $language_placeholders  = implode( ',', array_fill( 0, count( $language_codes ), '%s' ) );
        $sql                    = "SELECT string_id, context, name, language_code FROM {$table} WHERE context IN ({$context_placeholders}) AND language_code IN ({$language_placeholders})";
        $prepared               = $wpdb->prepare( $sql, array_merge( $context_chunk, $language_codes ) );
        $results                = $wpdb->get_results( $prepared, ARRAY_A );
        foreach ( $results as $row ) {
            // Validate each row before mapping it.
            $context = isset( $row['context'] ) ? (string) $row['context'] : '';
            $name    = isset( $row['name'] ) ? (string) $row['name'] : '';
            if ( '' === $context || '' === $name ) {
                continue;
            }
            if ( ! isset( $name_lookup[ $context ][ $name ] ) ) {
                continue;
            }
            $language_code = isset( $row['language_code'] ) ? (string) $row['language_code'] : '';
            $string_id     = isset( $row['string_id'] ) ? (int) $row['string_id'] : 0;
            if ( '' === $language_code || 0 === $string_id ) {
                continue;
            }
            $key                 = $context . '|' . $name . '|' . $language_code;
            $existing_map[ $key ] = $string_id;
        }
    }
    return $existing_map;
}

/**
 * Insert new string rows in batches to reduce query overhead.
 *
 * @param string $table             Table name including prefix.
 * @param array  $rows              Rows to insert with data and default flag.
 * @param bool   $has_processed_flag Whether the processed flag is available.
 *
 * @return int Number of default-language rows inserted.
 */
function wpr_translate_admin_insert_string_rows_batch( $table, $rows, $has_processed_flag ) {
    global $wpdb;

    // Exit early when there is nothing to insert.
    if ( empty( $rows ) ) {
        return 0;
    }

    // Build the column and placeholder list.
    $columns = array( 'context', 'name', 'value', 'language_code', 'md5', 'updated_at', 'translation', 'status' );

    if ( $has_processed_flag ) {
        $columns[] = 'processed';
    }

    $column_sql   = implode( ',', $columns );
    $placeholder_row = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d' );

    if ( $has_processed_flag ) {
        $placeholder_row[] = '%d';
    }

    $default_inserts = 0;
    $row_batches     = array_chunk( $rows, 50 );

    foreach ( $row_batches as $batch ) {
        // Prepare a batch insert query.
        $placeholders = array();
        $values       = array();
        $default_rows = 0;

        foreach ( $batch as $row ) {
            $placeholders[] = '(' . implode( ',', $placeholder_row ) . ')';

            $values[] = $row['data']['context'];
            $values[] = $row['data']['name'];
            $values[] = $row['data']['value'];
            $values[] = $row['data']['language_code'];
            $values[] = $row['data']['md5'];
            $values[] = $row['data']['updated_at'];
            $values[] = $row['data']['translation'];
            $values[] = (int) $row['data']['status'];

            if ( $has_processed_flag ) {
                $values[] = (int) $row['data']['processed'];
            }

            if ( isset( $row['is_default'] ) && true === $row['is_default'] ) {
                $default_rows++;
            }
        }

        $sql      = "INSERT INTO {$table} ({$column_sql}) VALUES " . implode( ',', $placeholders );
        $prepared = $wpdb->prepare( $sql, $values );
        $result   = $wpdb->query( $prepared );

        if ( false === $result ) {
            continue;
        }

        $default_inserts += $default_rows;
    }

    return $default_inserts;
}
