<?php
/**
 * Block Sync Service
 * Handles export, import, and diff of block definitions as JSON files
 */
namespace Mtc\Modules\BlockRegistry\Services;

use Mtc\Modules\BlockRegistry\Models\BlockRegistry;
use Mtc\Modules\BlockRegistry\Models\BlockRegistryCategory;
use Mtc\Modules\BlockRegistry\Models\BlockRegistryField;
use Mtc\Modules\BlockRegistry\Models\BlockRegistryTemplateAssignment;
use Illuminate\Support\Facades\DB;

class BlockSyncService
{
    protected BlockRegistryService $registryService;

    /**
     * Memo of decoded example_data keyed by slug, so a single preview/prefill
     * doesn't re-read and re-decode the same JSON file 2-3x. Keyed presence
     * (not value) signals "loaded" since null is a valid cached result.
     */
    protected array $exampleDataCache = [];

    public function __construct(BlockRegistryService $registryService)
    {
        $this->registryService = $registryService;
    }

    /**
     * Get the directory where JSON block files are stored
     */
    public function getBlocksDirectory(): string
    {
        return base_path('modules/BlockRegistry/blocks');
    }

    /**
     * Read a block's optional `example_data` from its JSON file.
     *
     * `example_data` is a file-only concern (the DB never stores it): one
     * sample instance in `entity_blocks.data` storage shape, used to drive the
     * /component + /example-blocks previews and the admin "Use example content"
     * prefill. Returns null when the file or key is absent. Exception-safe.
     */
    public function getExampleData(string $slug): ?array
    {
        if (array_key_exists($slug, $this->exampleDataCache)) {
            return $this->exampleDataCache[$slug];
        }

        $filePath = $this->getBlocksDirectory() . '/' . $slug . '.json';
        if (!is_file($filePath)) {
            return $this->exampleDataCache[$slug] = null;
        }

        $data = json_decode((string) file_get_contents($filePath), true);
        if (!is_array($data) || !isset($data['example_data']) || !is_array($data['example_data'])) {
            return $this->exampleDataCache[$slug] = null;
        }

        return $this->exampleDataCache[$slug] = $data['example_data'];
    }

    /**
     * Whether a block has authored example content.
     */
    public function hasExampleData(string $slug): bool
    {
        return $this->getExampleData($slug) !== null;
    }

    // -------------------------------------------------------------------------
    // EXPORT
    // -------------------------------------------------------------------------

    /**
     * Convert a block model to a JSON-safe array
     */
    public function blockToArray(BlockRegistry $block): array
    {
        $block->load(['fields.subFields', 'category', 'templateAssignments']);

        $data = [
            'name' => $block->name,
            'slug' => $block->slug,
            'description' => $block->description,
            'icon' => $block->icon,
            'template' => $block->template,
            'is_system' => $block->is_system,
            'is_active' => $block->is_active,
            'order_index' => $block->order_index,
        ];

        // Category
        if ($block->category) {
            $data['category'] = [
                'name' => $block->category->name,
                'slug' => $block->category->slug,
                'icon' => $block->category->icon,
                'order_index' => $block->category->order_index,
            ];
        } else {
            $data['category'] = null;
        }

        // Fields (top-level only, sub-fields nested inside repeaters)
        $data['fields'] = $block->fields->map(function ($field) {
            return $this->fieldToArray($field);
        })->values()->toArray();

        // Template assignments
        $data['template_assignments'] = $block->templateAssignments
            ->sortBy('order_index')
            ->map(function ($assignment) {
                return [
                    'template_name' => $assignment->template_name,
                    'is_default' => $assignment->is_default,
                    'order_index' => $assignment->order_index,
                ];
            })->values()->toArray();

        return $data;
    }

