<?php

namespace MintyPHP\Service\CustomField;

use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepositoryInterface;
use MintyPHP\Repository\CustomField\UserCustomFieldValueRepositoryInterface;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Service\Tenant\TenantScopeService;

class UserCustomFieldValueService
{
    public function __construct(
        private readonly TenantScopeService $userScopeGateway,
        private readonly TenantCustomFieldDefinitionRepositoryInterface $definitionRepository,
        private readonly TenantCustomFieldOptionRepositoryInterface $optionRepository,
        private readonly UserCustomFieldValueRepositoryInterface $valueRepository,
        private readonly UserCustomFieldValueOptionRepositoryInterface $valueOptionRepository
    ) {
    }

    public function validateForTenants(array $tenantIds, array $post, bool $canEdit): array
    {
        if (!$canEdit) {
            return ['ok' => true, 'errors' => []];
        }
        $tenantIds = toIntIds($tenantIds);
        if (!$tenantIds) {
            return ['ok' => true, 'errors' => []];
        }
        $prepared = $this->prepareForSync($tenantIds, $post);
        if ($prepared['errors']) {
            return ['ok' => false, 'errors' => $prepared['errors']];
        }
        return ['ok' => true, 'errors' => []];
    }

    public function buildDefinitionsByTenant(array $tenantIds, bool $includeInactive = false): array
    {
        $tenantIds = toIntIds($tenantIds);
        if (!$tenantIds) {
            return [];
        }

        $definitions = $this->definitionRepository->listByTenantIds($tenantIds, !$includeInactive);
        if (!$definitions) {
            return [];
        }

        $definitionIds = array_values(array_filter(array_map(
            static fn (array $definition): int => (int) ($definition['id'] ?? 0),
            $definitions
        ), static fn (int $id): bool => $id > 0));
        $options = $this->optionRepository->listByDefinitionIds($definitionIds, !$includeInactive);
        $optionsByDefinition = [];
        foreach ($options as $option) {
            $definitionId = (int) ($option['definition_id'] ?? 0);
            if ($definitionId <= 0) {
                continue;
            }
            $optionsByDefinition[$definitionId] ??= [];
            $optionsByDefinition[$definitionId][] = $option;
        }

        $grouped = [];
        foreach ($definitions as $definition) {
            $tenantId = (int) ($definition['tenant_id'] ?? 0);
            $definitionId = (int) ($definition['id'] ?? 0);
            if ($tenantId <= 0 || $definitionId <= 0) {
                continue;
            }
            $definition['options'] = $optionsByDefinition[$definitionId] ?? [];
            $grouped[$tenantId] ??= [];
            $grouped[$tenantId][] = $definition;
        }

        return $grouped;
    }

    public function buildUserValueMap(int $userId, array $definitionIds): array
    {
        if ($userId <= 0) {
            return [];
        }
        $definitionIds = toIntIds($definitionIds);
        if (!$definitionIds) {
            return [];
        }

        $rows = $this->valueRepository->listByUserAndDefinitionIds($userId, $definitionIds);
        if (!$rows) {
            return [];
        }

        $valueIds = array_values(array_filter(array_map(
            static fn (array $row): int => (int) ($row['id'] ?? 0),
            $rows
        ), static fn (int $id): bool => $id > 0));
        $optionIdsByValue = $this->valueOptionRepository->listOptionIdsByValueIds($valueIds);

        $map = [];
        foreach ($rows as $row) {
            $definitionId = (int) ($row['definition_id'] ?? 0);
            $valueId = (int) ($row['id'] ?? 0);
            if ($definitionId <= 0 || $valueId <= 0) {
                continue;
            }
            $map[$definitionId] = [
                'id' => $valueId,
                'value_text' => array_key_exists('value_text', $row) ? (string) ($row['value_text'] ?? '') : '',
                'value_bool' => array_key_exists('value_bool', $row) && $row['value_bool'] !== null
                    ? (int) $row['value_bool']
                    : null,
                'value_date' => array_key_exists('value_date', $row) ? (string) ($row['value_date'] ?? '') : '',
                'option_id' => array_key_exists('option_id', $row) && $row['option_id'] !== null
                    ? (int) $row['option_id']
                    : null,
                'option_ids' => $optionIdsByValue[$valueId] ?? [],
            ];
        }

        return $map;
    }

