<?php

declare(strict_types=1);

namespace App\Framework\Performance\ValueObjects;

use InvalidArgumentException;

/**
 * Immutable value object for metric context/metadata
 */
final readonly class MetricContext
{
    /** @var array<string, mixed> */
    private array $data;

    public function __construct(array $data = [])
    {
        $this->validateData($data);
        $this->data = $data;
    }

    private function validateData(array $data): void
    {
        foreach ($data as $key => $value) {
            if (! is_string($key)) {
                throw new InvalidArgumentException('Context keys must be strings');
            }

            if (! $this->isValidValue($value)) {
                throw new InvalidArgumentException(
                    sprintf('Invalid context value type for key "%s"', $key)
                );
            }
        }
    }

    private function isValidValue(mixed $value): bool
    {
        // Allow scalar values and null
        if (is_scalar($value) || is_null($value)) {
            return true;
        }

        // Allow objects that can be converted to string (have __toString method)
        if (is_object($value)) {
            return method_exists($value, '__toString') ||
                   $value instanceof \Stringable ||
                   $value instanceof \BackedEnum ||
                   $value instanceof \JsonSerializable;
        }

        // For arrays, check if it's a simple list of scalar values
        if (is_array($value)) {
            // Empty arrays are valid
            if (empty($value)) {
                return true;
            }

            // Check if it's a list (sequential numeric keys starting from 0)
            if (array_is_list($value)) {
                // All values must be scalar, null, or valid objects
                return array_reduce(
                    $value,
                    fn (bool $carry, mixed $item) => $carry && $this->isValidValue($item),
                    true
                );
            }

            // For associative arrays, recursively validate
            return array_reduce(
                array_keys($value),
                fn (bool $carry, mixed $key) => $carry && is_string($key) && $this->isValidValue($value[$key]),
                true
            );
        }

        return false;
    }

    public function with(string $key, mixed $value): self
    {
        $newData = $this->data;
        $newData[$key] = $value;

        return new self($newData);
    }

    public function without(string $key): self
    {
        $newData = $this->data;
        unset($newData[$key]);

        return new self($newData);
    }

    public function merge(self $other): self
    {
        return new self(array_merge($this->data, $other->data));
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->data[$key] ?? $default;
    }

    public function has(string $key): bool
    {
        return array_key_exists($key, $this->data);
    }

    public function isEmpty(): bool
    {
        return empty($this->data);
    }

    public function toArray(): array
    {
        return $this->normalizeData($this->data);
    }

    private function normalizeData(array $data): array
    {
        $normalized = [];

        foreach ($data as $key => $value) {
            $normalized[$key] = $this->normalizeValue($value);
        }

        return $normalized;
    }

    private function normalizeValue(mixed $value): mixed
    {
        if (is_scalar($value) || is_null($value)) {
            return $value;
        }

        if (is_object($value)) {
            if ($value instanceof \JsonSerializable) {
                return $value->jsonSerialize();
            }
            if ($value instanceof \BackedEnum) {
                return $value->value;
            }
            if (method_exists($value, '__toString') || $value instanceof \Stringable) {
                return (string) $value;
            }
        }

        if (is_array($value)) {
            return array_map([$this, 'normalizeValue'], $value);
        }

        return (string) $value;
    }

    public function equals(self $other): bool
    {
        return $this->data === $other->data;
    }

    // Common context factory methods
    public static function empty(): self
    {
        return new self([]);
    }

    public static function fromRequest(\App\Framework\Http\Method $method, string $path, ?string $userId = null): self
    {
        $data = [
            'request_method' => $method,
            'request_path' => $path,
        ];

        if ($userId !== null) {
            $data['user_id'] = $userId;
        }

        return new self($data);
    }

    public static function fromDatabase(string $query, string $type, ?string $table = null): self
    {
        $data = [
            'query' => $query,
            'query_type' => $type,
        ];

        if ($table !== null) {
            $data['table'] = $table;
        }

        return new self($data);
    }

    public static function fromCache(string $operation, string $key, ?bool $hit = null): self
    {
        $data = [
            'cache_operation' => $operation,
            'cache_key' => $key,
        ];

        if ($hit !== null) {
            $data['cache_hit'] = $hit;
        }

        return new self($data);
    }
}
