<?php

declare(strict_types=1);

namespace FecaMailshots\Application;

use FecaMailshots\Domain\AppError;

final class DslCompiler
{
    private SourceMetadataProvider $metadata;

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

    /**
     * @param array{sources:list<string>, where:array<int, mixed>} $ast
     * @return array{sql:string,params:list<mixed>}
     */
    public function compile(array $ast): array
    {
        $sources = $ast['sources'];
        if ($sources === []) {
            throw new AppError('dsl_compile', 'No sources provided');
        }

        $from = $this->quoteSource($sources[0]) . ' AS ' . $this->alias($sources[0]);
        $joins = [];

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

            [$leftRef, $rightRef] = $this->rewriteJoinRef($path['left'], $path['right']);
            $joins[] = 'INNER JOIN ' . $this->quoteSource($right) . ' AS ' . $this->alias($right)
                . ' ON ' . $leftRef . ' = ' . $rightRef;
        }

        if (in_array('accounts', $sources, true)) {
            $acc = $this->alias('accounts');
            $joins[] = 'LEFT JOIN `picklist_account_type` AS `p_account_type` ON `p_account_type`.`id` = ' . $acc . '.`account_type_id`';
            $joins[] = 'LEFT JOIN `picklist_public_location` AS `p_public_location` ON `p_public_location`.`id` = ' . $acc . '.`public_location_id`';
            $joins[] = 'LEFT JOIN `picklist_sector` AS `p_sector` ON `p_sector`.`id` = ' . $acc . '.`sector_id`';
        }

        $params = [];
        $whereParts = [];
        foreach ($ast['where'] as $predicate) {
            $whereParts[] = $this->compilePredicate($predicate, $params);
        }

        $whereSql = $whereParts === [] ? '' : ' WHERE ' . implode(' AND ', $whereParts);

        $selectSql = count($sources) > 1 ? $this->buildUniqueSelectProjection($sources) : '*';
        $sql = 'SELECT ' . $selectSql . ' FROM ' . $from;
        if ($joins !== []) {
            $sql .= ' ' . implode(' ', $joins);
        }
        $sql .= $whereSql;

