<?php

declare(strict_types=1);

namespace CliqThemes\UI\Data;

use CliqThemes\UI\Data\Concerns\SupportsColumnTransformers;
use InvalidArgumentException;
use UnexpectedValueException;

/**
 * Data provider that delegates persistence to a schema-aware coordinator.
 */
class SchemaAwareOrmProvider extends OrmTableProvider
{
    use SupportsColumnTransformers;

    private object $coordinator;

    /**
     * @param object $coordinator Expected to provide ORM persistence via list/retrieve/create/update/delete methods.
     * @param array<string,array<string,mixed>> $columns
     * @param array<string,array<string,mixed>> $filters
     */
    public function __construct(object $coordinator, array $columns = [], array $filters = [])
    {
        if (! method_exists($coordinator, 'getModelClass')) {
            throw new InvalidArgumentException(esc_html('Schema persistence coordinator must expose a getModelClass() method.'));
        }

        $modelClass = $coordinator->getModelClass();

        if (! is_string($modelClass) || $modelClass === '') {
            throw new InvalidArgumentException(esc_html('Schema persistence coordinator returned an invalid model class.'));
        }

        parent::__construct($modelClass, $columns, $filters);

        $this->coordinator = $coordinator;
    }

    /**
     * Delegates listing to the coordinator and normalises the response into a {@see PaginatedResult}.
     *
     * @param array<string,mixed> $args
     */
    public function list(array $args = []): iterable
    {
        // Query the model for the page via the parent provider — this applies
        // the model's soft-delete scope, filters, ordering and pagination and
        // yields real records (with ids). Then enrich each row with the
        // coordinator-hydrated fields (meta, pivots, json columns).
        $result = parent::list($args);

        if (! $result instanceof PaginatedResult) {
            return $this->normaliseListResult($result, $args);
        }

        $rows = $result->records();
        $ids = array_values(array_filter(array_map(static fn (array $r): int => (int) ($r['id'] ?? 0), $rows)));

        // Batched hydration: one set of queries for the whole page instead of a
        // per-row retrieve() (the N+1 that made the grid fire hundreds of
        // queries). Falls back to per-row when the coordinator can't batch.
        if ($ids !== [] && method_exists($this->coordinator, 'hydrateMany')) {
            $hydratedAll = $this->coordinator->hydrateMany($ids);

            $records = array_map(function (array $record) use ($hydratedAll): array {
                $id = (int) ($record['id'] ?? 0);
                if ($id > 0 && isset($hydratedAll[$id])) {
                    $record = array_merge($record, $hydratedAll[$id]);
                }

                return $this->applyColumnTransformers($record);
            }, $rows);
        } else {
            $records = array_map(function (array $record): array {
                $id = (int) ($record['id'] ?? 0);

                if ($id > 0 && method_exists($this->coordinator, 'retrieve')) {
                    $hydrated = $this->coordinator->retrieve($id);
                    if (is_array($hydrated)) {
                        $record = array_merge($record, $hydrated);
                    }
                }

                return $this->applyColumnTransformers($record);
            }, $rows);
        }

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

    /**
     * Delegates record retrieval and applies column transformers to the result.
     *
     * @param mixed                     $identifier
     * @param array<string,mixed>       $context
     *
     * @return array<string,mixed>
     */
    public function retrieve($identifier, array $context = []): array
    {
        $record = $this->callCoordinator('retrieve', [$identifier, $context]);

        if (! is_array($record)) {
            return [];
        }

        // Return RAW values. Column transformers produce DISPLAY shapes (badges,
        // related-record names, media previews) for the grid — applying them here
        // would poison the edit form, which binds directly to this record: on save
        // it would write those display shapes back, dropping pivots (names never
        // match ids) and non-slug column values. list()/create()/update() still
        // transform for the grid; single-record retrieval is the edit path.
        return $record;
    }

    /**
     * Persists a record via the coordinator and returns the transformed payload.
     *
     * @param array<string,mixed>       $payload
     * @param array<string,mixed>       $context
     *
     * @return array<string,mixed>
     */
    public function create(array $payload, array $context = []): array
    {
        $record = $this->callCoordinator('create', [$payload, $context]);

        if (! is_array($record)) {
            return [];
        }

        return $this->applyColumnTransformers($record);
    }

    /**
     * Updates a record via the coordinator and returns the transformed 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
    {
        $record = $this->callCoordinator('update', [$identifier, $payload, $context]);

        if (! is_array($record)) {
            return [];
        }

        return $this->applyColumnTransformers($record);
    }

    /**
     * Deletes a record via the coordinator and normalises the response to a boolean.
     *
     * @param mixed               $identifier
     * @param array<string,mixed>  $context
     *
     * @return bool
     */
    public function delete($identifier, array $context = []): bool
    {
        $result = $this->callCoordinator('delete', [$identifier, $context]);

        return (bool) $result;
    }

    /**
     * @param mixed $result
     * @param array<string,mixed> $args
     */
    private function normaliseListResult($result, array $args): PaginatedResult
    {
        if ($result instanceof PaginatedResult) {
            $records = array_map(fn (array $record): array => $this->applyColumnTransformers($record), $result->records());

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

        if (is_array($result) && isset($result['records']) && is_array($result['records'])) {
            $records = array_map(fn (array $record): array => $this->applyColumnTransformers($record), $result['records']);
            $total = (int) ($result['total'] ?? count($records));
            $perPage = (int) ($result['per_page'] ?? ($args['per_page'] ?? $total ?: 1));
            $page = (int) ($result['page'] ?? ($args['page'] ?? 1));

            return new PaginatedResult($records, $total, max(1, $perPage), max(1, $page));
        }

        if (is_iterable($result)) {
            $records = [];

            foreach ($result as $record) {
                if (! is_array($record)) {
                    continue;
                }

                $records[] = $this->applyColumnTransformers($record);
            }

            $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);
        }

        throw new UnexpectedValueException(esc_html('Schema persistence coordinator must return an iterable set of records.'));
    }

    /**
     * Invokes the given method on the coordinator with the supplied arguments.
     *
     * @return mixed
     */
    private function callCoordinator(string $method, array $arguments = [])
    {
        if (! method_exists($this->coordinator, $method)) {
            throw new UnexpectedValueException(esc_html(sprintf('Schema persistence coordinator is missing required method "%s".', $method)));
        }

        return $this->coordinator->{$method}(...$arguments);
    }
}