    /**
     * Build API-safe custom field payload grouped by tenant.
     *
     * @param array<int, array<string, mixed>> $tenantSummaries Assignment tenant rows
     *        from user assignment context, each with at least id/uuid/description/status.
     * @return array<int, array<string, mixed>>
     */
    public function buildPublicValuesByTenant(int $userId, array $tenantSummaries, ?int $tenantScopeId = null): array
    {
        if ($userId <= 0) {
            return [];
        }

        $tenantScopeId = (int) ($tenantScopeId ?? 0);
        $orderedTenants = [];
        foreach ($tenantSummaries as $tenantSummary) {
            $tenantId = (int) ($tenantSummary['id'] ?? 0);
            if ($tenantId <= 0) {
                continue;
            }
            if ($tenantScopeId > 0 && $tenantId !== $tenantScopeId) {
                continue;
            }
            $orderedTenants[$tenantId] = [
                'id' => $tenantId,
                'uuid' => (string) ($tenantSummary['uuid'] ?? ''),
                'description' => (string) ($tenantSummary['description'] ?? ''),
                'status' => (string) ($tenantSummary['status'] ?? ''),
            ];
        }

        if (!$orderedTenants) {
            return [];
        }

        $tenantIds = array_keys($orderedTenants);
        $definitionsByTenant = $this->buildDefinitionsByTenant($tenantIds, false);
        if (!$definitionsByTenant) {
            return [];
        }

        $definitionIds = [];
        foreach ($tenantIds as $tenantId) {
            foreach (($definitionsByTenant[$tenantId] ?? []) as $definition) {
                $definitionId = (int) ($definition['id'] ?? 0);
                if ($definitionId > 0) {
                    $definitionIds[] = $definitionId;
                }
            }
        }
        $definitionIds = array_values(array_unique($definitionIds));
        $valueMap = $this->buildUserValueMap($userId, $definitionIds);

        $result = [];
        foreach ($tenantIds as $tenantId) {
            $definitions = $definitionsByTenant[$tenantId] ?? [];
            if (!$definitions) {
                continue;
            }

            $fields = [];
            foreach ($definitions as $definition) {
                $definitionId = (int) ($definition['id'] ?? 0);
                if ($definitionId <= 0) {
                    continue;
                }

                $type = strtolower(trim((string) ($definition['type'] ?? 'text')));
                $optionsRaw = is_array($definition['options'] ?? null) ? $definition['options'] : [];
                $optionsById = [];
                $optionsPublic = [];
                foreach ($optionsRaw as $option) {
                    if (!is_array($option)) {
                        continue;
                    }
                    $optionId = (int) ($option['id'] ?? 0);
                    $optionKey = trim((string) ($option['option_key'] ?? ''));
                    if ($optionId <= 0 || $optionKey === '') {
                        continue;
                    }
                    $optionsById[$optionId] = $optionKey;
                    $optionsPublic[] = [
                        'option_key' => $optionKey,
                        'label' => (string) ($option['label'] ?? ''),
                    ];
                }

                $field = [
                    'uuid' => (string) ($definition['uuid'] ?? ''),
                    'field_key' => (string) ($definition['field_key'] ?? ''),
                    'label' => (string) ($definition['label'] ?? ''),
                    'type' => $type,
                    'is_filterable' => !empty($definition['is_filterable']),
                    'value' => self::mapPublicValueByType($type, $valueMap[$definitionId] ?? null, $optionsById),
                ];

                if (in_array($type, ['select', 'multiselect'], true)) {
                    $field['options'] = $optionsPublic;
                }

                $fields[] = $field;
            }

            if (!$fields) {
                continue;
            }

            $tenant = $orderedTenants[$tenantId];
            $result[] = [
                'tenant' => [
                    'uuid' => (string) $tenant['uuid'],
                    'description' => (string) $tenant['description'],
                    'status' => (string) $tenant['status'],
                ],
                'fields' => $fields,
            ];
        }

        return $result;
    }

