<?php
/**
 * Component reads from Bricks options and index.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Gateways;

use LolaCoreForBricks\Index\IndexBuilder;
use LolaCoreForBricks\Index\IndexReader;
use LolaCoreForBricks\Index\IndexRepository;
use LolaCoreForBricks\Index\IndexSchema;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\CoveragePayload;
use LolaCoreForBricks\Support\CssRegenerator;
use LolaCoreForBricks\Support\PaginatedList;

/**
 * Reads bricks_components with properties and instance counts.
 */
class ComponentGateway
{
    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function getComponents(array $params): array
    {
        $limit  = (int) ($params['limit'] ?? 50);
        $offset = (int) ($params['offset'] ?? 0);

        $components = $this->normalizeComponents(get_option(BricksMetaKeys::OPTION_COMPONENTS, []));
        $page       = PaginatedList::slice($components, $limit, $offset);

        return [
            'components' => $page['items'],
            'total'      => $page['total'],
            'truncated'  => $page['truncated'],
            'limit'      => $page['limit'],
            'offset'     => $page['offset'],
            'coverage'   => CoveragePayload::forResponse(),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function getComponentUsage(array $params): array
    {
        $componentRef = (string) ($params['component'] ?? '');
        $limit        = (int) ($params['limit'] ?? 50);
        $offset       = (int) ($params['offset'] ?? 0);

        if ($componentRef === '') {
            return ['error' => 'missing_component'];
        }

        $resolved = $this->resolveComponentRef($componentRef);
        if ($resolved === null) {
            return [
                'error'     => 'component_not_found',
                'component' => $componentRef,
            ];
        }

        $edges = $this->instanceLocations($resolved['id']);
        $items = [];
        foreach ($edges as $edge) {
            $postId = (int) ($edge['post_id'] ?? 0);
            $items[] = [
                'post_id'      => $postId,
                'post_title'   => $postId > 0 ? (string) get_the_title($postId) : 'globals',
                'area'         => (string) ($edge['area'] ?? ''),
                'element_id'   => $edge['element_id'] ?? null,
                'element_name' => $edge['element_name'] ?? null,
            ];
        }

        $page = PaginatedList::slice($items, $limit, $offset);

        return [
            'component'  => $resolved,
            'instances'  => $page['items'],
            'total'      => $page['total'],
            'truncated'  => $page['truncated'],
            'limit'      => $page['limit'],
            'offset'     => $page['offset'],
            'coverage'   => CoveragePayload::forResponse(),
        ];
    }

    /**
     * @return array{id: string, name: string}|null
     */
    public function resolveComponentRef(string $ref): ?array
    {
        foreach ($this->normalizeComponents(get_option(BricksMetaKeys::OPTION_COMPONENTS, [])) as $component) {
            if ($component['id'] === $ref || strcasecmp($component['name'], $ref) === 0) {
                return ['id' => $component['id'], 'name' => $component['name']];
            }
        }

        return null;
    }

    /**
     * @param mixed $raw
     * @return array<int, array<string, mixed>>
     */
    private function normalizeComponents(mixed $raw): array
    {
        if (!is_array($raw)) {
            return [];
        }

        $items = [];

        foreach ($raw as $component) {
            if (!is_array($component) || empty($component['id'])) {
                continue;
            }

            $id         = (string) $component['id'];
            $properties = is_array($component['properties'] ?? null) ? $component['properties'] : [];
            $elements   = is_array($component['elements'] ?? null) ? $component['elements'] : [];

            $items[] = [
                'id'               => $id,
                'name'             => (string) ($component['name'] ?? $component['label'] ?? $id),
                'properties'       => $this->summarizeProperties($properties),
                'properties_count' => count($properties),
                'element_count'    => count($elements),
                'instance_count'   => $this->instanceCount($id),
            ];
        }

        return $items;
    }

    /**
     * @param array<int|string, mixed> $properties
     * @return array<int, array<string, string>>
     */
    private function summarizeProperties(array $properties): array
    {
        $items = [];
        foreach ($properties as $property) {
            if (!is_array($property)) {
                continue;
            }

            $items[] = [
                'id'   => (string) ($property['id'] ?? ''),
                'name' => (string) ($property['name'] ?? $property['label'] ?? ''),
                'type' => (string) ($property['type'] ?? 'text'),
            ];
        }

        return $items;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function rawComponents(): array
    {
        $raw = get_option(BricksMetaKeys::OPTION_COMPONENTS, []);

        return is_array($raw) ? array_values($raw) : [];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findRaw(string $ref): ?array
    {
        foreach ($this->rawComponents() as $component) {
            if (!is_array($component) || empty($component['id'])) {
                continue;
            }

            $id   = (string) $component['id'];
            $name = (string) ($component['name'] ?? $component['label'] ?? '');
            if ($id === $ref || ($name !== '' && strcasecmp($name, $ref) === 0)) {
                return $component;
            }
        }

        return null;
    }

    /**
     * @return string[]
     */
    public function existingIds(): array
    {
        $ids = [];
        foreach ($this->rawComponents() as $component) {
            if (is_array($component) && !empty($component['id'])) {
                $ids[] = (string) $component['id'];
            }
        }

        return $ids;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function catalogById(): array
    {
        $map = [];
        foreach ($this->rawComponents() as $component) {
            if (is_array($component) && !empty($component['id'])) {
                $map[(string) $component['id']] = $component;
            }
        }

        return $map;
    }

    /**
     * @param array<string, mixed> $component
     * @return array<string, mixed>
     */
    public function saveComponent(array $component, bool $dryRun = false): array
    {
        $id = (string) ($component['id'] ?? '');
        if ($id === '') {
            return [
                'success' => false,
                'message' => 'I need a component ID before I can save that.',
                'error'   => 'missing_component_id',
            ];
        }

        $list  = $this->rawComponents();
        $found = false;
        foreach ($list as $index => $existing) {
            if (is_array($existing) && (string) ($existing['id'] ?? '') === $id) {
                $list[$index] = $component;
                $found        = true;
                break;
            }
        }

        if (!$found) {
            $list[] = $component;
        }

        if ($dryRun) {
            return [
                'success' => true,
                'dry_run' => true,
                'message' => 'Component preview is ready.',
                'id'      => $id,
            ];
        }

        update_option(BricksMetaKeys::OPTION_COMPONENTS, array_values($list));

        return [
            'success' => true,
            'message' => 'The component is saved.',
            'id'      => $id,
        ];
    }

    public function instanceCount(string $componentId): int
    {
        return count($this->instanceLocations($componentId));
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function instanceLocations(string $componentId): array
    {
        $reader = new IndexReader();
        $edges  = [];

        foreach ($reader->usageOf(BricksMetaKeys::REF_COMPONENT, $componentId) as $edge) {
            if ((int) ($edge['post_id'] ?? 0) > 0) {
                $edges[] = $edge;
            }
        }

        if (!$reader->isBuilt()) {
            $edges = $this->mergeEdges($edges, $this->scanInstanceEdges($componentId));
        }

        return $edges;
    }

    /**
     * @return array{instances: int, pages: int, posts: int[], coverage: array<string, mixed>}
     */
    public function instanceImpact(string $componentId): array
    {
        $locations = $this->instanceLocations($componentId);
        $postIds   = [];
        foreach ($locations as $edge) {
            $postId = (int) ($edge['post_id'] ?? 0);
            if ($postId > 0) {
                $postIds[$postId] = $postId;
            }
        }

        return [
            'instances' => count($locations),
            'pages'     => count($postIds),
            'posts'     => array_values($postIds),
            'coverage'  => CoveragePayload::forResponse(),
        ];
    }

    /**
     * @param int[] $postIds
     */
    public function syncIndex(array $postIds, bool $globals = false): void
    {
        if (!IndexSchema::tableExists()) {
            return;
        }

        $builder = new IndexBuilder();
        if ($globals) {
            $builder->indexGlobalScopes();
        }

        (new IndexRepository())->reindexPosts($postIds, $builder);
    }

    /**
     * @param int[] $postIds
     */
    public function regenerateCss(array $postIds): void
    {
        (new CssRegenerator())->afterComponentChange($postIds);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function scanInstanceEdges(string $componentId): array
    {
        $edges = [];
        foreach ($this->candidatePostIds() as $postId) {
            foreach (BricksMetaKeys::pageMetaKeyToArea() as $metaKey => $area) {
                $elements = get_post_meta($postId, $metaKey, true);
                if (!is_array($elements)) {
                    continue;
                }

                foreach ($elements as $element) {
                    if (!is_array($element) || (string) ($element['cid'] ?? '') !== $componentId) {
                        continue;
                    }

                    $edges[] = [
                        'ref_type'     => BricksMetaKeys::REF_COMPONENT,
                        'ref_id'       => $componentId,
                        'post_id'      => $postId,
                        'area'         => $area,
                        'element_id'   => isset($element['id']) ? (string) $element['id'] : null,
                        'element_name' => isset($element['name']) ? (string) $element['name'] : null,
                    ];
                }
            }
        }

        return $edges;
    }

    /**
     * @return int[]
     */
    private function candidatePostIds(): array
    {
        $ids = (new IndexBuilder())->discoverPostIds();
        if ($ids !== []) {
            return $ids;
        }

        $testPosts = $GLOBALS['_test_posts'] ?? null;
        if (is_array($testPosts)) {
            return array_values(array_map('intval', array_keys($testPosts)));
        }

        return [];
    }

    /**
     * @param array<int, array<string, mixed>> $left
     * @param array<int, array<string, mixed>> $right
     * @return array<int, array<string, mixed>>
     */
    private function mergeEdges(array $left, array $right): array
    {
        $unique = [];
        foreach (array_merge($left, $right) as $edge) {
            $key = implode('|', [
                (string) ($edge['post_id'] ?? 0),
                (string) ($edge['area'] ?? ''),
                (string) ($edge['element_id'] ?? ''),
            ]);
            $unique[$key] = $edge;
        }

        return array_values($unique);
    }
}
