<?php

namespace CliqThemes\ORM\Form\Storage;

use CliqThemes\ORM\Database\Connection;
use CliqThemes\ORM\Form\StorageStrategy;
use InvalidArgumentException;

class JsonColumnStrategy implements StorageStrategy
{
    protected Connection $connection;
    protected string $table;
    protected string $column;
    protected string $keyColumn;
    protected array $path;
    /** @var array<int, array<string, mixed>> request-scoped decoded-payload cache by model id */
    protected array $payloadCache = [];

    public function __construct(array $options, ?Connection $connection = null)
    {
        $this->connection = $connection ?? new Connection();

        $table = $options['table'] ?? null;
        $column = $options['column'] ?? null;

        if (!is_string($table) || $table === '') {
            throw new InvalidArgumentException('JsonColumnStrategy requires a non-empty "table" option.');
        }

        if (!is_string($column) || $column === '') {
            throw new InvalidArgumentException('JsonColumnStrategy requires a non-empty "column" option.');
        }

        $this->table = $table;
        $this->column = $column;
        $this->keyColumn = is_string($options['key_column'] ?? null) ? $options['key_column'] : 'model_id';
        $this->path = $this->normalisePath($options['path'] ?? []);
    }

    public function saveOrUpdate(int $modelId, string $field, mixed $value): void
    {
        $data = $this->getJsonPayload($modelId);
        $path = $this->path ?: [$field];
        $this->setValueByPath($data, $path, $value);
        $this->persistPayload($modelId, $data);
    }

    public function get(int $modelId, string $field): mixed
    {
        $data = $this->getJsonPayload($modelId);
        $path = $this->path ?: [$field];

        return $this->getValueByPath($data, $path);
    }

    public function delete(int $modelId, string $field): void
    {
        $data = $this->getJsonPayload($modelId);
        $path = $this->path ?: [$field];
        $this->unsetValueByPath($data, $path);
        $this->persistPayload($modelId, $data);
    }

    protected function persistPayload(int $modelId, array $payload): void
    {
        $table = $this->table;
        $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $keyColumn = $this->keyColumn;

        $data = [$this->column => $json];
        $where = [$keyColumn => $modelId];

        $updated = $this->connection->update($table, $data, $where);

        if ($updated === 0) {
            $this->connection->insert($table, array_merge($where, $data));
        }

        // Keep the cache consistent after a write so a later read in the same
        // request returns the new payload without re-querying.
        $this->payloadCache[$modelId] = $payload;
    }

    protected function getJsonPayload(int $modelId): array
    {
        // A single json column commonly backs MANY fields (each a different path),
        // so cache the decoded payload per model for the request — otherwise every
        // field hydration re-runs the same SELECT (an N-per-field query storm).
        if (array_key_exists($modelId, $this->payloadCache)) {
            return $this->payloadCache[$modelId];
        }

        $table = $this->connection->table($this->table);
        $keyColumn = $this->keyColumn;
        $sql = sprintf('SELECT %s FROM %s WHERE %s = %%d LIMIT 1', $this->wrapIdentifier($this->column), $table, $this->wrapIdentifier($keyColumn));
        $results = $this->connection->select($sql, [$modelId]);

        $encoded = empty($results) ? '' : ($results[0][$this->column] ?? '');
        $decoded = json_decode((string) $encoded, true);

        return $this->payloadCache[$modelId] = (is_array($decoded) ? $decoded : []);
    }

    protected function setValueByPath(array &$data, array $path, mixed $value): void
    {
        $reference = &$data;
        $lastIndex = array_key_last($path);

        foreach ($path as $index => $segment) {
            if ($index === $lastIndex) {
                $reference[$segment] = $value;
                break;
            }

            if (!isset($reference[$segment]) || !is_array($reference[$segment])) {
                $reference[$segment] = [];
            }

            $reference = &$reference[$segment];
        }
    }

    protected function getValueByPath(array $data, array $path): mixed
    {
        $reference = $data;

        foreach ($path as $segment) {
            if (!is_array($reference) || !array_key_exists($segment, $reference)) {
                return null;
            }

            $reference = $reference[$segment];
        }

        return $reference;
    }

    protected function unsetValueByPath(array &$data, array $path): void
    {
        if ($path === []) {
            return;
        }

        $segment = array_shift($path);

        if (!array_key_exists($segment, $data)) {
            return;
        }

        // Leaf segment: remove it directly.
        if ($path === []) {
            unset($data[$segment]);

            return;
        }

        // Descend; the by-reference parameter ensures mutations propagate back
        // up to $data (a destructured copy would silently drop the change).
        if (is_array($data[$segment])) {
            $this->unsetValueByPath($data[$segment], $path);

            // Prune parents that became empty after the removal.
            if ($data[$segment] === []) {
                unset($data[$segment]);
            }
        }
    }

    protected function normalisePath(mixed $path): array
    {
        if (is_string($path) && $path !== '') {
            return [$path];
        }

        if (is_array($path)) {
            return array_values(array_filter(array_map(static function ($segment) {
                return is_string($segment) && $segment !== '' ? $segment : null;
            }, $path), static fn ($segment) => $segment !== null));
        }

        return [];
    }

    protected function wrapIdentifier(string $identifier): string
    {
        $identifier = trim($identifier, '`');

        return '`' . str_replace('`', '``', $identifier) . '`';
    }
}