    public function syncForUser(int $userId, array $tenantIds, array $post, bool $canEdit): array
    {
        if ($userId <= 0) {
            return ['ok' => false, 'errors' => [t('User not found')]];
        }

        $tenantIds = toIntIds($tenantIds);
        // Always clean values for tenants no longer assigned — happens even when !$canEdit,
        // because orphaned custom field data should not persist after tenant reassignment.
        if (!$this->valueRepository->deleteByUserOutsideTenantIds($userId, $tenantIds)) {
            return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
        }
        if (!$canEdit) {
            return ['ok' => true, 'errors' => []];
        }

        if (!$tenantIds) {
            return ['ok' => true, 'errors' => []];
        }

        $prepared = $this->prepareForSync($tenantIds, $post);
        if ($prepared['errors']) {
            return ['ok' => false, 'errors' => $prepared['errors']];
        }
        if (!$prepared['prepared']) {
            return ['ok' => true, 'errors' => []];
        }
        foreach ($prepared['prepared'] as $definitionId => $valueData) {
            if (!empty($valueData['empty'])) {
                if (!$this->valueRepository->deleteByUserAndDefinitionIds($userId, [(int) $definitionId])) {
                    return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
                }
                continue;
            }

            if (!empty($valueData['option_ids'])) {
                $valueId = $this->valueRepository->upsertScalarValue($userId, (int) $definitionId, [
                    'value_text' => null,
                    'value_bool' => null,
                    'value_date' => null,
                    'option_id' => null,
                ]);
                if (!$valueId) {
                    return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
                }
                if (!$this->valueOptionRepository->replaceForValueId((int) $valueId, $valueData['option_ids'])) {
                    return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
                }
                continue;
            }

            $valueId = $this->valueRepository->upsertScalarValue($userId, (int) $definitionId, [
                'value_text' => $valueData['value_text'],
                'value_bool' => $valueData['value_bool'],
                'value_date' => $valueData['value_date'],
                'option_id' => $valueData['option_id'],
            ]);
            if (!$valueId) {
                return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
            }
            if (!$this->valueOptionRepository->replaceForValueId((int) $valueId, [])) {
                return ['ok' => false, 'errors' => [t('Custom field values can not be saved')]];
            }
        }

        return ['ok' => true, 'errors' => []];
    }

