<?php

namespace YesWiki\Bazar\Service;

use YesWiki\Bazar\Controller\EntryController;
use YesWiki\Bazar\Field\EnumField;
use YesWiki\Wiki;

class BazarListService
{
    protected $entryController;
    protected $entryManager;
    protected $entryExtraFields;
    protected $externalBazarService;
    protected $formManager;
    protected $wiki;

    public function __construct(
        Wiki $wiki,
        EntryManager $entryManager,
        EntryExtraFieldsService $entryExtrafields,
        EntryController $entryController,
        ExternalBazarService $externalBazarService,
        FormManager $formManager
    ) {
        $this->wiki = $wiki;
        $this->entryManager = $entryManager;
        $this->entryExtraFields = $entryExtrafields;
        $this->entryController = $entryController;
        $this->externalBazarService = $externalBazarService;
        $this->formManager = $formManager;
    }

    public function getForms($options): array
    {
        // External mode activated ?
        if (($options['externalModeActivated'] ?? false) === true) {
            return $this->externalBazarService
                ->getFormsForBazarListe($options['externalIds'], $options['refresh']);
        } else {
            return $this->formManager->getAll();
        }
    }

    private function replaceDefaultImage($options, $forms, $entries): array
    {
        if (!class_exists('attach')) {
            include 'tools/attach/libs/attach.lib.php';
        }
        $attach = new \Attach($this->wiki);
        $basePath = $attach->GetUploadPath();
        $basePath = $basePath . (substr($basePath, -1) != '/' ? '/' : '');
        $formIds = array_keys($forms) ?? [];

        foreach ($formIds as $id) {
            $template = $forms[(int)$id]['template'] ?? [];
            $image_names = array_map(
                function ($item) {
                    return $item[1];
                },
                array_filter(
                    $template,
                    function ($item) {
                        return $item[0] == 'image';
                    }
                )
            );
            foreach ($image_names as $image_name) {
                $default_image_filename = "defaultimage{$id}_{$image_name}.jpg";
                if (file_exists($basePath . $default_image_filename)) {
                    $image_key = 'image' . $image_name;
                    foreach ($entries as $key => $entry) {
                        if (array_key_exists($image_key, $entry) && ($entry[$image_key] == null)) {
                            $entry[$image_key] = $default_image_filename;
                        }
                        $entries[$key] = $entry;
                    }
                }
            }
        }

        return $entries;
    }

    public function getEntries($options, $forms = null): array
    {
        if (!$forms) {
            $forms = $this->getForms($options);
        }

        // External mode activated ?
        // TODO BazarListdynamic test externalmode works
        if (($options['externalModeActivated'] ?? false) === true) {
            $entries = $this->externalBazarService->getEntries([
                'forms' => $forms,
                'refresh' => $options['refresh'] ?? false,
                'queries' => $options['query'] ?? '',
                'correspondance' => $options['correspondance'] ?? '',
            ]);
        } else {
            $entries = $this->entryManager->search(
                [
                    'regexp' => $options['regexp'] ?? '0',
                    'queries' => $options['query'] ?? '',
                    'formsIds' => $options['idtypeannonce'] ?? [],
                    'keywords' => $this->wiki->services->get(SearchManager::class)->aggregateKeywords($_REQUEST['q'] ?? null, $_REQUEST['keywords'] ?? null),
                    'user' => $options['user'] ?? null,
                    'minDate' => $options['dateMin'] ?? null,
                    'keywords' => $options['keywords'] ?? null,
                    'searchfields' => $options['searchfields'] ?? '',
                    'user' => $options['user'] ?? null,
                    'minDate' => $options['dateMin'] ?? null,
                    'correspondance' => $options['correspondance'] ?? '',
                ],
                true, // filter on read ACL,
                true // use Guard
            );
        }
        $entries = $this->replaceDefaultImage($options, $forms, $entries);

        // add extra informations (comments, reactions, metadatas)
        if ($options['extrafields'] === true) {
            foreach ($entries as $i => $entry) {
                $this->entryExtraFields->setEntryId($entry['id_fiche']);
                foreach (EntryExtraFieldsService::EXTRA_FIELDS as $field) {
                    $entries[$i][$field] = $this->entryExtraFields->get($field);
                }
                // for the linked entries, we need to add some informations to html_data
                if (!empty($entries[$i]['linked_data'])) {
                    $entries[$i]['html_data'] .= $this->entryExtraFields->appendHtmlData($entries[$i]['linked_data']);
                }
            }
        }
        // filter entries on datefilter parameter
        if (!empty($options['datefilter'])) {
            $entries = $this->entryController->filterEntriesOnDate($entries, $options['datefilter']);
        }

        // Sort entries
        if ($options['random'] ?? false) {
            shuffle($entries);
        } else {
            usort($entries, $this->buildFieldSorter($options['ordre'] ?? 'asc', $options['champ'] ?? 'bf_titre'));
        }

        // Limit entries
        if ($options['nb'] ?? false) {
            $entries = array_slice($entries, 0, $options['nb']);
        }

        return $entries;
    }

