<?php

declare(strict_types=1);

namespace App\Framework\Http;

use InvalidArgumentException;

final readonly class Headers
{
    /**
     * Struktur:
     * [
     *   'content-type' => ['Content-Type', ['text/html']],
     *   'set-cookie'   => ['Set-Cookie', ['a=1', 'b=2']],
     * ]
     */

    /**
     * @var array<string, array{string, string[]}>
     * Struktur: 'normalized-lower-name' => [Original-Name, [Wert1, Wert2, ...]]
     */
    private array $headers;

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

    public function with(HeaderKey|string $name, string|array $value): self
    {
        $name = $this->getNameAsString($name);

        $key = strtolower($name);
        $original = $this->normalizeName($name);
        $values = is_array($value) ? array_values($value) : [$value];

        $new = $this->headers;
        $new[$key] = [$original, $values];

        return new self($new);
    }

    public function withAdded(HeaderKey|string $name, string $value): self
    {
        $name = $this->getNameAsString($name);

        $key = strtolower($name);
        $original = $this->normalizeName($name);

        $new = $this->headers;
        if (! isset($new[$key])) {
            $new[$key] = [$original, [$value]];
        } else {
            $new[$key][1][] = $value;
        }

        return new self($new);
    }

    public function without(HeaderKey|string $name): self
    {
        $name = $this->getNameAsString($name);

        $key = strtolower($name);
        $new = $this->headers;
        unset($new[$key]);

        return new self($new);
    }

    /**
     * @return string[]|null
     */
    public function get(HeaderKey|string $name): ?array
    {
        $name = $this->getNameAsString($name);

        return $this->headers[strtolower($name)][1] ?? null;
    }

    public function getFirst(HeaderKey|string $name, ?string $default = null): ?string
    {
        return $this->get($name)[0] ?? $default;
    }

    public function has(HeaderKey|string $name): bool
    {
        $name = $this->getNameAsString($name);

        return isset($this->headers[strtolower($name)]);

    }

    /**
     * Gibt die Header im Format ['Original-Name' => [Wert1, Wert2]]
     */
    public function all(): array
    {
        $output = [];
        foreach ($this->headers as [$original, $values]) {
            $output[$original] = $values;
        }

        return $output;
    }

    /**
     * Normalizes the given header name by converting it to a canonical format.
     * Each word in the name is capitalized, and hyphens are preserved.
     *
     * @param string $name The header name to be normalized.
     * @return string The normalized header name.
     */
    private function normalizeName(string $name): string
    {
        return preg_replace_callback('/(?:^|-)[a-z]/', fn ($m) => strtoupper($m[0]), strtolower($name));
    }

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

    private function getNameAsString(HeaderKey|string $name): string
    {
        return is_string($name) ? $name : $name->value;
    }

    /**
     * Parses an array into the internal headers format.
     * Handles both external format ['Header-Name' => 'value'] and internal format ['normalized-key' => ['Original-Name', ['values']]]
     *
     * @param array $headers Input array in either format
     * @return array Internal headers format: ['normalized-key' => ['Original-Name', ['value1', 'value2']]]
     */
    private function parseFromArray(array $headers): array
    {
        if (empty($headers)) {
            return [];
        }

        // Check if this is already in internal format
        if ($this->isInternalFormat($headers)) {
            return $headers;
        }

        // Parse external format
        $parsed = [];

        foreach ($headers as $name => $value) {
            if (! is_string($name)) {
                throw new InvalidArgumentException('Header name must be a string');
            }

            $key = strtolower($name);
            $original = $this->normalizeName($name);

            if (is_string($value)) {
                $parsed[$key] = [$original, [$value]];
            } elseif (is_array($value)) {
                // Alle Werte als Strings validieren
                foreach ($value as $val) {
                    if (! is_string($val)) {
                        throw new InvalidArgumentException('All header values must be strings: ' . print_r($headers, true));
                    }
                }
                $parsed[$key] = [$original, array_values($value)];
            } else {
                throw new InvalidArgumentException('Header value must be a string or array of strings');
            }
        }

        return $parsed;
    }

    /**
     * Checks if the array is already in internal format
     * Internal format: ['normalized-key' => ['Original-Name', ['value1', 'value2']]]
     */
    private function isInternalFormat(array $headers): bool
    {
        if (empty($headers)) {
            return true;
        }

        // Check the first entry to determine the format
        $firstValue = reset($headers);

        // Internal format has array values with exactly 2 elements: [string, array]
        return is_array($firstValue) &&
               count($firstValue) === 2 &&
               is_string($firstValue[0]) &&
               is_array($firstValue[1]);
    }
}