    /**
     * Convert a field model to a JSON-safe array
     */
    protected function fieldToArray(BlockRegistryField $field): array
    {
        $data = [
            'name' => $field->name,
            'label' => $field->label,
            'type' => $field->type,
            'order_index' => $field->order_index,
            'required' => $field->required,
            'css_class' => $field->css_class,
            'settings' => $field->settings ? json_decode($field->settings, true) : null,
            'options' => $field->options,
            'regex' => $field->regex,
            'mime' => $field->mime,
            'default_value' => $field->default_value,
            'info' => $field->info,
        ];

        // Repeater-specific fields
        if ($field->type === 'repeater') {
            $data['repeater_min'] = $field->repeater_min;
            $data['repeater_max'] = $field->repeater_max;
            $data['repeater_text'] = $field->repeater_text;

            $data['sub_fields'] = $field->subFields->map(function ($subField) {
                return $this->fieldToArray($subField);
            })->values()->toArray();
        }

        return $data;
    }

    /**
     * Export a single block to its JSON file
     *
     * @return string The file path written to
     */
    public function exportBlock(BlockRegistry $block, ?string $previousSlug = null): string
    {
        $dir = $this->getBlocksDirectory();
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $data = $this->blockToArray($block);
        $filePath = $dir . '/' . $block->slug . '.json';

        $isRename = $previousSlug !== null && $previousSlug !== $block->slug;

        // `example_data` lives only in the JSON file (never in the DB), so
        // preserve any existing authored content across an export round-trip.
        // On a slug rename the new file doesn't exist yet, so fall back to the
        // old slug's file to carry the authored content forward.
        $exampleData = $this->getExampleData($block->slug);
        if ($exampleData === null && $isRename) {
            $exampleData = $this->getExampleData($previousSlug);
        }
        if ($exampleData !== null) {
            $data['example_data'] = $exampleData;
        }

        file_put_contents(
            $filePath,
            json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
        );

        // Remove the stale old-slug file after a rename, otherwise it lingers
        // on disk and `blocks:import --all` would resurrect the renamed block.
        if ($isRename) {
            $oldPath = $dir . '/' . $previousSlug . '.json';
            if (is_file($oldPath)) {
                unlink($oldPath);
            }
        }

        return $filePath;
    }

    /**
     * Export all blocks to JSON files
     */
    public function exportAllBlocks(): array
    {
        $blocks = BlockRegistry::orderBy('name')->get();
        $exported = [];
        $errors = [];

        foreach ($blocks as $block) {
            try {
                $this->exportBlock($block);
                $exported[] = $block->slug;
            } catch (\Exception $e) {
                $errors[] = [
                    'slug' => $block->slug,
                    'error' => $e->getMessage(),
                ];
            }
        }

        return [
            'exported' => $exported,
            'errors' => $errors,
        ];
    }

    // -------------------------------------------------------------------------
    // IMPORT
    // -------------------------------------------------------------------------

    /**
     * Read and parse a JSON block file
     */
    public function readBlockFile(string $filePath): array
    {
        if (!file_exists($filePath)) {
            throw new \RuntimeException("Block file not found: {$filePath}");
        }

        $contents = file_get_contents($filePath);
        $data = json_decode($contents, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \RuntimeException("Invalid JSON in {$filePath}: " . json_last_error_msg());
        }

        $errors = $this->validateBlockJson($data);
        if (!empty($errors)) {
            throw new \RuntimeException("Validation failed for {$filePath}: " . implode(', ', $errors));
        }

        return $data;
    }

