<?php
/**
 * Multi-page clone and bulk writes with aggregated impact and batched execution.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\BricksEnv;
use LolaCoreForBricks\Elements\ElementTree;
use LolaCoreForBricks\Elements\IdGenerator;
use LolaCoreForBricks\Gateways\ClassGateway;
use LolaCoreForBricks\Gateways\ElementGateway;
use LolaCoreForBricks\Support\BatchProgress;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\CoveragePayload;
use LolaCoreForBricks\Support\CssTextGuard;
use LolaCoreForBricks\Support\ElementContentMapper;
use LolaCoreForBricks\Support\ElementSelector;

/**
 * Executes B15 cloneStructure, bulkUpdateContent, and bulkApplyClasses.
 */
class BatchCompositionHandler
{
    use BatchPagePlanner;

    /**
     * @var string[]
     */
    private const CONTENT_NAMES = ['heading', 'text', 'button', 'text-basic'];

    public function __construct(
        private readonly ?ElementGateway $gateway = null,
        private readonly ?IdGenerator $ids = null,
        private readonly ?ElementContentMapper $mapper = null,
        private readonly ?ElementSelector $selector = null,
        private readonly ?ClassGateway $classes = null
    ) {
    }

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

        $source = is_array($params['source'] ?? null) ? $params['source'] : [];
        $sourceId = absint($source['post_id'] ?? $params['source_post_id'] ?? 0);
        $sourceArea = (string) ($source['area'] ?? $params['area'] ?? BricksMetaKeys::AREA_CONTENT);
        $elementId = (string) ($source['element_id'] ?? $params['element_id'] ?? '');

        if ($sourceId <= 0 || get_post($sourceId) === null) {
            return $this->failure('I could not find the page to copy from.', 'source_not_found');
        }

        $sourceTree = ElementTree::fromList($this->gateway()->read($sourceId, $sourceArea));
        $branch     = $elementId === ''
            ? $sourceTree->toList()
            : $sourceTree->extractBranch($elementId);

        if ($branch === []) {
            return $this->failure(
                $elementId === ''
                    ? 'That page area is empty. There is nothing to copy.'
                    : 'I could not find that element on the source page.',
                'source_empty'
            );
        }

        if ($rejection = BricksCodeGuard::rejectionResponse($branch)) {
            return $rejection;
        }

        $targets = $this->normalizeTargets($params);
        if ($targets === []) {
            return $this->failure('I need the pages to copy to. Which ones?', 'missing_targets');
        }

        $plans = [];
        foreach ($targets as $target) {
            $plans[] = $this->planClone($branch, $target, $params);
        }

