<?php

declare(strict_types=1);

namespace PpPhp\Core\Designer\FieldControl;

use PpPhp\Core\PluginRegistry;

/**
 * FieldControlRuntime
 *
 * The single consumer of the field-control plugins (View / Edit / Filter).
 * Resolves a control id within a category, calls the plugin's render() with the
 * correct signature, and returns HTML + the plugin's front-end assets.
 *
 * Used by:
 *   - the Designer live-preview endpoint (DesignerController::fieldPreview)
 *   - any generated-page runtime that draws a field with its assigned control
 */
final class FieldControlRuntime
{
    /** Category -> the control id used when none is assigned or the assigned one is missing. */
    private const DEFAULTS = ['view' => 'PlainText', 'edit' => 'TextBox', 'filter' => 'TextFilter'];

    /**
     * Render a single field control.
     *
     * @return array{html:string, control:string, label:string, assets:array, ok:bool}
     */
    public static function render(
        string $category,
        string $controlId,
        string $fieldName,
        mixed $value = null,
        array $settings = []
    ): array {
        $category = strtolower($category);
        $items = self::items($category);

        $id = $controlId !== '' && isset($items[$controlId])
            ? $controlId
            : (self::DEFAULTS[$category] ?? '');

        // LookupWizard is an EDIT-only plugin with no 'view' class, but a field
        // whose View control is LookupWizard must still show the DISPLAY value
        // (the court name, not the raw FK). Resolve it here from the control's
        // own settings — table source (key→display via one query) or manual
        // options (value→label map) — instead of falling back to the raw key.
        if ($category === 'view' && $controlId === 'LookupWizard') {
            return [
                'html'    => self::resolveLookupDisplay($value, $settings),
                'control' => 'LookupWizard',
                'label'   => 'Lookup',
                'assets'  => ['js' => [], 'css' => []],
                'ok'      => true,
            ];
        }

        $item = $items[$id] ?? null;

        if ($item === null || !isset($item['class']) || !class_exists($item['class'])) {
            return [
                'html' => self::fallbackHtml($category, $fieldName, $value),
                'control' => $id,
                'label' => $id ?: 'Field',
                'assets' => ['js' => [], 'css' => []],
                'ok' => false,
            ];
        }

        $class = $item['class'];

        try {
            $html = match ($category) {
                'view'   => $class::render($value, $settings),
                'edit'   => $class::render($fieldName, $value, $settings),
                'filter' => $class::render($fieldName, $settings),
                default  => self::fallbackHtml($category, $fieldName, $value),
            };
        } catch (\Throwable $e) {
            $html = self::fallbackHtml($category, $fieldName, $value);
        }

        return [
            'html' => is_string($html) ? $html : '',
            'control' => $id,
            'label' => (string)($item['label'] ?? $id),
            'assets' => is_array($item['assets'] ?? null) ? $item['assets'] : ['js' => [], 'css' => []],
            'ok' => true,
        ];
    }

    /**
     * The picker catalogue for the front-end: id/label/settings per category.
     * (Strips class/path so nothing internal leaks to the browser.)
     *
     * @return array<string, array<int, array<string, mixed>>>
     */
    public static function catalogue(): array
    {
        $out = ['view' => [], 'edit' => [], 'filter' => []];

        foreach (array_keys($out) as $category) {
            foreach (self::items($category) as $id => $item) {
                $out[$category][] = [
                    'id' => $id,
                    'label' => $item['label'] ?? $id,
                    'settings' => is_array($item['settings'] ?? null) ? $item['settings'] : [],
                    'core' => (bool)($item['core'] ?? false),
                ];
            }
        }

        // Per-field validation rules (Presence / Format / Numeric / …) for the
        // inspector's "Validation" section. Separate shape from the controls
        // above: each carries its parameter schema, group and default message.
        $out['validation'] = [];
        foreach (PluginRegistry::validation() as $id => $item) {
            $out['validation'][] = [
                'id' => $id,
                'label' => $item['label'] ?? $id,
                'group' => $item['group'] ?? 'General',
                'params' => is_array($item['params'] ?? null) ? $item['params'] : [],
                'applies' => is_array($item['applies'] ?? null) ? $item['applies'] : [],
                'message' => (string) ($item['message'] ?? ''),
            ];
        }

        return $out;
    }

