<?php

declare(strict_types=1);

namespace Apexl\Io\Traits;

use Apexl\Io\Entity;
use Apexl\Io\Enums\Casts;
use Apexl\Io\Interfaces\HasCurrencyCastsInterface;
use Apexl\Io\Services\MoneyFactory;
use Apexl\Io\User\Entities\UserEntity;
use BackedEnum;
use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Money\Money;
use UnitEnum;
use ValueError;

trait HasCasts
{
    private const string FIELD__CAST = 'cast';

    protected function getCasted($name)
    {
        $value = $this->getField($name);

        if ($value !== null && $cast = $this->getCast($name)) {
            if ($cast instanceof Closure) {
                return $cast($value);
            }

            if ($cast instanceof Entity) {
                return $cast::find($value);
            }

            if (is_a($cast, BackedEnum::class, true)) {
                if (method_exists($cast, 'get')) {
                    return $cast::get($value);
                }

                /** @var BackedEnum $cast */
                return $cast::from($value);
            }

            if (is_string($cast) && is_a($cast, UnitEnum::class, true)) {
                /** @var string $cast */
                return constant(sprintf('%s::%s', $cast, $value));
            }

            if (is_array($cast)) {
                [$getter,] = $cast;

                return $getter($value);
            }

            switch ($cast) {
                case Casts::DATE:
                case Casts::DATETIME:
                case Casts::DATETIME_STAMP:
                    if ($cast === Casts::DATETIME_STAMP) {
                        $value = sprintf('@%d', $value);
                    }
                    try {
                        return new DateTimeImmutable($value);
                    } catch (Exception $e) {
                        user_error(
                            sprintf('Error parsing DateTime: %s', $e->getMessage()),
                            E_USER_WARNING
                        );

                        return null;
                    }
                case Casts::CURRENCY:
                    if ($this instanceof HasCurrencyCastsInterface) {
                        return \Apexl\Io\container()->get(MoneyFactory::class)
                            ->money($this, $value, $name);
                    }

                    throw new ValueError(
                        'Entities casting to `Casts::CURRENCY` must implement `HasCurrencyCastsInterface`'
                    );

                case Casts::SERIALIZE:
                    return unserialize(base64_decode($value));
                case Casts::JSON:
                    return json_decode($value);
                default:
                    settype($value, $cast->getType());
            }
        }

        return $value;
    }

    abstract public function getField($field): mixed;

    /**
     * @return Casts|Entity|class-string<UnitEnum>|array|Closure|null
     */
    private function getCast(string $name): Casts|Entity|string|array|Closure|null
    {
        if (!$this->fieldConfig) {
            $this->fieldConfig = $this->fieldConfig();
        }

        $cast = $this->fieldConfig[$name]['cast'] ?? null;

        if (is_string($cast)) {
            if (is_a($cast, BackedEnum::class, true)) {
                return $cast;
            } elseif (is_a($cast, Entity::class, true)) {
                return new $cast();
            }
        }

        return $cast;
    }

    protected function fieldConfig(): array
    {
        return $this->withCasts();
    }

    /** @noinspection PhpUnused */

    protected function withCasts(array $fieldConfig = []): array
    {
        return array_merge_recursive(
            $fieldConfig,
            array_map(
                fn(string|\Apexl\Io\Enums\Casts|array $cast) => ['cast' => $cast],
                $this->casts()
            )
        );
    }

    /**
     * @return (Casts|string|array)[]
     */
    public function casts(): array
    {
        $casts = [];

        foreach ($this->entityFields() as $field) {
            $casts[$field->field] = $field->type;
        }

        return [
            ...$casts,
            'created' => Casts::DATETIME_STAMP,
            'modified' => Casts::DATETIME_STAMP,
            'created_by' => UserEntity::class,
            'modified_by' => UserEntity::class,
        ];
    }

    protected function setCasted($name, $value): void
    {
        if ($value !== null && $cast = $this->getCast($name)) {
            if ($cast instanceof Entity && $value instanceof $cast) {
                $key = $cast->getPrimaryKey();
                $value = $cast->{$key};
            } elseif (is_a($cast, UnitEnum::class, true) && $value instanceof $cast) {
                $value = $value instanceof BackedEnum ? $value->value : $value->name;
            } elseif (is_array($cast)) {
                [, $setter] = $cast;
                $value = $setter($value);
            } else {
                switch ($cast) {
                    case Casts::DATE:
                        if ($value instanceof DateTimeInterface) {
                            $value = $value->format('Y-m-d');
                        }
                        break;
                    case Casts::DATETIME:
                        if ($value instanceof DateTimeInterface) {
                            $value = $value->format('Y-m-d H:i:s');
                        }
                        break;
                    case Casts::DATETIME_STAMP:
                        if ($value instanceof DateTimeInterface) {
                            $value = $value->getTimestamp();
                        }
                        break;
                    case Casts::CURRENCY:
                        if ($value instanceof Money) {
                            $value = $value->getAmount();
                        }
                        break;
                    case Casts::SERIALIZE:
                        $value = base64_encode(serialize($value));
                        break;
                    case Casts::JSON:
                        $value = json_encode($value);
                        break;
                    case Casts::BOOL:
                        $value = $value ? 1 : 0;
                        break;
                    default:
                        break;
                }
            }
        }

        $this->setField($name, $value);
    }

    abstract public function setField($field, $value): static;
}
