<?php

namespace Mtc\Plugins\BlockManager\Services;

use Mtc\Plugins\BlockManager\Models\BlockManagerBlock;
use Mtc\Plugins\BlockManager\Support\InstanceClass;

/**
 * Produces the Twig template body for a composed block, plus the corresponding
 * BlockRegistry field list. Both outputs share `computePlan()` so the generated
 * `{% include … with { … } %}` bindings line up 1:1 with the BlockRegistry fields
 * declared in the JSON.
 *
 * Phase 5 adds Fields panel awareness:
 *   - renames (auto-derived name → user name) apply to top-level fields and the
 *     `fields.{X}` bindings in the generated Twig
 *   - repeater grouping wraps the matching `{% include %}` lines in
 *     `{% for {singular} in {name} %} … {% endfor %}`, with bindings switched
 *     from `fields.{X}` to `{singular}.{input}`
 *   - `custom` adds element-less fields to the BlockRegistry JSON
 *   - `order` and `required` override the auto-derived ordering and required flag
 *
 * Fields panel is optional — absent state means Phase 4 auto-derivation.
 */
class TwigGenerator
{
    public function __construct(private ElementCatalogueService $catalogue)
    {
    }

    public function generate(BlockManagerBlock $block): string
    {
        $plan = $this->computePlan($block);
        if ($plan === []) {
            return "{# block has no elements #}\n";
        }

        $lines = [];
        $currentRepeaterName = null;

        foreach ($plan as $node) {
            $newRepeaterName = $node['repeater']['name'] ?? null;

            if ($newRepeaterName !== $currentRepeaterName) {
                if ($currentRepeaterName !== null) {
                    $lines[] = '{% endfor %}';
                }
                if ($newRepeaterName !== null) {
                    $singular = $node['repeater']['singular'];
                    $lines[] = "{% for {$singular} in {$newRepeaterName} %}";
                }
                $currentRepeaterName = $newRepeaterName;
            }

            $singular = $node['repeater']['singular'] ?? null;
            $this->emitNode($node, $lines, $singular);
        }

        if ($currentRepeaterName !== null) {
            $lines[] = '{% endfor %}';
        }

        return implode("\n", $lines) . "\n";
    }