    /**
     * Merged front-end assets across every field-control plugin, so live-rendered
     * controls (e.g. a date-picker's JS) work inside the designer / runtime.
     *
     * @return array{css: array<int,string>, js: array<int,string>}
     */
    public static function assets(): array
    {
        $css = [];
        $js = [];

        foreach (['view', 'edit', 'filter'] as $category) {
            foreach (self::items($category) as $item) {
                foreach (($item['assets']['css'] ?? []) as $c) {
                    if (is_string($c) && $c !== '') { $css[] = $c; }
                }
                foreach (($item['assets']['js'] ?? []) as $j) {
                    if (is_string($j) && $j !== '') { $js[] = $j; }
                }
            }
        }

        return ['css' => array_values(array_unique($css)), 'js' => array_values(array_unique($js))];
    }

    /** @return array<string, array<string, mixed>> */
    private static function items(string $category): array
    {
        return match ($category) {
            'view'   => PluginRegistry::view(),
            'edit'   => PluginRegistry::edit(),
            'filter' => PluginRegistry::filter(),
            default  => [],
        };
    }

    /**
     * Display value(s) for a LookupWizard VIEW control. Handles both sources:
     *  - table:  SELECT <displayField> FROM <table> WHERE <linkField> = value
     *            (CSV-aware for multi-value lookups; unknown keys stay raw).
     *  - manual: value→label from `manualOptions` ("key:Label, key2:Label2" or
     *            bare "Label, Label2" where the key is the label itself).
     * Falls back to the raw (escaped) value on any error or empty display.
     */
    private static function resolveLookupDisplay(mixed $value, array $settings): string
    {
        $raw = trim((string) ($value ?? ''));
        if ($raw === '') return '<span></span>';
        $esc = static fn (string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');

        $source = (string) ($settings['source'] ?? 'table');
        $parts = array_values(array_filter(array_map('trim', explode(',', $raw)), 'strlen'));

        if ($source === 'manual') {
            $map = [];
            foreach (preg_split('/\s*,\s*/', (string) ($settings['manualOptions'] ?? '')) as $opt) {
                $opt = trim($opt);
                if ($opt === '') continue;
                if (strpos($opt, ':') !== false) { [$k, $v] = explode(':', $opt, 2); $map[trim($k)] = trim($v); }
                else { $map[$opt] = $opt; }
            }
            $out = array_map(static fn ($p) => $map[$p] ?? $p, $parts);
            return '<span>' . $esc(implode(', ', $out)) . '</span>';
        }

        $table = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['table'] ?? ''));
        $key   = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['linkField'] ?? 'id')) ?: 'id';
        $disp  = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($settings['displayField'] ?? ''));
        if ($table === '' || $disp === '' || !class_exists('\PpPhp\Core\DB')) {
            return '<span>' . $esc($raw) . '</span>';
        }
        try {
            $in = implode(',', array_fill(0, count($parts), '?'));
            $rows = \PpPhp\Core\DB::FetchAll("SELECT `$key` AS k, `$disp` AS d FROM `$table` WHERE `$key` IN ($in)", $parts) ?: [];
            $map = [];
            foreach ($rows as $r) { $map[(string) $r['k']] = (string) $r['d']; }
            $out = array_map(static fn ($p) => $map[$p] ?? $p, $parts);
            return '<span>' . $esc(implode(', ', $out)) . '</span>';
        } catch (\Throwable $e) {
            return '<span>' . $esc($raw) . '</span>';
        }
    }

    private static function fallbackHtml(string $category, string $fieldName, mixed $value): string
    {
        $safe = htmlspecialchars((string)($value ?? ''), ENT_QUOTES, 'UTF-8');
        $name = htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8');

        return match ($category) {
            'edit'   => '<input type="text" name="' . $name . '" value="' . $safe . '" class="form-control">',
            'filter' => '<input type="text" name="filter_' . $name . '" class="form-control form-control-sm" placeholder="Filter…">',
            default  => '<span>' . $safe . '</span>',
        };
    }
}
