<?php

namespace CliqThemes\ORM\Form;

use InvalidArgumentException;
use RuntimeException;

/**
 * Maps field names to their storage descriptors derived from a form schema.
 */
class FieldStorageMap
{
    /** @var array<string, FieldStorageDescriptor> */
    protected array $descriptors = [];

    /**
     * @param FieldStorageDescriptor[] $descriptors
     */
    public function __construct(array $descriptors = [])
    {
        foreach ($descriptors as $descriptor) {
            $this->addDescriptor($descriptor);
        }
    }

    /**
     * Build a storage map from an associative form definition array.
     *
     * @param array<string, mixed> $definition
     */
    public static function fromForm(array $definition): self
    {
        $map = new self();
        $sections = $definition['sections'] ?? [];

        foreach ($sections as $section) {
            $path = [];
            if (isset($section['id']) && is_string($section['id'])) {
                $path[] = $section['id'];
            }

            if (!empty($section['fields']) && is_array($section['fields'])) {
                $map->collectFields($section['fields'], $path);
            }

            if (empty($section['groups']) || !is_array($section['groups'])) {
                continue;
            }

            foreach ($section['groups'] as $group) {
                $groupPath = $path;
                if (isset($group['id']) && is_string($group['id'])) {
                    $groupPath[] = $group['id'];
                }

                if (!empty($group['fields']) && is_array($group['fields'])) {
                    $map->collectFields($group['fields'], $groupPath);
                }
            }
        }

        return $map;
    }

    /**
     * Recursively collect field definitions from the form structure.
     *
     * @param array<int, mixed> $fields
     * @param string[] $path
     */
    protected function collectFields(array $fields, array $path = []): void
    {
        foreach ($fields as $field) {
            if (!is_array($field)) {
                continue;
            }

            $childPath = $path;

            if (isset($field['name']) && is_string($field['name'])) {
                if (!$this->isContainerWithoutStorage($field)) {
                    $descriptor = FieldStorageDescriptor::fromDefinition($field, $path);
                    $this->addDescriptor($descriptor);
                }

                $childPath = array_merge($childPath, [$field['name']]);
            }

            // Only descend into containers that do NOT persist their own value
            // (e.g. conditional groups). A repeater stores its sub-fields inside
            // its own value, so those sub-fields are not separate descriptors
            // and must not collide with top-level field names.
            if (
                !empty($field['fields'])
                && is_array($field['fields'])
                && $this->isContainerWithoutStorage($field)
            ) {
                $this->collectFields($field['fields'], $childPath);
            }
        }
    }

    /**
     * Determine whether the given field acts as a container without its own storage.
     *
     * @param array<string, mixed> $field
     */
    protected function isContainerWithoutStorage(array $field): bool
    {
        if (empty($field['fields']) || !is_array($field['fields'])) {
            return false;
        }

        return !array_key_exists('storage', $field);
    }

    /**
     * Register a descriptor ensuring no duplicate field names are present.
     */
    protected function addDescriptor(FieldStorageDescriptor $descriptor): void
    {
        if (isset($this->descriptors[$descriptor->field])) {
            throw new RuntimeException(esc_html(sprintf('Duplicate field name detected while building storage map: %s', $descriptor->field)));
        }

        $this->descriptors[$descriptor->field] = $descriptor;
    }

    /**
     * Fetch the descriptor for the given field or throw when absent.
     */
    public function storageFor(string $field): FieldStorageDescriptor
    {
        if (!isset($this->descriptors[$field])) {
            throw new InvalidArgumentException(esc_html(sprintf('Field "%s" is not part of the storage map.', $field)));
        }

        return $this->descriptors[$field];
    }

    /**
     * Determine if a descriptor exists for the specified field.
     */
    public function has(string $field): bool
    {
        return isset($this->descriptors[$field]);
    }

    /**
     * @return string[]
     */
    public function allFieldNames(): array
    {
        return array_keys($this->descriptors);
    }

    /**
     * @return FieldStorageDescriptor[]
     */
    public function descriptors(): array
    {
        return array_values($this->descriptors);
    }
}