    // Use bazarlist options like groups, titles, groupicons, groupsexpanded
    // To create a filters array to be used by the view
    // Note for [old-non-dynamic-bazarlist] For old bazarlist, most of the calculation happens on the backend
    // But with the new dynamic bazalist, everything is done on the front
    public function getFilters($options, $entries, $forms, $withIdIndexes = false): array
    {
        // add default options
        $options = array_merge([
            'groups' => [],
            'dynamic' => true,
            'groupsexpanded' => false,
        ], $options);

        $formIdsUsed = array_unique(array_column($entries, 'id_typeannonce'));
        $formsUsed = array_map(function ($formId) use ($forms) {
            return $forms[$formId];
        }, $formIdsUsed);
        $allFields = array_merge(...array_column($formsUsed, 'prepared'));

        $propNames = $options['groups'];
        // Special value groups=all use all available Enum fields
        if (count($propNames) == 1 && $propNames[0] == 'all') {
            $enumFields = array_filter($allFields, function ($field) {
                return $field instanceof EnumField;
            });
            $propNames = array_map(function ($field) {
                return $field->getPropertyName();
            }, $enumFields);
        }

        $filters = [];
        $linkedSep = '_-_';
        foreach ($propNames as $index => $propName) {
            // Create a filter object to be returned to the view
            $filter = [
                'propName' => $propName,
                'title' => '',
                'icon' => '',
                'nodes' => [],
                'collapsed' => true,
            ];

            // Check if linked data value
            if (str_contains($propName, $linkedSep)) {
                $field = $propName;
            } else {
                // Check if an existing Form Field existing by this propName
                foreach ($allFields as $aField) {
                    if ($aField->getPropertyName() == $propName) {
                        $field = $aField;
                        break;
                    }
                }
            }
            // Depending on the propName, get the list of filter nodes
            if (!empty($field) && $field instanceof EnumField) {
                // ENUM FIELD
                $filter['title'] = $field->getLabel();

                if (!empty($field->getOptionsTree()) && $options['dynamic'] == true) {
                    // OptionsTree only supported by bazarlist dynamic
                    foreach ($field->getOptionsTree() as $node) {
                        $filter['nodes'][] = $this->recursivelyCreateNode($node);
                    }
                } else {
                    foreach ($field->getOptions() as $value => $label) {
                        $filter['nodes'][] = $this->createFilterNode($value, $label);
                    }
                }
            } elseif ($propName == 'id_typeannonce') {
                // SPECIAL PROPNAME id_typeannonce
                $filter['title'] = _t('BAZ_TYPE_FICHE');
                foreach ($formsUsed as $form) {
                    $filter['nodes'][] = $this->createFilterNode($form['bn_id_nature'], $form['bn_label_nature']);
                }
                usort($filter['nodes'], function ($a, $b) {
                    return strcmp($a['label'], $b['label']);
                });
            } elseif (str_contains($propName, $linkedSep)) {
                $idLinkedData = explode($linkedSep, $propName);
                $linkedField = [];
                if (!empty($idLinkedData[0]) && !empty($idLinkedData[1])) {
                    $linkedField = $this->formManager->findFieldWithId($formIdsUsed, $idLinkedData[0]);
                    if (!empty($linkedField)) {
                        $linkedFormId = $linkedField->getLinkedObjectName();
                        $finalField = $this->formManager->findFieldWithId([$linkedFormId], $idLinkedData[1]);
                        if (!empty($finalField)) {
                            $filter['title'] = $finalField->getLabel();
                            if ($finalField instanceof EnumField) {
                                if (!empty($finalField->getOptionsTree()) && $options['dynamic'] == true) {
                                    // OptionsTree only supported by bazarlist dynamic
                                    foreach ($finalField->getOptionsTree() as $node) {
                                        $filter['nodes'][$node['value']] = $this->recursivelyCreateNode($node);
                                    }
                                } else {
                                    foreach ($finalField->getOptions() as $value => $label) {
                                        $filter['nodes'][$value] = $this->createFilterNode($value, $label);
                                    }
                                }
                            } else {
                                // TODO: options?
                            }
                        }
                    }
                }
            } else {
                // OTHER PROPNAME (for example a field that is not an Enum)
                $foundField = $this->formManager->findFieldWithId($formIdsUsed, $propName);
                if (!empty($foundField)) {
                    $filter['title'] = $foundField->getLabel();
                } else {
                    $filter['title'] = $propName == 'owner' ? _t('BAZ_CREATOR') : $propName;
                }

                // We collect all values
                $uniqValues = array_unique(array_column($entries, $propName));

                usort($uniqValues, function ($a, $b) {
                    return strcmp(strtolower(iconv('UTF-8', 'ASCII//TRANSLIT', $a)), strtolower(iconv('UTF-8', 'ASCII//TRANSLIT', $b)));
                });

                foreach ($uniqValues as $value) {
                    $filter['nodes'][] = $this->createFilterNode($value, $value);
                }
            }
            // Filter Icon
            if (!empty($options['groupicons'][$index])) {
                $filter['icon'] = '<i class="' . $options['groupicons'][$index] . '"></i> ';
            }
            // Custom title
            if (!empty($options['titles'][$index])) {
                $filter['title'] = $options['titles'][$index];
            }
            // Initial Collapsed state
            $filter['collapsed'] = ($index != 0) && !$options['groupsexpanded'];

            // [old-non-dynamic-bazarlist] For old bazarlist, most of the calculation happens on the backend
            if ($options['dynamic'] == false) {
                $checkedValues = $this->parseCheckedFiltersInURLForNonDynamic();
                // Calculate the count for each filterNode
                $entriesValues = array_column($entries, $propName);
                // convert string values to array
                $entriesValues = array_map(function ($val) {
                    return explode(',', $val);
                }, $entriesValues);
                // flatten the array
                $entriesValues = array_merge(...$entriesValues);
                $countedValues = array_count_values($entriesValues);
                $adjustedNodes = [];
                foreach ($filter['nodes'] as $rootNode) {
                    $adjustedNodes[] = $this->recursivelyInitValuesForNonDynamic($rootNode, $propName, $countedValues, $checkedValues);
                }
                $filter['nodes'] = $adjustedNodes;
            }
            if ($withIdIndexes) {
                $filters[$filter['propName']] = $filter;
            } else {
                $filters[] = $filter;
            }
        }

        return $filters;
    }

