<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\Data;

defined('ABSPATH') || exit;

use CliqThemes\StoreLocator\I18n\ContentTranslator;
use CliqThemes\StoreLocator\I18n\Multilingual;
use CliqThemes\StoreLocator\Repository\StoreRepository;
use CliqThemes\StoreLocator\Support\FrontendAssetRegistry;
use CliqThemes\UI\Data\PaginatedResult;
use CliqThemes\UI\Data\SchemaAwareOrmProvider;

/**
 * Store data provider that filters coordinator-enriched fields.
 *
 * Pivot and custom-meta values are not real Store columns. When one of those
 * filters is active we hydrate the full result set (base filters applied at the
 * query level), filter on the enriched values in memory, then paginate.
 * Column-only filters take the fast parent path untouched.
 */
final class StoreProvider extends SchemaAwareOrmProvider
{
    /** @var array<string,array<string,mixed>> */
    private array $enrichedFilters;

    /** @var array<string,array<string,mixed>> */
    private array $bulkActions;

    /**
     * @param array<string,array<string,mixed>> $columns
     * @param array<string,array<string,mixed>> $filters
     */
    public function __construct(object $coordinator, array $columns, array $filters, array $bulkActions = [])
    {
        parent::__construct($coordinator, $columns, $filters);
        $this->bulkActions = $bulkActions;
        $this->enrichedFilters = array_filter(
            $filters,
            static fn (array $definition): bool => ($definition['source'] ?? '') === 'enriched'
        );
    }

    /**
     * Apply an allow-listed update to multiple stores.
     *
     * Category updates deliberately merge raw pivot ids from StoreRepository;
     * list column transformers expose labels and therefore cannot be used as a
     * persistence source.
     *
     * @param int[]                $ids
     * @param array<string, mixed> $context
     */
    public function bulkUpdate(
        array $ids,
        string $action,
        string $field,
        mixed $value,
        string $mode = 'replace',
        array $context = []
    ): int {
        $definition = $this->bulkActions[$action] ?? null;
        if (! is_array($definition)) {
            throw new \InvalidArgumentException(esc_html__('This bulk action is not available.', 'cliq-store-locator'));
        }

        $allowedFields = isset($definition['fields'])
            ? array_map('strval', (array) $definition['fields'])
            : [(string) ($definition['field'] ?? '')];
        if ($field === '' || ! in_array($field, $allowedFields, true)) {
            throw new \InvalidArgumentException(esc_html__('This field cannot be changed in bulk.', 'cliq-store-locator'));
        }
        $expectedMode = (string) ($definition['mode'] ?? 'replace');
        if ($mode !== $expectedMode) {
            throw new \InvalidArgumentException(esc_html__('Invalid bulk update mode.', 'cliq-store-locator'));
        }

        if (isset($definition['allowed']) && ! in_array((string) $value, (array) $definition['allowed'], true)) {
            throw new \InvalidArgumentException(esc_html__('Choose a valid value.', 'cliq-store-locator'));
        }

        $storeIds = array_values(array_unique(array_filter(array_map('intval', $ids))));
        $repository = $action === 'categories' ? new StoreRepository() : null;
        $updated = 0;

        foreach ($storeIds as $storeId) {
            if ($storeId <= 0) {
                continue;
            }

            $nextValue = $value;
            if ($action === 'categories') {
                $incoming = array_values(array_unique(array_filter(array_map('intval', (array) $value))));
                if ($incoming === []) {
                    throw new \InvalidArgumentException(esc_html__('Choose at least one category.', 'cliq-store-locator'));
                }
                $record = $repository?->find($storeId);
                if ($record === null) {
                    continue;
                }
                $existing = array_map('intval', (array) ($record['categories'] ?? []));
                $nextValue = array_values(array_unique(array_merge($existing, $incoming)));
            } elseif ($action === 'brand') {
                $nextValue = (int) $value;
                if ($nextValue <= 0) {
                    throw new \InvalidArgumentException(esc_html__('Choose a valid brand.', 'cliq-store-locator'));
                }
            }

            parent::update($storeId, [$field => $nextValue], $context);
            ++$updated;
        }

        if ($updated > 0) {
            FrontendAssetRegistry::invalidate();
        }

        return $updated;
    }

    /**
     * Editing with a language active saves that language's TRANSLATION sibling
     * (name/description), not the original — language-neutral data stays on the
     * original. With no language (default) this is the normal update.
     *
     * @param int|string           $identifier
     * @param array<string, mixed> $payload
     * @param array<string, mixed> $context
     * @return array<string, mixed>
     */
    public function update($identifier, array $payload, array $context = []): array
    {
        $lang = Multilingual::normalize((string) ($context['lang'] ?? ''));
        if ($lang !== '') {
            $saved = (new \CliqThemes\StoreLocator\Repository\StoreRepository())
                ->saveTranslation((int) $identifier, $lang, $payload);
            if ($saved !== null) {
                // Return the original, overlaid with the just-saved language so the
                // grid/form reflects the new translation immediately.
                return $this->retrieve($identifier, $context);
            }
        }

        return parent::update($identifier, $payload, $context);
    }

    /**
     * Load a record, overlaying the active language's translation text so the
     * edit form prefills the translation being worked on (falls back to original).
     *
     * @param int|string           $identifier
     * @param array<string, mixed> $context
     * @return array<string, mixed>
     */
    public function retrieve($identifier, array $context = []): array
    {
        $record = parent::retrieve($identifier, $context);
        $lang = Multilingual::normalize((string) ($context['lang'] ?? ''));
        if ($record !== [] && $lang !== '') {
            Multilingual::setOverride($lang);
            $record = ContentTranslator::overlayOne($record, 'cliq_sl_stores', ['name', 'description'], 'id');
        }

        return $record;
    }