    /**
     * Emit Twig lines for one plan node. Container nodes emit a wrapper
     * `<div class="bm-container bm-ci-…">` around their recursively-emitted
     * children; leaf nodes emit a single `{% include %}` with field bindings.
     *
     * @param array<string, mixed> $node
     * @param array<int, string>   $lines  appended to by reference
     * @param string|null          $singular  repeater loop variable for leaf bindings (top-level only)
     */
    private function emitNode(array $node, array &$lines, ?string $singular): void
    {
        if (!empty($node['is_container'])) {
            $class = trim('bm-container ' . InstanceClass::for((string) ($node['instance']['instance_id'] ?? '')));
            $lines[] = '<div class="' . $class . '">';
            foreach ($node['children'] ?? [] as $child) {
                // Container children never participate in repeater grouping (v1),
                // so they always bind from `fields.*`, never a loop variable.
                $this->emitNode($child, $lines, null);
            }
            $lines[] = '</div>';
            return;
        }

        $type = $node['instance']['element_type'];
        $variant = $node['instance']['variant'] ?? ($node['manifest']['default_variant'] ?? 'default');
        // Resolved against the loader root registered by
        // BlockManagerServiceProvider::boot() (plugins/BlockManager/elements/).
        $partial = "{$type}/variants/{$variant}.twig";

        $bindings = [];
        if ($singular !== null) {
            foreach ($node['field_map'] as $inputName => $_) {
                $bindings[] = "{$inputName}: {$singular}.{$inputName}";
            }
        } else {
            foreach ($node['field_map'] as $inputName => $fieldName) {
                $bindings[] = "{$inputName}: fields.{$fieldName}";
            }
        }

        $withClause = $bindings === [] ? '' : ' with { ' . implode(', ', $bindings) . ' }';
        $lines[] = "{% include \"{$partial}\"{$withClause} %}";
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function deriveFields(BlockManagerBlock $block): array
    {
        // Flatten the (possibly nested) plan tree depth-first so nested
        // container children's inputs still register as BlockRegistry fields.
        $plan = $this->flattenPlan($this->computePlan($block));
        $panel = $this->panel($block);

        $ungroupedFields = [];                  // keyed by name, walk order preserved
        $repeaterSubFields = [];                // repName => list of sub-field arrays
        $repeaterFirstIndex = [];               // repName => plan index of first appearance
        $repeaterConfigs = [];                  // repName => raw config

        foreach ($plan as $i => $step) {
            $inputByName = [];
            foreach ($step['manifest']['inputs'] ?? [] as $input) {
                if (isset($input['name'])) {
                    $inputByName[$input['name']] = $input;
                }
            }

            if ($step['repeater'] !== null) {
                $repName = $step['repeater']['name'];
                if (!isset($repeaterFirstIndex[$repName])) {
                    $repeaterFirstIndex[$repName] = $i;
                    $repeaterConfigs[$repName] = $step['repeater']['config'] ?? [];
                }
                $repeaterSubFields[$repName] = $repeaterSubFields[$repName] ?? [];

                foreach ($step['field_map'] as $inputName => $fieldName) {
                    $input = $inputByName[$inputName] ?? null;
                    if ($input === null) {
                        continue;
                    }
                    $repeaterSubFields[$repName][] = $this->buildField($fieldName, $input);
                }
            } else {
                foreach ($step['field_map'] as $inputName => $fieldName) {
                    $input = $inputByName[$inputName] ?? null;
                    if ($input === null) {
                        continue;
                    }
                    $ungroupedFields[$fieldName] = $this->buildField($fieldName, $input);
                }
            }
        }

        // Merge ungrouped + repeater fields in plan order, emitting each repeater
        // once at the position of its first member.
        $merged = [];
        $emittedRepeater = [];
        foreach ($plan as $i => $step) {
            if ($step['repeater'] !== null) {
                $repName = $step['repeater']['name'];
                if (!isset($emittedRepeater[$repName])) {
                    $emittedRepeater[$repName] = true;
                    $cfg = $repeaterConfigs[$repName] ?? [];
                    $merged[] = [
                        'name'          => $repName,
                        'label'         => $cfg['label'] ?? $this->humanize($repName),
                        'type'          => 'repeater',
                        'order_index'   => 0,
                        'required'      => !empty($cfg['required']),
                        'repeater_min'  => $cfg['repeater_min'] ?? 0,
                        'repeater_max'  => $cfg['repeater_max'] ?? 100,
                        'repeater_text' => $cfg['repeater_text'] ?? 'Add Row',
                        'sub_fields'    => $repeaterSubFields[$repName] ?? [],
                    ];
                }
                continue;
            }

            foreach ($step['field_map'] as $inputName => $fieldName) {
                if (isset($ungroupedFields[$fieldName])) {
                    $merged[] = $ungroupedFields[$fieldName];
                    unset($ungroupedFields[$fieldName]);
                }
            }
        }

        // Custom fields appended in their declared order.
        foreach ($panel['custom'] ?? [] as $custom) {
            if (!is_array($custom) || empty($custom['name'])) {
                continue;
            }
            $options = $custom['options'] ?? null;
            if (is_array($options)) {
                $options = json_encode($options);
            }
            $merged[] = [
                'name'          => $custom['name'],
                'label'         => $custom['label'] ?? $this->humanize($custom['name']),
                'type'          => $custom['type'] ?? 'text',
                'order_index'   => 0,
                'required'      => !empty($custom['required']),
                'options'       => $options,
                'default_value' => $custom['default_value'] ?? $custom['default'] ?? null,
            ];
        }

        // Required overrides — keyed on the (post-rename) top-level field name.
        $requiredOverrides = is_array($panel['required'] ?? null) ? $panel['required'] : [];
        foreach ($merged as &$field) {
            if (array_key_exists($field['name'], $requiredOverrides)) {
                $field['required'] = (bool) $requiredOverrides[$field['name']];
            }
        }
        unset($field);

        // Apply panel.order — listed names first in that order, unlisted names keep
        // their relative position at the tail.
        $order = is_array($panel['order'] ?? null) ? $panel['order'] : [];
        if ($order !== []) {
            $byName = [];
            foreach ($merged as $field) {
                $byName[$field['name']] = $field;
            }
            $ordered = [];
            foreach ($order as $name) {
                if (isset($byName[$name])) {
                    $ordered[] = $byName[$name];
                    unset($byName[$name]);
                }
            }
            foreach ($byName as $field) {
                $ordered[] = $field;
            }
            $merged = $ordered;
        }

        // Renumber order_index after all rearrangement.
        foreach ($merged as $i => &$field) {
            $field['order_index'] = $i;
        }
        unset($field);

        return $merged;
    }

    /**
     * Walk the saved structure, resolve manifests, apply Fields panel state.
     * Returns a tree: container nodes carry a recursively-planned `children`
     * list and `is_container => true`. Non-container blocks plan exactly as
     * before (flat top-level list, no `children`).
     *
     * @return array<int, array{instance: array, manifest: array, field_map: array<string,string>, repeater: ?array, children?: array, is_container?: bool}>
     */
    private function computePlan(BlockManagerBlock $block): array
    {
        $structure = $block->structure_json ?? [];
        $elements = (is_array($structure) && isset($structure['elements']) && is_array($structure['elements']))
            ? $structure['elements']
            : [];

        $panel = $this->panel($block);
        $renames = is_array($panel['renames'] ?? null) ? $panel['renames'] : [];
        $instanceToRepeater = $this->buildInstanceToRepeater($panel);

        $takenTop = [];
        $takenInRepeater = [];

        return $this->planLevel($elements, true, $renames, $instanceToRepeater, $takenTop, $takenInRepeater);
    }

    /**
     * Plan one level of the instance tree. Repeater grouping applies to the
     * top level only; container children are planned with `$topLevel = false`
     * (no repeater membership in v1) while sharing `$takenTop` so generated
     * field names stay globally unique.
     *
     * @param array<int, mixed>   $elements
     * @param array<string,string> $renames
     * @param array<string, array> $instanceToRepeater
     * @param array<string, bool>  $takenTop
     * @param array<string, array<string,bool>> $takenInRepeater
     * @return array<int, array<string, mixed>>
     */
    private function planLevel(
        array $elements,
        bool $topLevel,
        array $renames,
        array $instanceToRepeater,
        array &$takenTop,
        array &$takenInRepeater
    ): array {
        $plan = [];

        foreach ($elements as $instance) {
            if (!is_array($instance)) {
                continue;
            }
            $type = $instance['element_type'] ?? null;
            if (!$type) {
                continue;
            }
            $manifest = $this->catalogue->get($type);
            if ($manifest === null) {
                continue;
            }

            $repeater = $topLevel ? ($instanceToRepeater[$instance['instance_id'] ?? ''] ?? null) : null;

            if ($repeater !== null) {
                $repName = $repeater['name'];
                $takenInRepeater[$repName] = $takenInRepeater[$repName] ?? [];
                $fieldMap = $this->deriveFieldMapForRepeater($manifest, $takenInRepeater[$repName]);
            } else {
                $fieldMap = $this->deriveFieldMap($instance, $manifest, $takenTop);
                // Apply renames at the top level only.
                if ($topLevel) {
                    foreach ($fieldMap as $inputName => $auto) {
                        if (isset($renames[$auto])) {
                            $fieldMap[$inputName] = $renames[$auto];
                        }
                    }
                }
            }

            $node = [
                'instance'  => $instance,
                'manifest'  => $manifest,
                'field_map' => $fieldMap,
                'repeater'  => $repeater,
            ];

            if (!empty($manifest['accepts_children'])) {
                $node['is_container'] = true;
                $children = is_array($instance['children'] ?? null) ? $instance['children'] : [];
                $node['children'] = $this->planLevel(
                    $children,
                    false,
                    $renames,
                    $instanceToRepeater,
                    $takenTop,
                    $takenInRepeater
                );
            }

            $plan[] = $node;
        }

        return $plan;
    }

    /**
     * Depth-first flatten of the plan tree into an ordered list of nodes
     * (parents before their children). Used by deriveFields(), which only
     * cares about each node's inputs, not the nesting.
     *
     * @param array<int, array<string, mixed>> $plan
     * @return array<int, array<string, mixed>>
     */
    private function flattenPlan(array $plan): array
    {
        $flat = [];
        foreach ($plan as $node) {
            $flat[] = $node;
            if (!empty($node['children'])) {
                $flat = array_merge($flat, $this->flattenPlan($node['children']));
            }
        }
        return $flat;
    }

    /**
     * @param array<string, mixed> $instance
     * @param array<string, mixed> $manifest
     * @param array<string, bool>  $taken
     * @return array<string, string>
     */
    private function deriveFieldMap(array $instance, array $manifest, array &$taken): array
    {
        $base = $instance['field_name'] ?? $instance['element_type'] ?? 'field';
        $map = [];

        foreach ($manifest['inputs'] ?? [] as $input) {
            $name = $input['name'] ?? null;
            if (!$name) {
                continue;
            }
            $candidate = "{$base}_{$name}";
            $n = 2;
            while (isset($taken[$candidate])) {
                $candidate = "{$base}_{$name}_{$n}";
                $n++;
            }
            $taken[$candidate] = true;
            $map[$name] = $candidate;
        }

        return $map;
    }

    /**
     * Sub-field name scheme inside a repeater: use the input name directly,
     * collision-suffix on repeat. Keeps `slide.text` binding semantics aligned
     * with sub-field naming.
     *
     * @param array<string, mixed> $manifest
     * @param array<string, bool>  $taken
     * @return array<string, string>
     */
    private function deriveFieldMapForRepeater(array $manifest, array &$taken): array
    {
        $map = [];
        foreach ($manifest['inputs'] ?? [] as $input) {
            $name = $input['name'] ?? null;
            if (!$name) {
                continue;
            }
            $candidate = $name;
            $n = 2;
            while (isset($taken[$candidate])) {
                $candidate = $name . '_' . $n;
                $n++;
            }
            $taken[$candidate] = true;
            $map[$name] = $candidate;
        }
        return $map;
    }

    /**
     * @return array<string, mixed>
     */
    private function panel(BlockManagerBlock $block): array
    {
        $structure = $block->structure_json ?? [];
        $panel = is_array($structure) ? ($structure['fields_panel'] ?? []) : [];
        return is_array($panel) ? $panel : [];
    }

    /**
     * @return array<string, array{name: string, singular: string, config: array<string,mixed>}>
     */
    private function buildInstanceToRepeater(array $panel): array
    {
        $out = [];
        foreach ($panel['repeaters'] ?? [] as $rep) {
            if (!is_array($rep) || empty($rep['name']) || empty($rep['instance_ids']) || !is_array($rep['instance_ids'])) {
                continue;
            }
            $name = (string) $rep['name'];
            $singular = (string) ($rep['singular'] ?? rtrim($name, 's'));
            if ($singular === '') {
                $singular = $name . '_row';
            }
            foreach ($rep['instance_ids'] as $iid) {
                if (is_string($iid) || is_int($iid)) {
                    $out[(string) $iid] = [
                        'name'     => $name,
                        'singular' => $singular,
                        'config'   => $rep,
                    ];
                }
            }
        }
        return $out;
    }

    /**
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    private function buildField(string $name, array $input): array
    {
        return [
            'name'          => $name,
            'label'         => $this->humanize($name),
            'type'          => $input['type'],
            'order_index'   => 0,
            'required'      => !empty($input['required']),
            'options'       => isset($input['options']) ? json_encode($input['options']) : null,
            'default_value' => $input['default'] ?? null,
        ];
    }

    private function humanize(string $name): string
    {
        return ucwords(str_replace('_', ' ', $name));
    }
}