    private function prepareForSync(array $tenantIds, array $post): array
    {
        $definitionsByTenant = $this->buildDefinitionsByTenant($tenantIds, false);
        $definitions = [];
        foreach ($definitionsByTenant as $tenantDefinitionList) {
            foreach ($tenantDefinitionList as $definition) {
                $definitionId = (int) ($definition['id'] ?? 0);
                if ($definitionId > 0) {
                    $definitions[$definitionId] = $definition;
                }
            }
        }
        if (!$definitions) {
            return ['prepared' => [], 'errors' => []];
        }

        $rawValues = is_array($post['custom_field_values'] ?? null) ? $post['custom_field_values'] : [];
        $rawMultiValues = is_array($post['custom_field_values_multi'] ?? null) ? $post['custom_field_values_multi'] : [];

        $errors = [];
        $prepared = [];
        foreach ($definitions as $definitionId => $definition) {
            $type = strtolower((string) ($definition['type'] ?? ''));
            $label = (string) ($definition['label'] ?? ('#' . $definitionId));
            $options = is_array($definition['options'] ?? null) ? $definition['options'] : [];
            $activeOptionIds = array_values(array_filter(array_map(
                static fn (array $option): int => !empty($option['active']) ? (int) ($option['id'] ?? 0) : 0,
                $options
            ), static fn (int $id): bool => $id > 0));
            $activeOptionMap = $activeOptionIds ? array_fill_keys($activeOptionIds, true) : [];

            $rawScalar = $rawValues[(string) $definitionId] ?? null;
            $rawMulti = $rawMultiValues[(string) $definitionId] ?? [];

            $valueData = [
                'value_text' => null,
                'value_bool' => null,
                'value_date' => null,
                'option_id' => null,
                'option_ids' => [],
                'empty' => true,
            ];

            if (in_array($type, ['text', 'textarea'], true)) {
                $text = trim((string) $rawScalar);
                if ($text !== '') {
                    $valueData['value_text'] = $text;
                    $valueData['empty'] = false;
                }
            } elseif ($type === 'boolean') {
                $bool = self::normalizeBool($rawScalar);
                if ($bool === null && trim((string) $rawScalar) !== '') {
                    $errors[] = sprintf(t('Invalid value for %s'), $label);
                    continue;
                }
                if ($bool !== null) {
                    $valueData['value_bool'] = $bool;
                    $valueData['empty'] = false;
                }
            } elseif ($type === 'date') {
                $date = self::normalizeDate($rawScalar);
                if ($date === null && trim((string) $rawScalar) !== '') {
                    $errors[] = sprintf(t('Invalid value for %s'), $label);
                    continue;
                }
                if ($date !== null) {
                    $valueData['value_date'] = $date;
                    $valueData['empty'] = false;
                }
            } elseif ($type === 'select') {
                $optionId = (int) $rawScalar;
                if ($optionId > 0 && !isset($activeOptionMap[$optionId])) {
                    $errors[] = sprintf(t('Invalid value for %s'), $label);
                    continue;
                }
                if ($optionId > 0) {
                    $valueData['option_id'] = $optionId;
                    $valueData['empty'] = false;
                }
            } elseif ($type === 'multiselect') {
                $optionIds = RepoQuery::normalizeIdList($rawMulti);
                $invalid = array_values(array_filter($optionIds, static fn (int $id): bool => !isset($activeOptionMap[$id])));
                if ($invalid) {
                    $errors[] = sprintf(t('Invalid value for %s'), $label);
                    continue;
                }
                if ($optionIds) {
                    $valueData['option_ids'] = $optionIds;
                    $valueData['empty'] = false;
                }
            }

            $prepared[$definitionId] = $valueData;
        }

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

    /**
     * Extract custom field filter specification from a URL query.
     *
     * Parses cf_*, cfm_*, and cfd_* query parameters against the user's
     * visible custom field definitions and returns validated filter data.
     *
     * @param array<string, mixed> $query URL query parameters
     * @return array{definitions: list<array<string, mixed>>, filters: array<string, array<int, mixed>>, query: array<string, string>}
     */
    public function extractCustomFieldFilterSpec(array $query, int $currentUserId): array
    {
        $tenantIds = $this->userScopeGateway()->getUserTenantIds($currentUserId);
        $definitions = $this->definitionRepository->listFilterableByTenantIds($tenantIds);
        if (!$definitions) {
            return ['definitions' => [], 'filters' => [], 'query' => []];
        }

        $definitionMapByUuid = [];
        $definitionIds = [];
        foreach ($definitions as $definition) {
            $uuid = strtolower(trim((string) ($definition['uuid'] ?? '')));
            $definitionId = (int) ($definition['id'] ?? 0);
            if ($uuid === '' || $definitionId <= 0) {
                continue;
            }
            $definitionMapByUuid[$uuid] = $definition;
            $definitionIds[] = $definitionId;
        }
        $definitionIds = array_values(array_unique($definitionIds));
        $options = $this->optionRepository->listByDefinitionIds($definitionIds, true);
        $optionMapByDefinition = [];
        foreach ($options as $option) {
            $definitionId = (int) ($option['definition_id'] ?? 0);
            $optionId = (int) ($option['id'] ?? 0);
            if ($definitionId <= 0 || $optionId <= 0) {
                continue;
            }
            $optionMapByDefinition[$definitionId] ??= [];
            $optionMapByDefinition[$definitionId][$optionId] = true;
        }

        $filters = [
            'select' => [],
            'boolean' => [],
            'multiselect' => [],
            'date' => [],
        ];
        $normalizedQuery = [];
        foreach ($query as $rawKey => $rawValue) {
            $key = strtolower(trim((string) $rawKey));
            if ($key === '') {
                continue;
            }

            // URL key prefixes: cf_ = single-value (select/boolean), cfm_ = multiselect, cfd_ = date range.
            if (preg_match('/^cf_([a-f0-9-]{36})$/', $key, $match)) {
                $uuid = $match[1];
                $definition = $definitionMapByUuid[$uuid] ?? null;
                if (!$definition) {
                    continue;
                }
                $definitionId = (int) ($definition['id'] ?? 0);
                $type = strtolower((string) ($definition['type'] ?? ''));
                if ($type === 'select') {
                    $optionId = (int) $rawValue;
                    if ($optionId > 0 && !empty($optionMapByDefinition[$definitionId][$optionId])) {
                        $filters['select'][$definitionId] = $optionId;
                        $normalizedQuery[$key] = (string) $optionId;
                    }
                } elseif ($type === 'boolean') {
                    $bool = self::normalizeBool($rawValue);
                    if ($bool !== null) {
                        $filters['boolean'][$definitionId] = $bool;
                        $normalizedQuery[$key] = (string) $bool;
                    }
                }
                continue;
            }

            if (preg_match('/^cfm_([a-f0-9-]{36})$/', $key, $match)) {
                $uuid = $match[1];
                $definition = $definitionMapByUuid[$uuid] ?? null;
                if (!$definition || strtolower((string) ($definition['type'] ?? '')) !== 'multiselect') {
                    continue;
                }
                $definitionId = (int) ($definition['id'] ?? 0);
                $optionIds = RepoQuery::normalizeIdList($rawValue);
                if (!$optionIds) {
                    continue;
                }
                $allowedMap = $optionMapByDefinition[$definitionId] ?? [];
                $optionIds = array_values(array_filter($optionIds, static fn (int $id): bool => isset($allowedMap[$id])));
                if (!$optionIds) {
                    continue;
                }
                $filters['multiselect'][$definitionId] = $optionIds;
                $normalizedQuery[$key] = implode(',', $optionIds);
                continue;
            }

            if (preg_match('/^cfd_([a-f0-9-]{36})_(from|to)$/', $key, $match)) {
                $uuid = $match[1];
                $bound = $match[2];
                $definition = $definitionMapByUuid[$uuid] ?? null;
                if (!$definition || strtolower((string) ($definition['type'] ?? '')) !== 'date') {
                    continue;
                }
                $date = self::normalizeDate($rawValue);
                if ($date === null) {
                    continue;
                }
                $definitionId = (int) ($definition['id'] ?? 0);
                $filters['date'][$definitionId] ??= ['from' => null, 'to' => null];
                $filters['date'][$definitionId][$bound] = $date;
                $normalizedQuery[$key] = $date;
            }
        }

        return [
            'definitions' => array_values($definitionMapByUuid),
            'filters' => $filters,
            'query' => $normalizedQuery,
        ];
    }

    private function userScopeGateway(): TenantScopeService
    {
        return $this->userScopeGateway;
    }

    private static function normalizeBool($value): ?int
    {
        if (is_bool($value)) {
            return $value ? 1 : 0;
        }
        $value = strtolower(trim((string) $value));
        if ($value === '') {
            return null;
        }
        if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
            return 1;
        }
        if (in_array($value, ['0', 'false', 'no', 'off'], true)) {
            return 0;
        }
        return null;
    }