    // [old-non-dynamic-bazarlist] filters state in stored in URL
    // ?Page&facette=field1=3,4|field2=web
    // => ['field1' => ['3', '4'], 'field2' => ['web']]
    private function parseCheckedFiltersInURLForNonDynamic()
    {
        if (empty($_GET['facette'])) {
            return [];
        }
        $result = [];
        foreach (explode('|', $_GET['facette']) as $field) {
            list($key, $values) = explode('=', $field);
            $result[$key] = explode(',', trim($values));
        }

        return $result;
    }

    private function createFilterNode($value, $label)
    {
        return [
            'value' => htmlspecialchars($value),
            'label' => $label,
            'children' => [],
        ];
    }

    private function recursivelyCreateNode($node)
    {
        $result = $this->createFilterNode($node['id'], $node['label']);
        foreach ($node['children'] as $childNode) {
            $result['children'][] = $this->recursivelyCreateNode($childNode);
        }

        return $result;
    }

    private function recursivelyInitValuesForNonDynamic($node, $propName, $countedValues, $checkedValues)
    {
        $result = array_merge($node, [
            'id' => $propName . $node['value'],
            'name' => $propName,
            'count' => $countedValues[$node['value']] ?? 1,
            'checked' => isset($checkedValues[$propName]) && in_array($node['value'], $checkedValues[$propName]) ? ' checked' : '',
        ]);

        foreach ($node['children'] as &$childNode) {
            $result['children'][] = $this->recursivelyInitValuesForNonDynamic($childNode, $propName, $countedValues, $checkedValues);
        }

        return $result;
    }

    private function getValueForArray($array, $key, $default = null)
    {
        if (!is_array($array)) {
            return $default;
        }
        if (is_null($key)) {
            return $array;
        }
        if (array_key_exists($key, $array)) {
            return $array[$key];
        }
        if (strpos($key, '.') === false) {
            return $array[$key] ?? $default;
        }
        foreach (explode('.', $key) as $segment) {
            if (is_array($array) && array_key_exists($segment, $array)) {
                $array = $array[$segment];
            } else {
                return $default;
            }
        }

        return $array;
    }

    private function buildFieldSorter($ordre, $champ): callable
    {
        return function ($a, $b) use ($ordre, $champ) {
            if (strstr($champ, '.')) {
                $val1 = $this->getValueForArray($a, $champ);
                $val2 = $this->getValueForArray($b, $champ);
            } else {
                $val1 = $a[$champ] ?? '';
                $val2 = $b[$champ] ?? '';
            }
            if ($ordre == 'desc') {
                return strnatcmp(
                    $this->sanitizeStringForCompare($val2),
                    $this->sanitizeStringForCompare($val1)
                );
            } else {
                return strnatcmp(
                    $this->sanitizeStringForCompare($val1),
                    $this->sanitizeStringForCompare($val2)
                );
            }
        };
    }

    private function sanitizeStringForCompare($value): string
    {
        if ($value === null) {
            $value = '';
        }
        $value = is_scalar($value)
            ? strval($value)
            : json_encode($value);

        return strtoupper(removeAccents($value));
    }
}
