<?php
/**
 * Per-page plans and batched writes for clone and bulk composition.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\Elements\ElementTree;
use LolaCoreForBricks\Support\BatchProgress;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\CoveragePayload;
use LolaCoreForBricks\Support\CssTextGuard;

/**
 * Keeps BatchCompositionHandler under the 600-line budget without changing its public API.
 */
trait BatchPagePlanner
{
    /**
     * @param array<int, array<string, mixed>> $branch
     * @param array<string, mixed>             $target
     * @param array<string, mixed>             $params
     * @return array<string, mixed>
     */
    private function planClone(array $branch, array $target, array $params): array
    {
        $postId = (int) ($target['post_id'] ?? 0);
        $area   = (string) ($target['area'] ?? $params['area'] ?? BricksMetaKeys::AREA_CONTENT);
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->pageFailure($postId, $area, 'I could not find that page. It may have been deleted.', 'post_not_found');
        }

        $destTree = ElementTree::fromList($this->gateway()->read($postId, $area));
        $reserved = array_merge(ElementTree::fromList($branch)->ids(), $destTree->ids());
        $idMap    = $this->ids()->remap(ElementTree::fromList($branch)->ids(), $reserved);
        $remapped = ElementTree::fromList($branch)->withRemappedIds($this->ids(), $idMap);
        $applied  = $this->applySubstitutions($remapped, $idMap, $target);
        if (($applied['success'] ?? true) === false) {
            return $this->pageFailure($postId, $area, (string) $applied['message'], (string) $applied['error']);
        }

        $remapped = $applied['list'];
        $merged   = $this->mergeClonedBranch($destTree, $remapped, $target, $params);
        if (isset($merged['error'])) {
            return $this->pageFailure($postId, $area, (string) $merged['message'], (string) $merged['error']);
        }