        return $this->runPlans('clone', $plans, $params, $dryRun, $targets);
    }

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

        $targets = $this->normalizeTargets($params);
        if ($targets === []) {
            return $this->failure('I need the pages to update. Which ones?', 'missing_targets');
        }

        $selector = is_array($params['selector'] ?? null) ? $params['selector'] : [];
        if ($this->selector()->isEmpty($selector)) {
            return $this->failure(
                'I need a class, an element name, or a position to find what to change.',
                'missing_selector'
            );
        }

        $classId = $this->resolveSelectorClass($selector);
        if (is_array($classId) && ($classId['success'] ?? true) === false) {
            return $classId;
        }

        $defaultContent = is_array($params['content'] ?? null) ? $params['content'] : [];
        $plans          = [];
        foreach ($targets as $target) {
            $plans[] = $this->planContentUpdate(
                $target,
                $selector,
                is_string($classId) ? $classId : null,
                is_array($target['content'] ?? null) ? $target['content'] : $defaultContent
            );
        }

        return $this->runPlans('content', $plans, $params, $dryRun, $targets, $selector);
    }

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

        $targets = $this->normalizeTargets($params);
        if ($targets === []) {
            return $this->failure('I need the pages to update. Which ones?', 'missing_targets');
        }

        $selector = is_array($params['selector'] ?? null) ? $params['selector'] : [];
        if ($this->selector()->isEmpty($selector)) {
            return $this->failure(
                'I need a class, an element name, or a position to find what to change.',
                'missing_selector'
            );
        }

        $add    = $this->stringList($params['add'] ?? $params['classes'] ?? []);
        $remove = $this->stringList($params['remove'] ?? []);
        if ($add === [] && $remove === []) {
            return $this->failure('I need the classes to add or remove.', 'missing_classes');
        }

        $classId = $this->resolveSelectorClass($selector);
        if (is_array($classId) && ($classId['success'] ?? true) === false) {
            return $classId;
        }

        $plans = [];
        foreach ($targets as $target) {
            $plans[] = $this->planClassChange(
                $target,
                $selector,
                is_string($classId) ? $classId : null,
                $add,
                $remove
            );
        }

        return $this->runPlans('classes', $plans, $params, $dryRun, $targets, $selector);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<int, array<string, mixed>>
     */
    private function normalizeTargets(array $params): array
    {
        $raw = $params['targets'] ?? [];
        if (!is_array($raw)) {
            return [];
        }

        $targets = [];
        foreach ($raw as $item) {
            if (is_numeric($item)) {
                $targets[] = ['post_id' => (int) $item];
                continue;
            }

            if (is_array($item) && absint($item['post_id'] ?? $item['id'] ?? 0) > 0) {
                $item['post_id'] = absint($item['post_id'] ?? $item['id'] ?? 0);
                $targets[]       = $item;
            }
        }

        return $targets;
    }

    /**
     * @param array<int, array<string, mixed>> $plans
     * @return array<string, mixed>
     */
    private function impactFromPlans(array $plans): array
    {
        $names   = [];
        $ids     = [];
        $details = [];
        $elements = 0;

        foreach ($plans as $plan) {
            $postId = (int) ($plan['post_id'] ?? 0);
            $title  = (string) ($plan['title'] ?? ($postId > 0 ? get_the_title($postId) : ''));
            $count  = (int) ($plan['element_count'] ?? count($plan['element_ids'] ?? []));
            $names[] = $title;
            $ids[]   = $postId;
            $elements += $count;
            $details[] = [
                'post_id'       => $postId,
                'title'         => $title,
                'area'          => (string) ($plan['area'] ?? BricksMetaKeys::AREA_CONTENT),
                'element_ids'   => is_array($plan['element_ids'] ?? null) ? $plan['element_ids'] : [],
                'element_count' => $count,
                'ok'            => ($plan['success'] ?? false) === true,
            ];
        }

        return [
            'pages'        => count($plans),
            'page_names'   => $names,
            'page_ids'     => $ids,
            'elements'     => $elements,
            'pages_detail' => $details,
        ];
    }

    /**
     * @param array<string, mixed> $selector
     * @return string|array<string, mixed>|null
     */
    private function resolveSelectorClass(array $selector): string|array|null
    {
        $ref = (string) ($selector['class'] ?? '');
        if ($ref === '') {
            return null;
        }

        $resolved = $this->classes()->resolveClassRef($ref);
        if ($resolved === null) {
            return $this->failure('I could not find that global class. Check the name and try again.', 'class_not_found');
        }

        return $resolved['id'];
    }

    /**
     * @param string[] $refs
     * @return string[]|array{error: string, message: string}
     */
    private function resolveClassIds(array $refs): array
    {
        $ids = [];
        foreach ($refs as $ref) {
            $resolved = $this->classes()->resolveClassRef($ref);
            if ($resolved === null) {
                return [
                    'error'   => 'class_not_found',
                    'message' => 'I could not find that global class. Check the name and try again.',
                ];
            }

            $ids[] = $resolved['id'];
        }

        return $ids;
    }

    /**
     * @param array{indexed: int, total: int, complete: bool} $coverage
     */
    private function coverageNote(bool $classSelector, array $coverage): string
    {
        if (!$classSelector || ($coverage['complete'] ?? false) === true) {
            return '';
        }

        return 'The design index is incomplete. I only searched the pages you listed.';
    }

    /**
     * @return array<string, mixed>
     */
    private function pageFailure(int $postId, string $area, string $message, string $error): array
    {
        return [
            'success' => false,
            'post_id' => $postId,
            'title'   => $postId > 0 ? (string) get_the_title($postId) : '',
            'area'    => $area,
            'message' => $message,
            'error'   => $error,
            'element_ids' => [],
            'element_count' => 0,
        ];
    }

    /**
     * @param mixed $value
     * @return string[]
     */
    private function stringList(mixed $value): array
    {
        if (is_string($value) && $value !== '') {
            return [$value];
        }

        if (!is_array($value)) {
            return [];
        }

        $items = [];
        foreach ($value as $item) {
            $item = trim((string) $item);
            if ($item !== '') {
                $items[] = $item;
            }
        }

        return $items;
    }

    /**
     * @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 pages in bulk.', '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 failure(string $message, string $error): array
    {
        return [
            'success' => false,
            'message' => $message,
            'error'   => $error,
        ];
    }

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

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

    private function mapper(): ElementContentMapper
    {
        return $this->mapper ?? new ElementContentMapper();
    }

    private function selector(): ElementSelector
    {
        return $this->selector ?? new ElementSelector();
    }

    private function classes(): ClassGateway
    {
        return $this->classes ?? new ClassGateway();
    }
}