    public function list(array $args = []): iterable
    {
        $filters = is_array($args['filters'] ?? null) ? $args['filters'] : [];

        $enrichedActive = [];
        foreach ($this->enrichedFilters as $key => $definition) {
            $value = $filters[$key] ?? '';
            if ($value !== '' && $value !== null && $value !== []) {
                $enrichedActive[$key] = [
                    'value' => $value,
                    'definition' => $definition,
                ];
            }
        }

        if ($enrichedActive === []) {
            return $this->overlay(parent::list($args), $args);
        }

        $page = max(1, (int) ($args['page'] ?? 1));
        $perPage = max(1, (int) ($args['per_page'] ?? 20));

        // Hydrate everything with only the column filters applied.
        $baseArgs = $args;
        $baseArgs['filters'] = array_diff_key($filters, $this->enrichedFilters);
        $baseArgs['page'] = 1;
        $baseArgs['per_page'] = 100000;

        $all = parent::list($baseArgs);
        $records = $all instanceof PaginatedResult
            ? $all->records()
            : (is_array($all) ? $all : iterator_to_array($all));

        $filtered = array_values(array_filter($records, function (array $record) use ($enrichedActive): bool {
            foreach ($enrichedActive as $key => $filter) {
                if (
                    ! $this->matchesEnrichedFilter(
                        $record[$key] ?? null,
                        $filter['value'],
                        $filter['definition']
                    )
                ) {
                    return false;
                }
            }

            return true;
        }));

        $slice = array_slice($filtered, ($page - 1) * $perPage, $perPage);

        return $this->overlay(new PaginatedResult($slice, count($filtered), $perPage, $page), $args);
    }

    /**
     * Match scalar or multi-value enriched fields with ANY-value semantics.
     */
    private function matchesEnrichedFilter(mixed $actual, mixed $wanted, array $definition): bool
    {
        $actualValues = array_map('strval', is_array($actual) ? $actual : [$actual]);
        $wantedValues = array_values(array_filter(
            array_map('strval', is_array($wanted) ? $wanted : [$wanted]),
            static fn (string $value): bool => $value !== ''
        ));
        $operator = strtolower((string) ($definition['operator'] ?? 'equals'));

        // A column transformer may replace pivot ids with their display labels
        // before this provider gets the hydrated page (categories do this for
        // the column manager). Accept both the submitted option value and its
        // label so filtering remains based on the same provider definition.
        $options = is_array($definition['options'] ?? null) ? $definition['options'] : [];
        $candidates = $wantedValues;
        foreach ($wantedValues as $wantedValue) {
            foreach ($options as $optionValue => $option) {
                if (is_array($option)) {
                    $value = (string) ($option['value'] ?? $option['id'] ?? '');
                    $label = (string) ($option['label'] ?? $option['name'] ?? $value);
                } else {
                    $value = (string) $optionValue;
                    $label = (string) $option;
                }

                if ($value === $wantedValue && $label !== '') {
                    $candidates[] = $label;
                    break;
                }
            }
        }

        if ($candidates === []) {
            return true;
        }

        foreach (array_unique($candidates) as $needle) {
            foreach ($actualValues as $value) {
                if ($operator === 'contains') {
                    if (stripos($value, $needle) !== false) {
                        return true;
                    }
                } elseif ($value === $needle) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Overlay the active language's translated text onto the page of results.
     * Runs AFTER the parent's coordinator enrichment so it isn't overwritten.
     * Translation-row scoping is handled separately by the generic list
     * processor (registered in Plugin), keeping pagination totals correct.
     */
    private function overlay(PaginatedResult $result, array $args): PaginatedResult
    {
        // Reset per call (see ContentTranslator::listProcessor); the scope
        // processor already set this, but keep it explicit and idempotent.
        $lang = $args['context']['lang'] ?? null;
        Multilingual::setOverride($lang === null ? null : (string) $lang);

        $records = ContentTranslator::overlay($result->records(), 'cliq_sl_stores', ['name', 'description'], 'id');

        // Turn the (now-overlaid) `lang` into a grid "Lang" column badge showing
        // per-row translation status for the active language. Done HERE (after
        // overlay) because column transformers run before overlay and would be
        // clobbered when overlay stamps the row's language.
        $active = Multilingual::current();
        foreach ($records as &$record) {
            $record['lang'] = $this->langBadge($active, (string) ($record['lang'] ?? ''));
        }
        unset($record);

        return new PaginatedResult($records, $result->total(), $result->perPage(), $result->page());
    }

    /**
     * Render the "Lang" cell: '—' when no language is active, a green badge with
     * the language label when the row is translated in it, else a muted
     * "Not translated" badge. Returns the grid's {value,isHtml} cell shape.
     *
     * @return array{value:string,isHtml:bool}
     */
    private function langBadge(string $active, string $rowLang): array
    {
        if ($active === '') {
            return ['value' => '—', 'isHtml' => false];
        }
        if ($rowLang !== '') {
            $label = Multilingual::label($active);
            return ['value' => '<span class="cliq-lang cliq-lang--on">' . esc_html($label) . '</span>', 'isHtml' => true];
        }

        return ['value' => '<span class="cliq-lang cliq-lang--off">' . esc_html__('Not translated', 'cliq-store-locator') . '</span>', 'isHtml' => true];
    }
}