    /**
     * Import a single block from a parsed JSON array
     *
     * Uses updateOrCreate by slug. Fields are deleted and recreated.
     */
    public function importBlock(array $data): BlockRegistry
    {
        return DB::transaction(function () use ($data) {
            $categoryKeyExists = array_key_exists('category', $data);
            $hasCategory = $categoryKeyExists && !empty($data['category']);
            $categoryId = $hasCategory ? $this->resolveCategory($data['category']) : null;

            // Find or create block by slug
            $block = BlockRegistry::where('slug', $data['slug'])->first();
            $isNew = !$block;

            $blockData = [
                'name' => $data['name'],
                'description' => $data['description'] ?? null,
                'icon' => $data['icon'] ?? 'fa-cube',
                'template' => $data['template'] ?? null,
                'is_system' => $data['is_system'] ?? false,
                'is_active' => $data['is_active'] ?? true,
                'order_index' => $data['order_index'] ?? 0,
            ];

            // Set category_id when JSON explicitly defines a category (non-null value),
            // or explicitly clears it (null). Only preserve existing DB value when
            // the category key is entirely absent from the JSON.
            if ($hasCategory) {
                $blockData['category_id'] = $categoryId;
            } elseif ($categoryKeyExists) {
                $blockData['category_id'] = null;
            }

            if ($isNew) {
                $blockData['slug'] = $data['slug'];
                if (!$categoryKeyExists) {
                    $blockData['category_id'] = null;
                }
                $block = BlockRegistry::create($blockData);
            } else {
                $block->update($blockData);
            }

            // Delete and recreate all fields
            $block->allFields()->delete();
            if (!empty($data['fields'])) {
                $this->registryService->createFieldsWithSubFields($block->id, $data['fields']);
            }

            // Sync template assignments
            $block->templateAssignments()->delete();
            if (!empty($data['template_assignments'])) {
                foreach ($data['template_assignments'] as $assignment) {
                    BlockRegistryTemplateAssignment::create([
                        'block_id' => $block->id,
                        'template_name' => $assignment['template_name'],
                        'is_default' => $assignment['is_default'] ?? false,
                        'order_index' => $assignment['order_index'] ?? 0,
                    ]);
                }
            }

            return $block->fresh(['fields', 'category', 'templateAssignments']);
        });
    }

    /**
     * Import all blocks from the blocks/ directory
     */
    public function importAllBlocks(): array
    {
        $dir = $this->getBlocksDirectory();
        $created = [];
        $updated = [];
        $errors = [];

        if (!is_dir($dir)) {
            return ['created' => $created, 'updated' => $updated, 'errors' => $errors];
        }

        $files = glob($dir . '/*.json');
        sort($files);

        foreach ($files as $file) {
            $slug = basename($file, '.json');
            try {
                $data = $this->readBlockFile($file);
                $existed = BlockRegistry::where('slug', $data['slug'])->exists();
                $this->importBlock($data);

                if ($existed) {
                    $updated[] = $slug;
                } else {
                    $created[] = $slug;
                }
            } catch (\Exception $e) {
                $errors[] = [
                    'slug' => $slug,
                    'error' => $e->getMessage(),
                ];
            }
        }

        return [
            'created' => $created,
            'updated' => $updated,
            'errors' => $errors,
        ];
    }

    // -------------------------------------------------------------------------
    // DIFF
    // -------------------------------------------------------------------------

