<?php

declare(strict_types=1);

namespace FecaMailshots\Application;

use FecaMailshots\Domain\AppError;

final class DslValidator
{
    private SourceMetadataProvider $metadata;

    /** @var array<string, list<string>> */
    private array $filterSourceRequirements = [
        'selected-renewal' => ['renewals'],
        'pending-renewal' => ['renewals'],
        'primary-contact' => ['contacts'],
        'fen1-contact' => ['contacts'],
        'member-or-affiliate-or-parish-council' => ['accounts'],
    ];

    public function __construct(SourceMetadataProvider $metadata)
    {
        $this->metadata = $metadata;
    }

    /**
     * @param array{sources:list<string>, where:array<int, mixed>} $ast
     * @return array{errors:list<string>,warnings:list<string>,expected_fields:list<string>}
     */
    public function validate(array $ast): array
    {
        $errors = [];
        $warnings = [];

        $sources = $ast['sources'];
        if ($sources === []) {
            $errors[] = 'At least one source must be provided.';
            return ['errors' => $errors, 'warnings' => $warnings, 'expected_fields' => []];
        }

        $hasBuiltIn = false;
        $hasCustom = false;
        foreach ($sources as $source) {
            if (!$this->metadata->sourceExists($source)) {
                $errors[] = 'Unknown source: ' . $source;
                continue;
            }
            if (str_contains($source, '.')) {
                $hasCustom = true;
            } else {
                $hasBuiltIn = true;
            }
        }

        if ($hasBuiltIn && $hasCustom) {
            $errors[] = 'Mixing built-in and custom sources is not supported in v1.5.';
        }

        for ($i = 1; $i < count($sources); $i++) {
            if ($this->metadata->joinPath($sources[$i - 1], $sources[$i]) === null) {
                $errors[] = sprintf('No approved join path between %s and %s', $sources[$i - 1], $sources[$i]);
            }
        }

        $expectedFields = [];
        foreach ($sources as $source) {
            foreach ($this->metadata->sourceFields($source) as $field) {
                $expectedFields[] = $source . '.' . $field;
            }
        }

        if (!$this->hasAnyEmailField($sources)) {
            $warnings[] = 'Data Source has no obvious email field; select an explicit Recipient Email Field on each Mailshot.';
        }

        foreach ($ast['where'] as $predicate) {
            $this->validatePredicate($predicate, $sources, $errors);
        }

        return [
            'errors' => $errors,
            'warnings' => $warnings,
            'expected_fields' => $expectedFields,
        ];
    }

    /** @param list<string> $sources @param list<string> &$errors */
    private function validatePredicate(array $predicate, array $sources, array &$errors): void
    {
        if ($predicate['type'] === 'group') {
            foreach ($predicate['items'] as $item) {
                $this->validatePredicate($item, $sources, $errors);
            }
            return;
        }

        if ($predicate['type'] === 'filter') {
            $name = $predicate['name'];
            if ($name === 'fen2-contact') {
                $errors[] = 'Filter fen2-contact is not supported. Use fen1-contact.';
                return;
            }
            if (!isset($this->filterSourceRequirements[$name])) {
                $errors[] = 'Unknown filter: ' . $name;
                return;
            }
            foreach ($this->filterSourceRequirements[$name] as $requiredSource) {
                if (!in_array($requiredSource, $sources, true)) {
                    $errors[] = sprintf('Filter %s requires source %s', $name, $requiredSource);
                }
            }
            return;
        }

        if (isset($predicate['lhs'])) {
            $this->validateFieldRef($predicate['lhs'], $sources, $errors);
        }
        if (isset($predicate['rhs']) && is_array($predicate['rhs']) && isset($predicate['rhs']['source'])) {
            $this->validateFieldRef($predicate['rhs'], $sources, $errors);
        }

        if ($predicate['type'] === 'comparison_field' && !in_array($predicate['op'], ['=', '!='], true)) {
            $errors[] = 'Field-to-field comparison supports only = and !=';
        }

        if ($predicate['type'] === 'comparison_field' && in_array($predicate['op'], ['contains', 'starts-with', 'ends-with'], true)) {
            $errors[] = $predicate['op'] . ' requires a literal right-hand value';
        }
    }

    /** @param array{source:string,field:string} $fieldRef @param list<string> $sources @param list<string> &$errors */
    private function validateFieldRef(array $fieldRef, array $sources, array &$errors): void
    {
        $source = $fieldRef['source'];
        if (!in_array($source, $sources, true) && !str_contains($source, '.')) {
            $errors[] = 'Field source is not in selected sources: ' . $source;
            return;
        }

        if (!$this->metadata->sourceExists($source)) {
            $errors[] = 'Unknown field source: ' . $source;
            return;
        }

        $fields = $this->metadata->sourceFields($source);
        if (!in_array($fieldRef['field'], $fields, true)) {
            $errors[] = sprintf('Unknown field %s on %s', $fieldRef['field'], $source);
        }
    }

    /** @param list<string> $sources */
    private function hasAnyEmailField(array $sources): bool
    {
        foreach ($sources as $source) {
            if ($this->metadata->hasEmailField($source)) {
                return true;
            }
        }
        return false;
    }
}
