<?php

namespace CliqThemes\ORM\Form\Storage;

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

class TableStorageStrategy implements StorageStrategy
{
    protected Connection $connection;
    protected string $table;
    protected string $column;
    protected string $keyColumn;

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

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

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

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

    public function saveOrUpdate(int $modelId, string $field, mixed $value): void
    {
        if ($value === null) {
            $this->delete($modelId, $field);

            return;
        }

        $table = $this->table;
        $column = $this->column !== '' ? $this->column : $field;
        $keyColumn = $this->keyColumn;
        $data = [$column => $this->prepareValue($value)];
        $where = [$keyColumn => $modelId];

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

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

    public function get(int $modelId, string $field): mixed
    {
        $table = $this->connection->table($this->table);
        $column = $this->column !== '' ? $this->column : $field;
        $keyColumn = $this->keyColumn;
        $sql = sprintf('SELECT %s FROM %s WHERE %s = %%d LIMIT 1', $this->wrapIdentifier($column), $table, $this->wrapIdentifier($keyColumn));
        $results = $this->connection->select($sql, [$modelId]);

        if (empty($results)) {
            return null;
        }

        $value = $results[0][$column] ?? null;

        return $this->restoreValue($value);
    }

    public function delete(int $modelId, string $field): void
    {
        $this->connection->delete($this->table, [$this->keyColumn => $modelId]);
    }

    protected function prepareValue(mixed $value): mixed
    {
        if (is_array($value) || is_object($value)) {
            if (function_exists('maybe_serialize')) {
                return maybe_serialize($value);
            }

            return serialize($value);
        }

        return $value;
    }

    protected function restoreValue(mixed $value): mixed
    {
        if (!is_string($value)) {
            return $value;
        }

        if (function_exists('is_serialized') && !is_serialized($value)) {
            return $value;
        }

        // Stored values are scalars and arrays of scalars. Saying so explicitly
        // means a crafted string in this column cannot instantiate a class and
        // reach its __wakeup/__destruct, which is the object-injection route out
        // of unserialize(). `false` on malformed input is expected, hence the
        // suppression: the value is returned as the string it was.
        $decoded = @unserialize($value, ['allowed_classes' => false]);

        return $decoded === false && $value !== 'b:0;' ? $value : $decoded;
    }

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

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