<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\I18n;

defined('ABSPATH') || exit;

// Translation overlays read and update plugin-owned custom tables directly.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

use CliqThemes\ORM\Query\Builder;
use CliqThemes\StoreLocator\Schema\SchemaInstaller;

/**
 * Applies the TRID translation model to repository reads/writes.
 *
 * Reads: lists are scoped to ORIGINAL rows (source_id IS NULL); translated text
 * for the active language is then overlaid in PHP by a single batched lookup
 * keyed on source_id → original id. Missing translations fall back to the
 * original text automatically (we only overwrite columns we actually find).
 *
 * Writes: originals get trid = own id; translations are sibling rows carrying
 * only the translatable text plus {lang, trid, source_id}.
 *
 * Everything is a no-op when no language is active, so single-language installs
 * pay nothing.
 */
final class ContentTranslator
{
    /** Limit list queries to the canonical original rows. */
    public static function scopeOriginals(Builder $query): Builder
    {
        return $query->whereNull('source_id');
    }

    /**
     * Build a generic list processor for the cliq-ui data providers
     * (OrmTableProvider::addListProcessor). It (1) honours an admin language
     * selection passed as `$args['context']['lang']`, (2) drops translation
     * rows so only originals appear in the grid, and (3) overlays the active
     * language's text for the given columns. Pass no columns for scope-only.
     *
     * Returned as a plain callable so the framework stays domain-agnostic.
     *
     * @param array<int, string> $overlayColumns
     */
    public static function listProcessor(string $table, array $overlayColumns = []): callable
    {
        return static function (array $records, array $args) use ($table, $overlayColumns): array {
            // Reset per call: null = auto-detect (WPML/Polylang), '' = force the
            // default language, a code = that language. Avoids a previous call's
            // selection leaking into this one within the same process.
            $lang = $args['context']['lang'] ?? null;
            Multilingual::setOverride($lang === null ? null : (string) $lang);

            $records = array_values(array_filter(
                $records,
                static fn ($r) => empty($r['source_id'])
            ));

            return $overlayColumns === []
                ? $records
                : self::overlay($records, $table, $overlayColumns, 'id');
        };
    }

    /**
     * Overlay active-language text onto a list of original records.
     *
     * @param array<int, array<string, mixed>> $items   Normalised rows (must contain $idKey).
     * @param string                            $table   Logical table name (e.g. 'stores').
     * @param array<int, string>                $columns Translatable text columns to overlay.
     * @return array<int, array<string, mixed>>
     */
    public static function overlay(array $items, string $table, array $columns, string $idKey = 'id'): array
    {
        $lang = Multilingual::current();
        if ($lang === '' || $items === []) {
            return $items;
        }

        $ids = array_values(array_filter(array_map(
            static fn ($it) => (int) ($it[$idKey] ?? 0),
            $items
        )));
        if ($ids === []) {
            return $items;
        }

        $translations = self::fetchTranslations($table, $ids, $lang, $columns);
        if ($translations === []) {
            return $items;
        }

        foreach ($items as &$item) {
            $sourceId = (int) ($item[$idKey] ?? 0);
            $tr = $translations[$sourceId] ?? null;
            if ($tr === null) {
                continue;
            }
            foreach ($columns as $col) {
                if (isset($tr[$col]) && $tr[$col] !== null && $tr[$col] !== '') {
                    $item[$col] = $tr[$col];
                }
            }
            // Mark the row as showing the active language's translation, so the
            // admin "Lang" column can reflect real per-row translation status
            // (originals stay lang='' — i.e. falling back to the default).
            $item['lang'] = $lang;
        }
        unset($item);

        return $items;
    }

    /**
     * Overlay active-language text onto a single original record (by its id).
     *
     * @param array<string, mixed> $item
     * @param array<int, string>   $columns
     * @return array<string, mixed>
     */
    public static function overlayOne(array $item, string $table, array $columns, string $idKey = 'id'): array
    {
        $result = self::overlay([$item], $table, $columns, $idKey);
        return $result[0] ?? $item;
    }

    /**
     * Fetch translation rows for a set of original ids in the active language.
     *
     * @param array<int, int>    $ids
     * @param array<int, string> $columns
     * @return array<int, array<string, mixed>>  Keyed by source_id (original id).
     */
    private static function fetchTranslations(string $table, array $ids, string $lang, array $columns): array
    {
        global $wpdb;

        $columns = array_values(array_filter(array_map(
            static fn ($column): string => preg_replace('/[^a-z0-9_]/i', '', (string) $column) ?: '',
            $columns
        )));
        if ($columns === []) {
            return [];
        }

        $columnPlaceholders = implode(', ', array_fill(0, count($columns), '%i'));
        $idPlaceholders = implode(', ', array_fill(0, count($ids), '%d'));
        $prefixed = SchemaInstaller::qualify(preg_replace('/[^a-z0-9_]/i', '', $table));

        $query = "SELECT source_id, {$columnPlaceholders} FROM %i WHERE source_id IN ({$idPlaceholders}) AND lang = %s";
        $values = array_merge($columns, [$prefixed], $ids, [$lang]);
        // Identifier/id placeholders are generated above and every value is bound.
        // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared
        $sql = $wpdb->prepare(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Generated identifier/id placeholders are all bound below.
            $query,
            $values
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is the prepared result from the immediately preceding statement.
        $rows = $wpdb->get_results($sql, ARRAY_A) ?: [];
        $out = [];
        foreach ($rows as $row) {
            $out[(int) $row['source_id']] = $row;
        }

        return $out;
    }

    /** Stamp a freshly-created original row so its trid equals its own id. */
    public static function stampOriginalTrid(string $table, int $id): void
    {
        if ($id <= 0) {
            return;
        }
        global $wpdb;
        $prefixed = SchemaInstaller::qualify(preg_replace('/[^a-z0-9_]/i', '', $table));
        $wpdb->query($wpdb->prepare(
            'UPDATE %i SET trid = id WHERE id = %d AND trid IS NULL',
            $prefixed,
            $id
        ));
    }

    /** The trid (translation-group id) for an original/source row. */
    public static function tridFor(string $table, int $sourceId): int
    {
        global $wpdb;
        $prefixed = SchemaInstaller::qualify(preg_replace('/[^a-z0-9_]/i', '', $table));
        $trid = (int) $wpdb->get_var($wpdb->prepare(
            'SELECT trid FROM %i WHERE id = %d',
            $prefixed,
            $sourceId
        ));

        return $trid > 0 ? $trid : $sourceId;
    }

    /**
     * Existing translations of an original, as [lang => id].
     *
     * @return array<string, int>
     */
    public static function translationsOf(string $table, int $sourceId): array
    {
        global $wpdb;
        $prefixed = SchemaInstaller::qualify(preg_replace('/[^a-z0-9_]/i', '', $table));
        $rows = $wpdb->get_results($wpdb->prepare(
            'SELECT id, lang FROM %i WHERE source_id = %d',
            $prefixed,
            $sourceId
        ), ARRAY_A) ?: [];

        $out = [];
        foreach ($rows as $row) {
            $out[(string) $row['lang']] = (int) $row['id'];
        }

        return $out;
    }
}