    /**
     * Compare a block's DB state against JSON data
     */
    public function diffBlock(BlockRegistry $block, array $jsonData): array
    {
        $dbData = $this->blockToArray($block);
        $diff = [];

        // Compare metadata
        $metadataKeys = ['name', 'description', 'icon', 'template', 'is_system', 'is_active', 'order_index'];
        $changedMeta = [];
        foreach ($metadataKeys as $key) {
            $dbVal = $dbData[$key] ?? null;
            $jsonVal = $jsonData[$key] ?? null;
            if ($dbVal !== $jsonVal) {
                $changedMeta[$key] = ['db' => $dbVal, 'json' => $jsonVal];
            }
        }

        // Compare category
        $dbCat = $dbData['category']['slug'] ?? null;
        $jsonCat = $jsonData['category']['slug'] ?? null;
        if ($dbCat !== $jsonCat) {
            $changedMeta['category'] = ['db' => $dbCat, 'json' => $jsonCat];
        }

        if (!empty($changedMeta)) {
            $diff['metadata'] = $changedMeta;
        }

        // Compare fields
        $dbFields = collect($dbData['fields'])->keyBy('name');
        $jsonFields = collect($jsonData['fields'])->keyBy('name');

        $addedFields = $jsonFields->diffKeys($dbFields)->keys()->toArray();
        $removedFields = $dbFields->diffKeys($jsonFields)->keys()->toArray();

        $modifiedFields = [];
        foreach ($jsonFields as $name => $jsonField) {
            if (!$dbFields->has($name)) {
                continue;
            }
            $dbField = $dbFields[$name];
            $fieldDiff = $this->diffField($dbField, $jsonField);
            if (!empty($fieldDiff)) {
                $modifiedFields[$name] = $fieldDiff;
            }
        }

        if (!empty($addedFields) || !empty($removedFields) || !empty($modifiedFields)) {
            $diff['fields'] = [
                'added' => $addedFields,
                'removed' => $removedFields,
                'modified' => $modifiedFields,
            ];
        }

        // Compare template assignments
        $dbTemplates = collect($dbData['template_assignments'])->keyBy('template_name');
        $jsonTemplates = collect($jsonData['template_assignments'])->keyBy('template_name');

        $addedTemplates = $jsonTemplates->diffKeys($dbTemplates)->keys()->toArray();
        $removedTemplates = $dbTemplates->diffKeys($jsonTemplates)->keys()->toArray();

        if (!empty($addedTemplates) || !empty($removedTemplates)) {
            $diff['template_assignments'] = [
                'added' => $addedTemplates,
                'removed' => $removedTemplates,
            ];
        }

        return $diff;
    }

    /**
     * Compare two field arrays
     */
    protected function diffField(array $dbField, array $jsonField): array
    {
        $changes = [];
        $keys = ['label', 'type', 'order_index', 'required', 'css_class', 'settings', 'options', 'regex', 'mime', 'default_value', 'info'];

        foreach ($keys as $key) {
            $dbVal = $dbField[$key] ?? null;
            $jsonVal = $jsonField[$key] ?? null;
            if ($dbVal !== $jsonVal) {
                $changes[$key] = ['db' => $dbVal, 'json' => $jsonVal];
            }
        }

        // Compare repeater-specific fields
        if (($jsonField['type'] ?? null) === 'repeater') {
            foreach (['repeater_min', 'repeater_max', 'repeater_text'] as $key) {
                $dbVal = $dbField[$key] ?? null;
                $jsonVal = $jsonField[$key] ?? null;
                if ($dbVal !== $jsonVal) {
                    $changes[$key] = ['db' => $dbVal, 'json' => $jsonVal];
                }
            }

            // Compare sub-fields
            $dbSubs = collect($dbField['sub_fields'] ?? [])->keyBy('name');
            $jsonSubs = collect($jsonField['sub_fields'] ?? [])->keyBy('name');

            $subChanges = [];
            $addedSubs = $jsonSubs->diffKeys($dbSubs)->keys()->toArray();
            $removedSubs = $dbSubs->diffKeys($jsonSubs)->keys()->toArray();

            if (!empty($addedSubs) || !empty($removedSubs)) {
                $subChanges['added'] = $addedSubs;
                $subChanges['removed'] = $removedSubs;
            }

            foreach ($jsonSubs as $name => $jsonSub) {
                if (!$dbSubs->has($name)) {
                    continue;
                }
                $subDiff = $this->diffField($dbSubs[$name], $jsonSub);
                if (!empty($subDiff)) {
                    $subChanges['modified'][$name] = $subDiff;
                }
            }

            if (!empty($subChanges)) {
                $changes['sub_fields'] = $subChanges;
            }
        }

        return $changes;
    }