        return ['sql' => $sql, 'params' => $params];
    }

    /** @param list<mixed> &$params */
    private function compilePredicate(array $predicate, array &$params): string
    {
        if ($predicate['type'] === 'group') {
            $parts = [];
            foreach ($predicate['items'] as $item) {
                $parts[] = $this->compilePredicate($item, $params);
            }
            $sql = '(' . implode(' AND ', $parts) . ')';
            return $predicate['not'] ? 'NOT ' . $sql : $sql;
        }

        if ($predicate['type'] === 'filter') {
            $sql = $this->compileFilter($predicate['name']);
            return $predicate['not'] ? 'NOT (' . $sql . ')' : $sql;
        }

        if ($predicate['type'] === 'comparison_value') {
            $lhs = $this->fieldRefSql($predicate['lhs']);
            $op = $predicate['op'];
            $value = $predicate['rhs'];
            if ($op === 'contains') {
                $op = 'LIKE';
                $value = '%' . $value . '%';
            } elseif ($op === 'starts-with') {
                $op = 'LIKE';
                $value = $value . '%';
            } elseif ($op === 'ends-with') {
                $op = 'LIKE';
                $value = '%' . $value;
            }
            $params[] = $value;
            $sql = sprintf('%s %s ?', $lhs, $op);
            return $predicate['not'] ? 'NOT (' . $sql . ')' : $sql;
        }

        if ($predicate['type'] === 'comparison_field') {
            $lhs = $this->fieldRefSql($predicate['lhs']);
            $rhs = $this->fieldRefSql($predicate['rhs']);
            $sql = sprintf('%s %s %s', $lhs, $predicate['op'], $rhs);
            return $predicate['not'] ? 'NOT (' . $sql . ')' : $sql;
        }

        if ($predicate['type'] === 'set_values') {
            $lhs = $this->fieldRefSql($predicate['lhs']);
            $placeholders = [];
            foreach ($predicate['values'] as $value) {
                $placeholders[] = '?';
                $params[] = $value;
            }
            $op = $predicate['op'] === 'not in' ? 'NOT IN' : 'IN';
            $sql = sprintf('%s %s (%s)', $lhs, $op, implode(',', $placeholders));
            return $predicate['not'] ? 'NOT (' . $sql . ')' : $sql;
        }

        if ($predicate['type'] === 'set_ref') {
            $lhs = $this->fieldRefSql($predicate['lhs']);
            $rhs = $this->fieldRefSql($predicate['rhs']);
            $op = $predicate['op'] === 'not in' ? 'NOT IN' : 'IN';
            $sql = sprintf('%s %s (SELECT %s)', $lhs, $op, $rhs);
            return $predicate['not'] ? 'NOT (' . $sql . ')' : $sql;
        }

        throw new AppError('dsl_compile', 'Unknown predicate type', ['type' => $predicate['type']]);
    }

    private function compileFilter(string $name): string
    {
        if ($name === 'selected-renewal') {
            return $this->alias('renewals') . '.`selected` = 1';
        }
        if ($name === 'pending-renewal') {
            return $this->alias('renewals') . '.`status` = \'pending\'';
        }
        if ($name === 'fen1-contact') {
            return $this->alias('contacts') . '.`is_fen_1` = 1';
        }
        if ($name === 'primary-contact') {
            return $this->alias('contacts') . '.`is_contact_1` = 1';
        }
        if ($name === 'member-or-affiliate-or-parish-council') {
            $acc = $this->alias('accounts');
            return sprintf(
                "(%s.`account_type_id` IN (SELECT id FROM `picklist_account_type` WHERE value IN ('Member','Affiliate')) OR %s.`name` LIKE '%%Parish Council%%')",
                $acc,
                $acc
            );
        }

        throw new AppError('dsl_compile', 'Unknown filter', ['filter' => $name]);
    }

    /** @return array{string,string} */
    private function rewriteJoinRef(string $left, string $right): array
    {
        $leftParts = explode('.', $left, 2);
        $rightParts = explode('.', $right, 2);

        return [
            $this->alias($leftParts[0]) . '.`' . $leftParts[1] . '`',
            $this->alias($rightParts[0]) . '.`' . $rightParts[1] . '`',
        ];
    }

    /** @param array{source:string,field:string} $fieldRef */
    private function fieldRefSql(array $fieldRef): string
    {
        if ($fieldRef['source'] === 'accounts') {
            $virtual = $this->accountsVirtualFieldSql($fieldRef['field']);
            if ($virtual !== null) {
                return $virtual;
            }
        }
        return $this->alias($fieldRef['source']) . '.`' . $fieldRef['field'] . '`';
    }

    private function alias(string $source): string
    {
        return 's_' . preg_replace('/[^a-z0-9_]/', '_', strtolower($source));
    }

    private function quoteSource(string $source): string
    {
        if (str_contains($source, '.')) {
            [$schema, $table] = explode('.', $source, 2);
            return '`' . $schema . '`.`' . $table . '`';
        }
        return '`' . $source . '`';
    }

    /** @param list<string> $sources */
    private function buildUniqueSelectProjection(array $sources): string
    {
        $parts = [];
        foreach ($sources as $source) {
            $alias = $this->alias($source);
            foreach ($this->metadata->sourceFields($source) as $field) {
                if ($source === 'accounts') {
                    $virtual = $this->accountsVirtualFieldSql($field);
                    if ($virtual !== null) {
                        $parts[] = $virtual . ' AS `' . $source . '.' . $field . '`';
                        continue;
                    }
                }
                $parts[] = $alias . '.`' . $field . '` AS `' . $source . '.' . $field . '`';
            }
        }
        if ($parts === []) {
            throw new AppError('dsl_compile', 'No fields available for selected sources');
        }
        return implode(', ', $parts);
    }

    private function accountsVirtualFieldSql(string $field): ?string
    {
        $name = strtolower(trim($field));
        if ($name === 'type') {
            return '`p_account_type`.`value`';
        }
        if ($name === 'public_location') {
            return '`p_public_location`.`value`';
        }
        if ($name === 'account_sector') {
            return '`p_sector`.`value`';
        }
        return null;
    }
}