    private static function normalizeDate($value): ?string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }
        $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
        if (!$dt || $dt->format('Y-m-d') !== $value) {
            return null;
        }
        return $value;
    }

    private static function mapPublicValueByType(string $type, ?array $valueData, array $optionsById)
    {
        $hasValue = $valueData !== null;
        if ($type === 'multiselect') {
            if (!$hasValue) {
                return [];
            }
            $optionIds = is_array($valueData['option_ids'] ?? null) ? $valueData['option_ids'] : [];
            $keys = [];
            foreach ($optionIds as $optionIdRaw) {
                $optionId = (int) $optionIdRaw;
                if ($optionId > 0 && isset($optionsById[$optionId])) {
                    $keys[] = (string) $optionsById[$optionId];
                }
            }
            return array_values(array_unique($keys));
        }

        if (!$hasValue) {
            return null;
        }

        return match ($type) {
            'boolean' => array_key_exists('value_bool', $valueData) && $valueData['value_bool'] !== null
                ? ((int) $valueData['value_bool'] === 1)
                : null,
            'date' => (($date = trim((string) ($valueData['value_date'] ?? ''))) !== '' ? $date : null),
            'select' => (($optionId = (int) ($valueData['option_id'] ?? 0)) > 0 && isset($optionsById[$optionId]))
                ? (string) $optionsById[$optionId]
                : null,
            default => (($text = (string) ($valueData['value_text'] ?? '')) !== '' ? $text : null),
        };
    }
}