    /**
     * Get sync status for all blocks (DB + JSON files combined)
     */
    public function getSyncStatus(): array
    {
        $dbBlocks = BlockRegistry::with(['fields.subFields', 'category', 'templateAssignments'])
            ->orderBy('name')
            ->get()
            ->keyBy('slug');

        $dir = $this->getBlocksDirectory();
        $jsonFiles = [];

        if (is_dir($dir)) {
            foreach (glob($dir . '/*.json') as $file) {
                $slug = basename($file, '.json');
                try {
                    $jsonFiles[$slug] = $this->readBlockFile($file);
                } catch (\Exception $e) {
                    $jsonFiles[$slug] = ['_error' => $e->getMessage()];
                }
            }
        }

        $allSlugs = collect(array_merge(
            $dbBlocks->keys()->toArray(),
            array_keys($jsonFiles)
        ))->unique()->sort()->values();

        $status = [];

        foreach ($allSlugs as $slug) {
            $dbBlock = $dbBlocks->get($slug);
            $jsonData = $jsonFiles[$slug] ?? null;

            if (isset($jsonData['_error'])) {
                $status[] = [
                    'slug' => $slug,
                    'name' => $dbBlock ? $dbBlock->name : $slug,
                    'category' => $dbBlock && $dbBlock->category ? $dbBlock->category->name : null,
                    'status' => 'error',
                    'error' => $jsonData['_error'],
                    'json_file' => $dir . '/' . $slug . '.json',
                ];
                continue;
            }

            if ($dbBlock && $jsonData) {
                // Both exist — compare
                $diff = $this->diffBlock($dbBlock, $jsonData);
                $status[] = [
                    'slug' => $slug,
                    'name' => $dbBlock->name,
                    'category' => $dbBlock->category ? $dbBlock->category->name : null,
                    'status' => empty($diff) ? 'in_sync' : 'local_changes',
                    'diff' => $diff,
                    'json_file' => $dir . '/' . $slug . '.json',
                ];
            } elseif ($dbBlock && !$jsonData) {
                // Only in DB
                $status[] = [
                    'slug' => $slug,
                    'name' => $dbBlock->name,
                    'category' => $dbBlock->category ? $dbBlock->category->name : null,
                    'status' => 'not_exported',
                    'json_file' => null,
                ];
            } elseif (!$dbBlock && $jsonData) {
                // Only in JSON
                $status[] = [
                    'slug' => $slug,
                    'name' => $jsonData['name'] ?? $slug,
                    'category' => $jsonData['category']['name'] ?? null,
                    'status' => 'not_imported',
                    'json_file' => $dir . '/' . $slug . '.json',
                ];
            }
        }

        return $status;
    }

    // -------------------------------------------------------------------------
    // HELPERS
    // -------------------------------------------------------------------------

    /**
     * Resolve or create a category from JSON data
     */
    protected function resolveCategory(?array $categoryData): ?int
    {
        if (!$categoryData || empty($categoryData['slug'])) {
            return null;
        }

        $category = BlockRegistryCategory::firstOrCreate(
            ['slug' => $categoryData['slug']],
            [
                'name' => $categoryData['name'] ?? $categoryData['slug'],
                'icon' => $categoryData['icon'] ?? null,
                'order_index' => $categoryData['order_index'] ?? 0,
            ]
        );

        return $category->id;
    }

    /**
     * Validate block JSON structure
     */
    public function validateBlockJson(array $data): array
    {
        $errors = [];

        if (empty($data['name'])) {
            $errors[] = 'Block name is required';
        }

        if (empty($data['slug'])) {
            $errors[] = 'Block slug is required';
        }

        if (isset($data['fields']) && is_array($data['fields'])) {
            foreach ($data['fields'] as $index => $field) {
                if (empty($field['name'])) {
                    $errors[] = "Field at index {$index} is missing a name";
                }
                if (!empty($field['type']) && !array_key_exists($field['type'], BlockRegistryField::FIELD_TYPES)) {
                    $errors[] = "Field '{$field['name']}' has invalid type: {$field['type']}";
                }
            }
        }

        return $errors;
    }
}
