<?php

declare(strict_types=1);

namespace CliqThemes\UI\Data;

use CliqThemes\UI\Contracts\DataProvider;

/**
 * In-memory data provider useful for demos, tests, or static datasets.
 */
final class ArrayTableProvider implements DataProvider
{
    private string $primaryKey;

    /**
     * @var array<string,array<string,mixed>>
     */
    private array $records = [];

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

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

    private int $nextId = 1;

    /**
     * @param array<int,array<string,mixed>> $records
     * @param array<string,array<string,mixed>> $columns
     * @param array<string,array<string,mixed>> $filters
     */
    public function __construct(
        array $records = [],
        array $columns = [],
        array $filters = [],
        string $primaryKey = 'id'
    ) {
        $this->primaryKey = $primaryKey;
        $this->columns = $columns ?: $this->inferColumns($records);
        $this->filters = $filters;

        foreach ($records as $record) {
            $this->storeRecord($record);
        }
    }

    /**
     * Returns a paginated slice of in-memory records with optional filters and sorting.
     *
     * @param array<string,mixed> $args Supported keys: per_page, page, order_by, order, filters.
     */
    public function list(array $args = []): iterable
    {
        $records = array_values($this->records);

        $records = $this->applyFilters($records, $args['filters'] ?? []);

        if (isset($args['order_by'])) {
            $orderBy = (string) $args['order_by'];
            $order = strtolower((string) ($args['order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';

            usort($records, static function (array $a, array $b) use ($orderBy, $order): int {
                $valueA = $a[$orderBy] ?? null;
                $valueB = $b[$orderBy] ?? null;

                if ($valueA === $valueB) {
                    return 0;
                }

                if ($order === 'asc') {
                    return ($valueA <=> $valueB);
                }

                return ($valueB <=> $valueA);
            });
        }

        $total = count($records);
        $perPage = max(1, (int) ($args['per_page'] ?? $total ?: 1));
        $page = max(1, (int) ($args['page'] ?? 1));
        $offset = ($page - 1) * $perPage;

        $slice = array_slice($records, $offset, $perPage);

        return new PaginatedResult($slice, $total, $perPage, $page);
    }

    /**
     * Locates a single record by its identifier.
     *
     * @param mixed                     $identifier
     * @param array<string,mixed>       $context
     *
     * @return array<string,mixed>
     */
    public function retrieve($identifier, array $context = []): array
    {
        $key = $this->normaliseIdentifier($identifier);

        return $this->records[$key] ?? [];
    }

    /**
     * Stores a new record and returns the persisted payload including the generated identifier.
     *
     * @param array<string,mixed>       $payload
     * @param array<string,mixed>       $context
     *
     * @return array<string,mixed>
     */
    public function create(array $payload, array $context = []): array
    {
        return $this->storeRecord($payload);
    }

    /**
     * Updates an existing record and returns the merged payload.
     *
     * @param mixed                     $identifier
     * @param array<string,mixed>       $payload
     * @param array<string,mixed>       $context
     *
     * @return array<string,mixed>
     */
    public function update($identifier, array $payload, array $context = []): array
    {
        $key = $this->normaliseIdentifier($identifier);

        if (! isset($this->records[$key])) {
            throw new \InvalidArgumentException(esc_html(sprintf('Unknown record "%s".', (string) $key)));
        }

        $record = array_merge($this->records[$key], $payload);
        $record[$this->primaryKey] = $key;

        $this->records[$key] = $record;

        return $record;
    }

    /**
     * Removes a record from the in-memory store.
     *
     * @param mixed               $identifier
     * @param array<string,mixed>  $context
     *
     * @return bool
     */
    public function delete($identifier, array $context = []): bool
    {
        $key = $this->normaliseIdentifier($identifier);

        if (! isset($this->records[$key])) {
            return false;
        }

        unset($this->records[$key]);

        return true;
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    public function columns(): array
    {
        return $this->columns;
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    public function filters(): array
    {
        return $this->filters;
    }

    /**
     * @param array<string,mixed> $record
     */
    private function storeRecord(array $record): array
    {
        if (! array_key_exists($this->primaryKey, $record)) {
            $record[$this->primaryKey] = $this->generateIdentifier();
        }

        $key = (string) $record[$this->primaryKey];
        $this->records[$key] = $record;

        $this->nextId = max($this->nextId, ((int) $record[$this->primaryKey]) + 1);

        return $record;
    }

    /**
     * @param array<int,array<string,mixed>> $records
     * @return array<string,array<string,mixed>>
     */
    private function inferColumns(array $records): array
    {
        $first = $records[0] ?? [];
        $columns = [];

        foreach (array_keys($first) as $column) {
            $columns[$column] = [
                'label' => ucwords(str_replace('_', ' ', (string) $column)),
                'sortable' => true,
            ];
        }

        return $columns;
    }

    private function generateIdentifier(): int
    {
        return $this->nextId++;
    }

    private function normaliseIdentifier($identifier): string
    {
        if (is_array($identifier) && isset($identifier[$this->primaryKey])) {
            return (string) $identifier[$this->primaryKey];
        }

        return (string) $identifier;
    }

    /**
     * @param array<int,array<string,mixed>> $records
     * @param array<string,mixed>            $filters
     *
     * @return array<int,array<string,mixed>>
     */
    private function applyFilters(array $records, array $filters): array
    {
        if ($filters === []) {
            return $records;
        }

        return array_values(array_filter($records, function (array $record) use ($filters): bool {
            foreach ($filters as $key => $value) {
                if ($value === null || $value === '' || $value === []) {
                    continue;
                }

                $definition = $this->filters[(string) $key] ?? [];
                $operator = strtolower((string) ($definition['operator'] ?? 'equals'));
                $fields = $definition['fields'] ?? null;
                $fieldKey = $definition['field'] ?? (string) $key;

                if (is_array($fields) && $fields !== []) {
                    $matched = false;

                    foreach ($fields as $candidate) {
                        $recordValue = $record[(string) $candidate] ?? null;

                        if ($this->matchesFilter($recordValue, $value, $operator)) {
                            $matched = true;
                            break;
                        }
                    }

                    if (! $matched) {
                        return false;
                    }

                    continue;
                }

                $recordValue = $record[(string) $fieldKey] ?? null;

                if (! $this->matchesFilter($recordValue, $value, $operator)) {
                    return false;
                }
            }

            return true;
        }));
    }

    private function matchesFilter($recordValue, $filterValue, string $operator): bool
    {
        switch ($operator) {
            case 'contains':
                if (is_array($recordValue)) {
                    $needles = is_array($filterValue) ? $filterValue : [$filterValue];

                    foreach ($needles as $needle) {
                        if (in_array($needle, $recordValue, true)) {
                            return true;
                        }
                    }

                    return false;
                }

                return stripos((string) $recordValue, (string) $filterValue) !== false;
            case 'not_equals':
                if (is_array($filterValue)) {
                    return ! in_array($recordValue, $filterValue, true);
                }

                return (string) $recordValue !== (string) $filterValue;
            case 'in':
                $needles = is_array($filterValue) ? $filterValue : [$filterValue];

                return in_array((string) $recordValue, array_map('strval', $needles), true);
            case 'not_in':
                $excluded = is_array($filterValue) ? $filterValue : [$filterValue];

                return ! in_array((string) $recordValue, array_map('strval', $excluded), true);
            case 'search':
                if (is_array($recordValue)) {
                    $haystack = implode(' ', array_map('strval', $recordValue));

                    return stripos($haystack, (string) $filterValue) !== false;
                }

                return stripos((string) $recordValue, (string) $filterValue) !== false;
            case 'equals':
            default:
                if (is_array($filterValue)) {
                    return in_array((string) $recordValue, array_map('strval', $filterValue), true);
                }

                return (string) $recordValue === (string) $filterValue;
        }
    }
}
