<?php

namespace App\Services;

use App\Models\Report;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

class ReportQueryBuilder
{
    /**
     * Build a query from a database-driven report configuration.
     */
    public function build(Report $report, array $filters = []): Builder
    {
        // Load all required relationships
        $report->load(['entities', 'columns', 'conditions', 'orders']);

        // 1. Start with base model query
        $query = $this->buildBaseQuery($report);

        // 2. Apply joins from report_entities
        $this->applyJoins($query, $report);

        // 3. Select columns from report_columns
        $this->applySelects($query, $report);

        // 4. Apply GROUP BY if aggregate functions are used
        $this->applyGroupBy($query, $report);

        // 5. Apply conditions from report_conditions
        $this->applyConditions($query, $report, $filters);

        // 6. Apply ORDER BY from report_orders
        $this->applyOrders($query, $report);

        return $query;
    }

    /**
     * Build the base query from the first entity.
     */
    protected function buildBaseQuery(Report $report): Builder
    {
        $baseEntity = $report->entities->firstWhere('join_type', 'base');

        if (!$baseEntity) {
            throw new \RuntimeException("Report {$report->slug} has no base entity defined.");
        }

        $modelClass = $baseEntity->model_class;

        if (!class_exists($modelClass)) {
            throw new \RuntimeException("Model class {$modelClass} does not exist.");
        }

        return $modelClass::query();
    }

    /**
     * Apply joins from report entities.
     */
    protected function applyJoins(Builder $query, Report $report): void
    {
        $entities = $report->entities->where('join_type', '!=', 'base');

        foreach ($entities as $entity) {
            $table = $this->getTableFromModel($entity->model_class);
            
            // Use alias if provided
            if ($entity->alias) {
                $table = "{$table} as {$entity->alias}";
            }
            
            $joinMethod = $entity->join_type;

            $query->{$joinMethod}(
                $table,
                $entity->join_first,
                $entity->join_operator ?? '=',
                $entity->join_second
            );
        }
    }

    /**
     * Apply column selections.
     */
    protected function applySelects(Builder $query, Report $report): void
    {
        $columns = $report->columns
            ->where('visible', true)
            ->map(fn($col) => DB::raw("{$col->expression} as {$col->key}"))
            ->toArray();

        if (!empty($columns)) {
            $query->select($columns);
        }
    }

    /**
     * Apply GROUP BY clause if aggregate functions are detected.
     */
    protected function applyGroupBy(Builder $query, Report $report): void
    {
        $visibleColumns = $report->columns->where('visible', true);
        
        // Check if any column uses aggregate functions
        $hasAggregates = $visibleColumns->contains(function ($col) {
            return preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
        });

        if (!$hasAggregates) {
            return; // No aggregates, no grouping needed
        }

        // Find non-aggregate columns that need to be in GROUP BY
        $groupByColumns = $visibleColumns
            ->filter(function ($col) {
                // Check if this column does NOT use an aggregate function
                return !preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
            })
            ->map(function ($col) {
                // Extract the actual column expression (before any COALESCE, CAST, etc.)
                // For COALESCE(segments.name, 'default'), we need segments.name
                if (preg_match('/COALESCE\s*\(\s*([^,]+)\s*,/i', $col->expression, $matches)) {
                    return trim($matches[1]);
                }
                // For simple columns, use as-is
                return $col->expression;
            })
            ->filter() // Remove empty values
            ->values()
            ->toArray();

        if (!empty($groupByColumns)) {
            foreach ($groupByColumns as $column) {
                $query->groupBy(DB::raw($column));
            }
        }
    }

    /**
     * Apply conditions (WHERE clauses).
     */
    protected function applyConditions(Builder $query, Report $report, array $filters): void
    {
        $conditions = $report->conditions->where('enabled', true);

        // Group conditions by group_id
        $groups = $conditions->groupBy('group_id');

        foreach ($groups as $groupId => $groupConditions) {
            $query->where(function ($subQuery) use ($groupConditions, $filters) {
                foreach ($groupConditions as $condition) {
                    $value = $this->resolveConditionValue($condition, $filters);

                    // Skip if filter-based and no value provided
                    if ($condition->value_type === 'filter' && $value === null) {
                        continue;
                    }

                    $method = $condition->logical_operator === 'OR' ? 'orWhere' : 'where';

                    $this->applyCondition($subQuery, $condition, $value, $method);
                }
            });
        }
    }

    /**
     * Apply a single condition to the query.
     */
    protected function applyCondition(Builder $query, $condition, $value, string $method): void
    {
        $column = $condition->column;
        $operator = strtoupper($condition->operator);

        switch ($operator) {
            case 'IS NULL':
                $query->{$method . 'Null'}($column);
                break;

            case 'IS NOT NULL':
                $query->{$method . 'NotNull'}($column);
                break;

            case 'IN':
                $values = is_array($value) ? $value : explode(',', $value);
                $query->{$method . 'In'}($column, $values);
                break;

            case 'NOT IN':
                $values = is_array($value) ? $value : explode(',', $value);
                $query->{$method . 'NotIn'}($column, $values);
                break;

            case 'BETWEEN':
                if (is_array($value) && count($value) === 2) {
                    $query->{$method . 'Between'}($column, $value);
                }
                break;

            case 'LIKE':
                $query->{$method}($column, 'LIKE', $value);
                break;

            default:
                $query->{$method}($column, $operator, $value);
                break;
        }
    }

    /**
     * Resolve condition value based on value_type.
     */
    protected function resolveConditionValue($condition, array $filters)
    {
        return match ($condition->value_type) {
            'static' => $condition->value,
            'filter' => $filters[$condition->filter_key] ?? null,
            'expression' => DB::raw($condition->value),
            default => null,
        };
    }

    /**
     * Apply ORDER BY clauses.
     */
    protected function applyOrders(Builder $query, Report $report): void
    {
        foreach ($report->orders as $order) {
            $query->orderBy($order->column, $order->direction);
        }
    }

    /**
     * Get table name from model class.
     */
    protected function getTableFromModel(string $modelClass): string
    {
        if (!class_exists($modelClass)) {
            throw new \RuntimeException("Model class {$modelClass} does not exist.");
        }

        return (new $modelClass)->getTable();
    }
}
