<?php

declare(strict_types=1);

namespace App\Support\Diff;

use BackedEnum;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
use Stringable;

final class ValueStringifier
{
    public function stringify(mixed $value): string
    {
        if ($value === null) {
            return '—';
        }

        if ($value instanceof BackedEnum) {
            return $this->stringify($value->value);
        }

        if ($value instanceof Stringable) {
            $value = (string) $value;
        }

        if (is_bool($value)) {
            return $value ? 'Enabled' : 'Disabled';
        }

        if (is_string($value)) {
            return $value === '' ? '""' : $value;
        }

        if (is_int($value) || is_float($value)) {
            return (string) $value;
        }

        if (is_array($value)) {
            if ($value === []) {
                return '[]';
            }

            if ($this->isInlineList($value)) {
                return implode(', ', array_map(fn (mixed $item): string => $this->stringify($item), $value));
            }

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

        if ($value instanceof Arrayable) {
            return $this->stringify($value->toArray());
        }

        if ($value instanceof JsonSerializable) {
            return $this->stringify($value->jsonSerialize());
        }

        return $this->encodeStructuredValue(['value' => get_debug_type($value)]);
    }

    /**
     * @param  array<int|string, mixed>  $value
     */
    private function encodeStructuredValue(array $value): string
    {
        return json_encode(
            $this->normalizeStructuredValue($value),
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
        ) ?: '{}';
    }

    /**
     * @param  array<int|string, mixed>  $value
     * @return array<int|string, mixed>
     */
    private function normalizeStructuredValue(array $value): array
    {
        $normalized = [];

        foreach ($value as $key => $item) {
            if ($item instanceof BackedEnum) {
                $normalized[$key] = $item->value;

                continue;
            }

            if ($item instanceof Stringable) {
                $normalized[$key] = (string) $item;

                continue;
            }

            if ($item instanceof Arrayable) {
                $normalized[$key] = $this->normalizeStructuredValue($item->toArray());

                continue;
            }

            if ($item instanceof JsonSerializable) {
                $json = $item->jsonSerialize();
                $normalized[$key] = is_array($json)
                    ? $this->normalizeStructuredValue($json)
                    : $json;

                continue;
            }

            if (is_array($item)) {
                $normalized[$key] = $this->normalizeStructuredValue($item);

                continue;
            }

            if (is_object($item)) {
                $normalized[$key] = get_debug_type($item);

                continue;
            }

            $normalized[$key] = $item;
        }

        return array_is_list($normalized) ? array_values($normalized) : $normalized;
    }

    /**
     * @param  array<int, mixed>  $value
     */
    private function isInlineList(array $value): bool
    {
        if (! array_is_list($value)) {
            return false;
        }

        foreach ($value as $item) {
            if ($item !== null
                && ! is_scalar($item)
                && ! $item instanceof BackedEnum
                && ! $item instanceof Stringable) {
                return false;
            }
        }

        return true;
    }
}
