<?php
/**
 * Component create, properties, structure, instances, and detach.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\BricksEnv;
use LolaCoreForBricks\Elements\ElementTree;
use LolaCoreForBricks\Elements\IdGenerator;
use LolaCoreForBricks\Gateways\ComponentGateway;
use LolaCoreForBricks\Gateways\ElementGateway;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\ComponentNesting;
use LolaCoreForBricks\Support\ComponentProperties;

/**
 * Executes B12 component write abilities.
 */
class ComponentWriteHandler
{
    use ComponentInstanceWrites;

    public function __construct(
        private readonly ?ComponentGateway $components = null,
        private readonly ?ElementGateway $elements = null,
        private readonly ?IdGenerator $ids = null
    ) {
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function createComponentFromElement(array $params, bool $dryRun = false): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $loaded = $this->loadTree($params);
        if (isset($loaded['error'])) {
            return $loaded;
        }

        $elementId = (string) ($params['element_id'] ?? '');
        if ($elementId === '') {
            return $this->failure('I need the element to turn into a component. Which one?', 'missing_element');
        }

        /** @var ElementTree $tree */
        $tree   = $loaded['tree'];
        $source = $tree->findById($elementId);
        if ($source === null) {
            return $this->failure('I could not find that element on this page.', 'element_not_found');
        }

        $branch = $this->rootExtractedBranch($tree->extractBranch($elementId), $elementId);
        if ($branch === []) {
            return $this->failure('I could not find that element on this page.', 'element_not_found');
        }

        $codeGuard = BricksCodeGuard::rejectionResponse($branch);
        if ($codeGuard !== null) {
            return $codeGuard;
        }

        $coherence = ElementTree::fromList($branch)->firstCoherenceError();
        if ($coherence !== null) {
            return $this->treeFailure($coherence);
        }

        $taken         = array_merge($tree->ids(), $this->components()->existingIds());
        $componentId   = $this->ids()->generate($taken);
        $componentName = trim((string) ($params['name'] ?? $params['title'] ?? ''));
        if ($componentName === '') {
            $componentName = (string) ($source['label'] ?? $source['name'] ?? 'Component');
        }

        $remapped = ElementTree::fromList($branch)->withRemappedIds(
            $this->ids(),
            [$elementId => $componentId]
        );
        $remappedTree = ElementTree::fromList($remapped);
        $root         = $remappedTree->findById($componentId);
        if ($root !== null) {
            $root['parent'] = ElementTree::ROOT_PARENT;
            $list           = [];
            foreach ($remappedTree->toList() as $element) {
                $list[] = (string) ($element['id'] ?? '') === $componentId ? $root : $element;
            }
            $remapped = $list;
        }

        $error = ElementTree::fromList($remapped)->firstCoherenceError();
        if ($error !== null) {
            return $this->treeFailure($error);
        }

        $nesting = $this->nestingForComponent([
            'id'       => $componentId,
            'elements' => $remapped,
        ]);

        $component = [
            'id'         => $componentId,
            'name'       => $componentName,
            'elements'   => $remapped,
            'properties' => [],
        ];

        $instance = $source;
        $instance['cid']        = $componentId;
        $instance['children']   = [];
        $instance['properties'] = [];

        if ($dryRun) {
            return [
                'success'      => true,
                'dry_run'      => true,
                'message'      => 'Convert preview is ready.',
                'component_id' => $componentId,
                'name'         => $componentName,
                'instance_id'  => $elementId,
                'element_count'=> count($remapped),
                'nesting'      => $nesting,
            ];
        }

        $saved = $this->components()->saveComponent($component);
        if (($saved['success'] ?? false) !== true) {
            return $saved;
        }

        $remove = array_flip($tree->descendantIds($elementId));
        $list   = [];
        foreach ($tree->toList() as $element) {
            $id = (string) ($element['id'] ?? '');
            if (isset($remove[$id])) {
                continue;
            }

            $list[] = $id === $elementId ? $instance : $element;
        }

        $pageTree = ElementTree::fromList($list);
        $pageError = $pageTree->firstCoherenceError();
        if ($pageError !== null) {
            return $this->treeFailure($pageError);
        }

        $written = $this->elements()->write($loaded['post_id'], $loaded['area'], $pageTree->toList(), false);
        if (($written['success'] ?? false) !== true) {
            return $written;
        }

        $this->components()->syncIndex([$loaded['post_id']], true);

        return [
            'success'       => true,
            'message'       => 'I turned that block into a component. The page still shows the same content.',
            'component_id'  => $componentId,
            'name'          => $componentName,
            'instance_id'   => $elementId,
            'post_id'       => $loaded['post_id'],
            'area'          => $loaded['area'],
            'element_count' => count($remapped),
            'nesting'       => $nesting,
            'impact'        => $this->components()->instanceImpact($componentId),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function updateComponentProperties(array $params, bool $dryRun = false): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $resolved = $this->requireComponent($params);
        if (isset($resolved['error'])) {
            return $resolved;
        }

        $normalized = ComponentProperties::normalize($params['properties'] ?? []);
        if (($normalized['success'] ?? false) !== true) {
            return $normalized;
        }

        $component               = $resolved['component'];
        $component['properties'] = $normalized['properties'];
        $impact                  = $this->components()->instanceImpact($resolved['id']);

        if ($dryRun) {
            return [
                'success'      => true,
                'dry_run'      => true,
                'message'      => $this->impactMessage($impact, 'Update the properties'),
                'component_id' => $resolved['id'],
                'properties'   => $normalized['properties'],
                'impact'       => $impact,
            ];
        }

        $saved = $this->components()->saveComponent($component);
        if (($saved['success'] ?? false) !== true) {
            return $saved;
        }

        $this->components()->regenerateCss($impact['posts']);
        $this->components()->syncIndex($impact['posts'], true);

        return [
            'success'      => true,
            'message'      => sprintf(
                'I updated the properties. This changes %d instance%s.',
                $impact['instances'],
                $impact['instances'] === 1 ? '' : 's'
            ),
            'component_id' => $resolved['id'],
            'properties'   => $normalized['properties'],
            'impact'       => $impact,
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function updateComponent(array $params, bool $dryRun = false): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $resolved = $this->requireComponent($params);
        if (isset($resolved['error'])) {
            return $resolved;
        }

        $component = $resolved['component'];
        $elements  = is_array($params['elements'] ?? null) ? $params['elements'] : null;
        if ($elements !== null) {
            $codeGuard = BricksCodeGuard::rejectionResponse($elements);
            if ($codeGuard !== null) {
                return $codeGuard;
            }

            $error = ElementTree::fromList($elements)->firstCoherenceError();
            if ($error !== null && $elements !== []) {
                return $this->treeFailure($error);
            }

            $component['elements'] = $elements;
        }

        if (isset($params['name']) && trim((string) $params['name']) !== '') {
            $component['name'] = trim((string) $params['name']);
        }

        $impact  = $this->components()->instanceImpact($resolved['id']);
        $nesting = $this->nestingForComponent($component);

        if ($dryRun) {
            return [
                'success'      => true,
                'dry_run'      => true,
                'message'      => $this->impactMessage($impact, 'Edit the component'),
                'component_id' => $resolved['id'],
                'impact'       => $impact,
                'nesting'      => $nesting,
            ];
        }

        $saved = $this->components()->saveComponent($component);
        if (($saved['success'] ?? false) !== true) {
            return $saved;
        }

        $this->components()->regenerateCss($impact['posts']);
        $this->components()->syncIndex($impact['posts'], true);

        return [
            'success'      => true,
            'message'      => sprintf(
                'I updated the component. This changes %d instance%s. Refresh the front to see it.',
                $impact['instances'],
                $impact['instances'] === 1 ? '' : 's'
            ),
            'component_id' => $resolved['id'],
            'impact'       => $impact,
            'nesting'      => $nesting,
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function requireComponent(array $params): array
    {
        $ref = (string) ($params['component'] ?? $params['component_id'] ?? $params['id'] ?? '');
        if ($ref === '') {
            return $this->failure('I need the component. Which one?', 'missing_component');
        }

        $component = $this->components()->findRaw($ref);
        if ($component === null) {
            return $this->failure('I could not find that component in the library.', 'component_not_found');
        }

        return [
            'id'        => (string) $component['id'],
            'component' => $component,
        ];
    }

    /**
     * A extracted branch is a new tree: its root parent must be 0 before coherence checks.
     *
     * @param array<int, array<string, mixed>> $branch
     * @return array<int, array<string, mixed>>
     */
    private function rootExtractedBranch(array $branch, string $rootId): array
    {
        $rooted = [];
        foreach ($branch as $element) {
            if ((string) ($element['id'] ?? '') === $rootId) {
                $element['parent'] = ElementTree::ROOT_PARENT;
            }
            $rooted[] = $element;
        }

        return $rooted;
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function loadTree(array $params): array
    {
        $postId = absint($params['post_id'] ?? $params['postId'] ?? 0);
        $area   = $this->elements()->resolveArea((string) ($params['area'] ?? BricksMetaKeys::AREA_CONTENT));
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->failure('I could not find that page. It may have been deleted.', 'post_not_found');
        }

        return [
            'post_id' => $postId,
            'area'    => $area,
            'tree'    => ElementTree::fromList($this->elements()->read($postId, $area)),
        ];
    }

    /**
     * @param array<string, mixed> $component
     * @return array{depth: int, at_limit: bool, over_limit: bool}
     */
    private function nestingForComponent(array $component): array
    {
        $catalog = $this->components()->catalogById();
        $id      = (string) ($component['id'] ?? '');
        if ($id !== '') {
            $catalog[$id] = $component;
        }

        return ComponentNesting::evaluate(ComponentNesting::internalDepth($component, $catalog));
    }

    /**
     * @param array<string, mixed> $component
     * @return array{depth: int, at_limit: bool, over_limit: bool}
     */
    private function nestingAtPlacement(
        ElementTree $tree,
        string $position,
        string $referenceId,
        array $component
    ): array {
        $anchorId = $referenceId;
        if ($position !== ElementTree::POSITION_CHILD && $referenceId !== '') {
            $reference = $tree->findById($referenceId);
            $parent    = $reference['parent'] ?? ElementTree::ROOT_PARENT;
            $anchorId  = ($parent === ElementTree::ROOT_PARENT || $parent === 0 || $parent === '0')
                ? ''
                : (string) $parent;
        }

        $ancestor = $anchorId === '' ? 0 : ComponentNesting::ancestorDepth($tree, $anchorId);
        $internal = ComponentNesting::internalDepth($component, $this->components()->catalogById());
        $depth    = $ancestor + 1 + $internal;

        return ComponentNesting::evaluate($depth);
    }

    /**
     * @param array<string, mixed> $component
     */
    private function componentRootName(array $component): string
    {
        $id       = (string) ($component['id'] ?? '');
        $elements = is_array($component['elements'] ?? null) ? $component['elements'] : [];
        foreach ($elements as $element) {
            if (is_array($element) && (string) ($element['id'] ?? '') === $id) {
                return (string) ($element['name'] ?? 'div');
            }
        }

        foreach ($elements as $element) {
            if (is_array($element) && (($element['parent'] ?? 0) === 0 || ($element['parent'] ?? '') === '0')) {
                return (string) ($element['name'] ?? 'div');
            }
        }

        return 'div';
    }

    /**
     * @param array<string, mixed> $impact
     */
    private function impactMessage(array $impact, string $action): string
    {
        $instances = (int) ($impact['instances'] ?? 0);
        $pages     = (int) ($impact['pages'] ?? 0);

        if ($instances === 0) {
            return $action . ' — no instances yet.';
        }

        return sprintf(
            '%s — this changes %d instance%s on %d page%s.',
            $action,
            $instances,
            $instances === 1 ? '' : 's',
            $pages,
            $pages === 1 ? '' : 's'
        );
    }

    /**
     * @param array<string, mixed> $params
     */
    private function insertReference(array $params, string $position): string
    {
        if ($position === ElementTree::POSITION_CHILD) {
            return (string) ($params['parent_id'] ?? $params['reference_id'] ?? '');
        }

        return (string) ($params['reference_id'] ?? $params['parent_id'] ?? '');
    }

    /**
     * @param array<string, mixed> $params
     */
    private function position(array $params): string
    {
        $position = strtolower((string) ($params['position'] ?? ElementTree::POSITION_CHILD));

        return in_array($position, [
            ElementTree::POSITION_CHILD,
            ElementTree::POSITION_BEFORE,
            ElementTree::POSITION_AFTER,
        ], true) ? $position : ElementTree::POSITION_CHILD;
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>|null
     */
    private function environmentGuard(array $params): ?array
    {
        if ($rejection = BricksCodeGuard::rejectionResponse($params)) {
            return $rejection;
        }

        if (!BricksEnv::userCanReadDesignSystem()) {
            return $this->failure('I need administrator access to change Bricks components.', 'forbidden');
        }

        if (!BricksEnv::isAvailable() || !BricksEnv::meetsFloor()) {
            return $this->failure(
                'Bricks is not available on this site yet. Activate Bricks '
                . BricksEnv::FLOOR_VERSION . ' or newer, then try again.',
                'bricks_unavailable'
            );
        }

        return null;
    }

    /**
     * @return array{success: false, message: string, error: string}
     */
    private function treeFailure(string $error): array
    {
        $messages = [
            'element_not_found'   => 'I could not find that element on this page.',
            'reference_not_found' => 'I could not find the placement target on this page.',
            'cycle'               => 'That change would nest the element inside itself. I left the tree as it is.',
            'invalid_id'          => 'I could not create a unique element ID. Try again.',
            'missing_parent'      => 'The page tree is inconsistent (a parent is missing). I will not write that.',
            'missing_child'       => 'The page tree is inconsistent (a child is missing). I will not write that.',
            'parent_mismatch'     => 'The page tree is inconsistent (parent and children do not match). I will not write that.',
            'orphan'              => 'The page tree is inconsistent (an element would be left orphaned). I will not write that.',
            'duplicate_id'        => 'The page tree is inconsistent (duplicate IDs). I will not write that.',
            'missing_id'          => 'The page tree is inconsistent (an element has no ID). I will not write that.',
        ];

        return $this->failure(
            $messages[$error] ?? 'I could not update the tree without breaking it.',
            $error
        );
    }

    /**
     * @return array{success: false, message: string, error: string}
     */
    private function failure(string $message, string $error): array
    {
        return [
            'success' => false,
            'message' => $message,
            'error'   => $error,
        ];
    }

    private function components(): ComponentGateway
    {
        return $this->components ?? new ComponentGateway();
    }

    private function elements(): ElementGateway
    {
        return $this->elements ?? new ElementGateway();
    }

    private function ids(): IdGenerator
    {
        return $this->ids ?? new IdGenerator();
    }
}
