<?php

declare(strict_types=1);

namespace MauticPlugin\DealFlowBundle\Service;

use Doctrine\ORM\EntityManagerInterface;
use MauticPlugin\DealFlowBundle\Entity\Deal;
use MauticPlugin\DealFlowBundle\Entity\DealField;
use MauticPlugin\DealFlowBundle\Entity\DealFieldRepository;
use MauticPlugin\DealFlowBundle\Entity\DealRepository;

class DealFieldManager
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly DealFieldRepository $dealFieldRepository,
        private readonly DealRepository $dealRepository,
    ) {
    }

    /**
     * @return list<DealField>
     */
    public function getFields(): array
    {
        return $this->dealFieldRepository->findOrdered();
    }

    /**
     * @return list<DealField>
     */
    public function getActiveFields(): array
    {
        return $this->dealFieldRepository->findActiveOrdered();
    }

    /**
     * @return list<DealField>
     */
    public function getListFields(): array
    {
        return $this->dealFieldRepository->findListVisibleOrdered();
    }

    public function normalizeAlias(string $value): string
    {
        $value = trim($value);
        if ('' === $value) {
            return 'field';
        }

        $ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
        if (false !== $ascii) {
            $value = $ascii;
        }

        $value = strtolower($value);
        $value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? '';
        $value = trim($value, '_');

        return '' !== $value ? $value : 'field';
    }

    /**
     * @param array<string, mixed> $submittedValues
     * @param list<DealField>|null $fields
     *
     * @return array{values: array<string, mixed>, errors: array<string, string>}
     */
    public function normalizeSubmittedValues(array $submittedValues, ?array $fields = null): array
    {
        $fields ??= $this->getActiveFields();
        $values = [];
        $errors = [];

        foreach ($fields as $field) {
            $alias = $field->getAlias();
            $raw = $submittedValues[$alias] ?? null;

            if ($field->getFieldType() === DealField::TYPE_BOOLEAN && !array_key_exists($alias, $submittedValues)) {
                $raw = false;
            }

            [$normalized, $error] = $this->normalizeFieldValue($field, $raw);

            if (null !== $error) {
                $errors[$alias] = $error;
                continue;
            }

            if (null === $normalized && !$field->isRequired() && $field->getFieldType() !== DealField::TYPE_BOOLEAN) {
                continue;
            }

            if ($field->isRequired() && $this->isEmptyValue($field, $normalized, true)) {
                $errors[$alias] = sprintf('%s is required.', $field->getLabel());
                continue;
            }

            if (null !== $normalized || $field->getFieldType() === DealField::TYPE_BOOLEAN) {
                $values[$alias] = $normalized;
            }
        }

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

    /**
     * @param list<DealField>|null $fields
     *
     * @return array<string, mixed>
     */
    public function getFormValues(Deal $deal, ?array $fields = null): array
    {
        $fields ??= $this->getActiveFields();
        $stored = $deal->getCustomFields();
        $values = [];

        foreach ($fields as $field) {
            $alias = $field->getAlias();
            if (array_key_exists($alias, $stored)) {
                $values[$alias] = $stored[$alias];
                continue;
            }

            $defaultValue = $this->resolveDefaultFormValue($field);
            if (null !== $defaultValue || $field->getFieldType() === DealField::TYPE_BOOLEAN) {
                $values[$alias] = $defaultValue;
            }
        }

        return $values;
    }

    /**
     * @param list<DealField>|null $fields
     *
     * @return list<array{label: string, alias: string, type: string, value: mixed, displayValue: string, showInList: bool}>
     */
    public function buildDisplayRows(Deal $deal, ?array $fields = null, bool $includeEmpty = false): array
    {
        $fields ??= $this->getActiveFields();
        $stored = $deal->getCustomFields();
        $rows = [];

        foreach ($fields as $field) {
            $alias = $field->getAlias();
            $value = $stored[$alias] ?? null;

            if (!$includeEmpty && !array_key_exists($alias, $stored)) {
                continue;
            }

            if (!$includeEmpty && $this->isEmptyValue($field, $value)) {
                continue;
            }

            $rows[] = [
                'label' => $field->getLabel(),
                'alias' => $alias,
                'type' => $field->getFieldType(),
                'value' => $value,
                'displayValue' => $this->formatDisplayValue($field, $value, $includeEmpty),
                'showInList' => $field->showInList(),
            ];
        }

        return $rows;
    }

    /**
     * @param list<Deal> $deals
     * @param list<DealField>|null $fields
     *
     * @return array<int, array<string, array{label: string, alias: string, type: string, value: mixed, displayValue: string, showInList: bool}>>
     */
    public function buildListValueMap(array $deals, ?array $fields = null): array
    {
        $fields ??= $this->getListFields();
        $map = [];

        foreach ($deals as $deal) {
            $dealId = $deal->getId();
            if (null === $dealId) {
                continue;
            }

            foreach ($this->buildDisplayRows($deal, $fields, true) as $row) {
                $map[$dealId][$row['alias']] = $row;
            }
        }

        return $map;
    }

    /**
     * @param list<DealField>|null $fields
     *
     * @return array<string, mixed>
     */
    public function getApiValues(Deal $deal, ?array $fields = null): array
    {
        $fields ??= $this->getActiveFields();
        $stored = $deal->getCustomFields();
        $values = [];

        foreach ($fields as $field) {
            $alias = $field->getAlias();
            if (array_key_exists($alias, $stored)) {
                $values[$alias] = $stored[$alias];
            }
        }

        return $values;
    }

    /**
     * @param array<string, mixed> $existingValues
     * @param array<string, mixed> $replacementValues
     * @param list<DealField>|null $fields
     *
     * @return array<string, mixed>
     */
    public function mergeStoredValues(array $existingValues, array $replacementValues, ?array $fields = null): array
    {
        $fields ??= $this->getActiveFields();
        $merged = $existingValues;

        foreach ($fields as $field) {
            unset($merged[$field->getAlias()]);
        }

        foreach ($replacementValues as $alias => $value) {
            $merged[$alias] = $value;
        }

        return $merged;
    }

    public function removeFieldFromDeals(DealField $field): void
    {
        $alias = $field->getAlias();
        $changed = false;

        foreach ($this->dealRepository->findAll() as $deal) {
            $values = $deal->getCustomFields();
            if (!array_key_exists($alias, $values)) {
                continue;
            }

            unset($values[$alias]);
            $deal->setCustomFields($values)
                ->setDateModified(new \DateTime());

            $this->entityManager->persist($deal);
            $changed = true;
        }

        if ($changed) {
            $this->entityManager->flush();
        }
    }

    /**
     * @return array{0: mixed, 1: string|null}
     */
    private function normalizeFieldValue(DealField $field, mixed $rawValue): array
    {
        return match ($field->getFieldType()) {
            DealField::TYPE_TEXT,
            DealField::TYPE_TEXTAREA => $this->normalizeTextValue($rawValue),
            DealField::TYPE_NUMBER => $this->normalizeNumberValue($field, $rawValue),
            DealField::TYPE_BOOLEAN => $this->normalizeBooleanValue($field, $rawValue),
            DealField::TYPE_SELECT => $this->normalizeSelectValue($field, $rawValue),
            DealField::TYPE_DATE => $this->normalizeDateValue($field, $rawValue),
            default => [null, sprintf('%s has an unsupported field type.', $field->getLabel())],
        };
    }

    private function resolveDefaultFormValue(DealField $field): mixed
    {
        return match ($field->getFieldType()) {
            DealField::TYPE_BOOLEAN => $field->getBooleanDefaultValue() ?? false,
            DealField::TYPE_NUMBER => $this->castNumericDefault($field->getDefaultValue()),
            DealField::TYPE_TEXT,
            DealField::TYPE_TEXTAREA,
            DealField::TYPE_SELECT,
            DealField::TYPE_DATE => $field->getDefaultValue(),
            default => null,
        };
    }

    private function castNumericDefault(?string $value): float|int|null
    {
        if (null === $value || '' === trim($value) || !is_numeric($value)) {
            return null;
        }

        return str_contains($value, '.') ? (float) $value : (int) $value;
    }

    /**
     * @return array{0: string|null, 1: string|null}
     */
    private function normalizeTextValue(mixed $rawValue): array
    {
        if (null === $rawValue) {
            return [null, null];
        }

        $value = trim((string) $rawValue);

        return ['' !== $value ? $value : null, null];
    }

    /**
     * @return array{0: float|int|null, 1: string|null}
     */
    private function normalizeNumberValue(DealField $field, mixed $rawValue): array
    {
        if (null === $rawValue || '' === trim((string) $rawValue)) {
            return [null, null];
        }

        $value = trim((string) $rawValue);
        if (!is_numeric($value)) {
            return [null, sprintf('%s must be a valid number.', $field->getLabel())];
        }

        return [str_contains($value, '.') ? (float) $value : (int) $value, null];
    }

    /**
     * @return array{0: bool, 1: string|null}
     */
    private function normalizeBooleanValue(DealField $field, mixed $rawValue): array
    {
        if (is_bool($rawValue)) {
            return [$rawValue, null];
        }

        $value = strtolower(trim((string) $rawValue));
        if (in_array($value, ['', '0', 'false', 'no', 'off'], true)) {
            return [false, null];
        }

        if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
            return [true, null];
        }

        return [false, sprintf('%s must be yes or no.', $field->getLabel())];
    }

    /**
     * @return array{0: string|null, 1: string|null}
     */
    private function normalizeSelectValue(DealField $field, mixed $rawValue): array
    {
        $value = trim((string) $rawValue);
        if ('' === $value) {
            return [null, null];
        }

        if (!in_array($value, $field->getOptions(), true)) {
            return [null, sprintf('Select a valid value for %s.', $field->getLabel())];
        }

        return [$value, null];
    }

    /**
     * @return array{0: string|null, 1: string|null}
     */
    private function normalizeDateValue(DealField $field, mixed $rawValue): array
    {
        $value = trim((string) $rawValue);
        if ('' === $value) {
            return [null, null];
        }

        $date = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
        if (!$date instanceof \DateTimeImmutable || $date->format('Y-m-d') !== $value) {
            return [null, sprintf('%s must use the YYYY-MM-DD format.', $field->getLabel())];
        }

        return [$date->format('Y-m-d'), null];
    }

    private function formatDisplayValue(DealField $field, mixed $value, bool $includeEmpty = false): string
    {
        if (null === $value || '' === $value) {
            return $includeEmpty ? 'Not set' : '';
        }

        return match ($field->getFieldType()) {
            DealField::TYPE_BOOLEAN => $value ? 'Yes' : 'No',
            DealField::TYPE_DATE => (string) $value,
            default => (string) $value,
        };
    }

    private function isEmptyValue(DealField $field, mixed $value, bool $requiredMode = false): bool
    {
        if ($field->getFieldType() === DealField::TYPE_BOOLEAN) {
            return false;
        }

        if (null === $value) {
            return true;
        }

        if (is_string($value)) {
            return '' === trim($value);
        }

        if ($requiredMode && is_array($value)) {
            return [] === $value;
        }

        return false;
    }
}