        return [
            'success'     => true,
            'post_id'     => $postId,
            'title'       => (string) get_the_title($postId),
            'area'        => $area,
            'elements'    => $merged['tree']->toList(),
            'element_ids' => array_values($idMap),
            'id_map'      => $idMap,
            'element_count' => count($idMap),
        ];
    }

    /**
     * @param array<string, mixed> $target
     * @param array<string, mixed> $selector
     * @param array<string, mixed> $content
     * @return array<string, mixed>
     */
    private function planContentUpdate(array $target, array $selector, ?string $classId, array $content): array
    {
        $postId = (int) ($target['post_id'] ?? 0);
        $area   = (string) ($target['area'] ?? BricksMetaKeys::AREA_CONTENT);
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->pageFailure($postId, $area, 'I could not find that page. It may have been deleted.', 'post_not_found');
        }

        if ($content === []) {
            return $this->pageFailure($postId, $area, 'I need the new content for this page.', 'missing_content');
        }

        $mapped = $this->mapper()->toSettings('', $content);
        if (($mapped['success'] ?? false) !== true) {
            return $this->pageFailure($postId, $area, (string) $mapped['message'], (string) ($mapped['error'] ?? 'invalid_content'));
        }

        $cssGuard = CssTextGuard::validateSettings($mapped['settings']);
        if ($cssGuard !== null) {
            return $this->pageFailure($postId, $area, (string) $cssGuard['message'], (string) ($cssGuard['error'] ?? 'forbidden_css'));
        }

        $list    = $this->gateway()->read($postId, $area);
        $matches = $this->selector()->select($list, $selector, $classId);
        if ($matches === []) {
            return $this->pageFailure($postId, $area, 'I could not find a matching element on that page.', 'element_not_found');
        }

        foreach ($list as $index => $element) {
            if (!in_array((string) ($element['id'] ?? ''), $matches, true)) {
                continue;
            }

            $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
            $list[$index]['settings'] = array_merge($settings, $mapped['settings']);
        }

        return [
            'success'       => true,
            'post_id'       => $postId,
            'title'         => (string) get_the_title($postId),
            'area'          => $area,
            'elements'      => $list,
            'element_ids'   => $matches,
            'element_count' => count($matches),
        ];
    }

    /**
     * @param array<string, mixed> $target
     * @param array<string, mixed> $selector
     * @param string[]             $add
     * @param string[]             $remove
     * @return array<string, mixed>
     */
    private function planClassChange(array $target, array $selector, ?string $classId, array $add, array $remove): array
    {
        $postId = (int) ($target['post_id'] ?? 0);
        $area   = (string) ($target['area'] ?? BricksMetaKeys::AREA_CONTENT);
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->pageFailure($postId, $area, 'I could not find that page. It may have been deleted.', 'post_not_found');
        }

        $addIds    = $this->resolveClassIds($add);
        $removeIds = $this->resolveClassIds($remove);
        if (isset($addIds['error'])) {
            return $this->pageFailure($postId, $area, (string) $addIds['message'], 'class_not_found');
        }
        if (isset($removeIds['error'])) {
            return $this->pageFailure($postId, $area, (string) $removeIds['message'], 'class_not_found');
        }

        $list    = $this->gateway()->read($postId, $area);
        $matches = $this->selector()->select($list, $selector, $classId);
        if ($matches === []) {
            return $this->pageFailure($postId, $area, 'I could not find a matching element on that page.', 'element_not_found');
        }

        foreach ($list as $index => $element) {
            if (!in_array((string) ($element['id'] ?? ''), $matches, true)) {
                continue;
            }

            $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
            $current  = $this->stringList($settings['_cssGlobalClasses'] ?? []);
            $current  = array_values(array_unique(array_merge($current, $addIds)));
            $current  = array_values(array_filter($current, static fn (string $id): bool => !in_array($id, $removeIds, true)));
            $settings['_cssGlobalClasses'] = $current;
            $list[$index]['settings']      = $settings;
        }

        return [
            'success'       => true,
            'post_id'       => $postId,
            'title'         => (string) get_the_title($postId),
            'area'          => $area,
            'elements'      => $list,
            'element_ids'   => $matches,
            'element_count' => count($matches),
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $plans
     * @param array<string, mixed>             $params
     * @param array<int, array<string, mixed>> $targets
     * @param array<string, mixed>             $selector
     * @return array<string, mixed>
     */
    private function runPlans(
        string $kind,
        array $plans,
        array $params,
        bool $dryRun,
        array $targets,
        array $selector = []
    ): array {
        $impact = $this->impactFromPlans($plans);
        $coverage = CoveragePayload::forResponse();
        $classSelector = (string) ($selector['class'] ?? '') !== '';

        if ($dryRun) {
            return [
                'success'    => true,
                'dry_run'    => true,
                'message'    => sprintf(
                    'I would change %d page%s: %s. Call again without dry_run to apply the first batch.',
                    $impact['pages'],
                    $impact['pages'] === 1 ? '' : 's',
                    BatchProgress::joinNames($impact['page_names'])
                ),
                'impact'     => $impact,
                'done'       => 0,
                'total'      => count($targets),
                'finished'   => false,
                'elapsed_ms' => 0,
                'coverage'   => $coverage,
                'coverage_note' => $this->coverageNote($classSelector, $coverage),
            ];
        }

        $offset    = BatchProgress::offset($params);
        $batchSize = BatchProgress::batchSize($params);
        $slice     = BatchProgress::slice($plans, $offset, $batchSize);
        $startedAt = microtime(true);
        $written   = [];

        foreach ($slice as $index => $plan) {
            if (($plan['success'] ?? false) !== true) {
                return $this->stoppedResponse(
                    $kind,
                    $plans,
                    $offset + $index,
                    $written,
                    $plan,
                    $coverage,
                    $classSelector,
                    (int) round((microtime(true) - $startedAt) * 1000)
                );
            }

            $writtenResult = $this->gateway()->write(
                (int) $plan['post_id'],
                (string) $plan['area'],
                is_array($plan['elements'] ?? null) ? $plan['elements'] : [],
                false
            );

            if (($writtenResult['success'] ?? false) !== true) {
                return $this->stoppedResponse(
                    $kind,
                    $plans,
                    $offset + $index,
                    $written,
                    array_merge($plan, [
                        'success' => false,
                        'message' => (string) ($writtenResult['message'] ?? 'I could not write that page.'),
                        'error'   => (string) ($writtenResult['error'] ?? 'write_failed'),
                    ]),
                    $coverage,
                    $classSelector,
                    (int) round((microtime(true) - $startedAt) * 1000)
                );
            }

            $written[] = [
                'post_id' => (int) $plan['post_id'],
                'title'   => (string) $plan['title'],
            ];
        }

        $done     = $offset + count($slice);
        $total    = count($plans);
        $finished = $done >= $total;

        return [
            'success'    => true,
            'message'    => $finished
                ? sprintf('I updated all %d pages. Refresh the front to see the changes.', $total)
                : sprintf('I updated %d of %d pages. Call again until finished is true.', $done, $total),
            'impact'     => $impact,
            'written'    => $written,
            'done'       => $done,
            'total'      => $total,
            'finished'   => $finished,
            'offset'     => $done,
            'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
            'coverage'   => $coverage,
            'coverage_note' => $this->coverageNote($classSelector, $coverage),
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $plans
     * @param array<int, array<string, mixed>> $written
     * @param array<string, mixed>             $failed
     * @param array{indexed: int, total: int, complete: bool} $coverage
     * @return array<string, mixed>
     */
    private function stoppedResponse(
        string $kind,
        array $plans,
        int $failedOffset,
        array $written,
        array $failed,
        array $coverage,
        bool $classSelector,
        int $elapsedMs
    ): array {
        unset($kind);

        $title = (string) ($failed['title'] ?? get_the_title((int) ($failed['post_id'] ?? 0)));

        return [
            'success'    => false,
            'message'    => sprintf(
                'I updated %d page%s. I stopped at %s because %s Call again from offset %d after you fix it.',
                count($written),
                count($written) === 1 ? '' : 's',
                $title !== '' ? $title : 'that page',
                rtrim((string) ($failed['message'] ?? 'I could not write that page.'), '.') . '.',
                $failedOffset
            ),
            'error'      => 'page_failed',
            'stopped_at' => [
                'post_id' => (int) ($failed['post_id'] ?? 0),
                'title'   => $title,
                'offset'  => $failedOffset,
                'error'   => (string) ($failed['error'] ?? 'page_failed'),
            ],
            'written'    => $written,
            'remaining'  => array_values(array_map(
                static fn (array $plan): array => [
                    'post_id' => (int) ($plan['post_id'] ?? 0),
                    'title'   => (string) ($plan['title'] ?? ''),
                ],
                array_slice($plans, $failedOffset + 1)
            )),
            'impact'     => $this->impactFromPlans($plans),
            'done'       => count($written),
            'total'      => count($plans),
            'finished'   => false,
            'offset'     => $failedOffset,
            'elapsed_ms' => $elapsedMs,
            'coverage'   => $coverage,
            'coverage_note' => $this->coverageNote($classSelector, $coverage),
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $list
     * @param array<string, string>            $idMap
     * @param array<string, mixed>             $target
     * @return array<string, mixed>
     */
    private function applySubstitutions(array $list, array $idMap, array $target): array
    {
        $subs = [];
        if (isset($target['substitutions']) && is_array($target['substitutions'])) {
            $subs = $target['substitutions'];
        } elseif (isset($target['content']) && is_array($target['content'])) {
            $subs = [['content' => $target['content']]];
        }

        foreach ($subs as $sub) {
            if (!is_array($sub)) {
                continue;
            }

            $content = is_array($sub['content'] ?? null) ? $sub['content'] : [];
            $mapped  = $this->mapper()->toSettings('', $content);
            if (($mapped['success'] ?? false) !== true) {
                return $mapped;
            }

            if ($mapped['settings'] === []) {
                continue;
            }

            $cssGuard = CssTextGuard::validateSettings($mapped['settings']);
            if ($cssGuard !== null) {
                return $cssGuard;
            }

            $matchId = $this->substitutionTarget($list, $idMap, $sub);
            if ($matchId === '') {
                return $this->failure('I could not find the element to change on that copy.', 'element_not_found');
            }

            foreach ($list as $index => $element) {
                if ((string) ($element['id'] ?? '') !== $matchId) {
                    continue;
                }

                $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
                $list[$index]['settings'] = array_merge($settings, $mapped['settings']);
                break;
            }
        }

        return [
            'success' => true,
            'list'    => $list,
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $list
     * @param array<string, string>            $idMap
     * @param array<string, mixed>             $sub
     */
    private function substitutionTarget(array $list, array $idMap, array $sub): string
    {
        $sourceId = (string) ($sub['source_id'] ?? $sub['element_id'] ?? '');
        if ($sourceId !== '') {
            return (string) ($idMap[$sourceId] ?? $sourceId);
        }

        $name = sanitize_key((string) ($sub['name'] ?? ''));
        $preferred = $name !== '' ? [$name] : self::CONTENT_NAMES;

        foreach ($preferred as $wanted) {
            foreach ($list as $element) {
                if (sanitize_key((string) ($element['name'] ?? '')) === $wanted) {
                    return (string) ($element['id'] ?? '');
                }
            }
        }

        return (string) ($list[0]['id'] ?? '');
    }

    /**
     * @param array<int, array<string, mixed>> $remapped
     * @param array<string, mixed>             $target
     * @param array<string, mixed>             $params
     * @return array<string, mixed>
     */
    private function mergeClonedBranch(ElementTree $destTree, array $remapped, array $target, array $params): array
    {
        if ($destTree->toList() === []) {
            return ['tree' => ElementTree::fromList($remapped)];
        }

        $position = (string) ($target['position'] ?? $params['position'] ?? ElementTree::POSITION_CHILD);
        $reference = (string) ($target['parent_id'] ?? $target['reference_id'] ?? $params['parent_id'] ?? $params['reference_id'] ?? '');
        $error     = $destTree->insertBranch($remapped, $position, $reference);
        if ($error !== null) {
            return [
                'error'   => $error,
                'message' => 'I could not place that copy on the page without breaking the tree.',
            ];
        }

        return ['tree' => $destTree];
    }
}
