<?php

namespace Ignite\Core\Services\Reports;

use Ignite\Core\Models\Grid\AbstractTable;
use Ignite\Core\Models\Grid\EloquentTable;
use Ignite\Core\Models\Grid\QueryTable;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Http\Request;
use InvalidArgumentException;

/**
 * Serializable description of data that should be exported in the background.
 *
 * Live query builders, HTTP requests, sessions, and closures cannot safely cross a
 * queue boundary. This value object reduces those inputs to the small, validated
 * set of values that ReportDownloadService needs to reconstruct the download later.
 */
class ReportDownloadRequest
{
    public const SOURCE_TABLE = 'table';
    public const SOURCE_ELOQUENT_QUERY = 'eloquent_query';
    public const SOURCE_QUERY = 'query';
    public const FORMAT_CSV = 'csv';
    public const FORMAT_XLSX = 'xlsx';

    /** @var array<string, mixed> */
    protected $data;

    /**
     * @param array<string, mixed> $data
     */
    protected function __construct(array $data)
    {
        $this->data = $this->validate($data);
    }

    /**
     * Describe a download backed by any Core QueryTable or EloquentTable subclass.
     *
     * @param int $userId
     * @param class-string<AbstractTable> $tableClass
     * @param string $name
     * @param string $format
     * @param array<string, mixed> $params Constructor parameters passed under `params`.
     * @param array<string, mixed> $filters Values consumed by FilterFactory.
     * @param array<int, array{column:string,direction:string}> $sort
     * @param array{value?:string,columns?:array<string,string>} $search
     * @param array<int, string> $visibleColumns
     */
    public static function forTable(
        int $userId,
        string $tableClass,
        string $name,
        string $format = self::FORMAT_CSV,
        array $params = [],
        array $filters = [],
        array $sort = [],
        array $search = [],
        array $visibleColumns = []
    ): self {
        return new static([
            'user_id' => $userId,
            'name' => $name,
            'format' => $format,
            'source' => [
                'type' => self::SOURCE_TABLE,
                'class' => $tableClass,
                'params' => $params,
            ],
            'filters' => $filters,
            'sort' => $sort,
            'search' => $search,
            'visible_columns' => $visibleColumns,
            'columns' => [],
        ]);
    }

    /**
     * Describe a trusted ad-hoc Eloquent query using explicit raw result columns.
     *
     * @param array<int|string, string> $columns Column keys or key => heading pairs.
     * @param array<int, array{column:string,direction:string}> $sort
     * @param array<string, mixed> $filters Stored for audit/reproduction context.
     * @param array{value?:string,columns?:array<string,string>} $search
     */
    public static function forEloquentQuery(
        int $userId,
        EloquentBuilder $query,
        string $name,
        array $columns,
        string $format = self::FORMAT_CSV,
        array $sort = [],
        array $filters = [],
        array $search = []
    ): self {
        return static::forQuery(
            $userId,
            $query,
            $name,
            $columns,
            $format,
            $sort,
            $filters,
            $search,
            self::SOURCE_ELOQUENT_QUERY
        );
    }

    /**
     * Describe a trusted ad-hoc Laravel query using explicit raw result columns.
     *
     * @param array<int|string, string> $columns Column keys or key => heading pairs.
     * @param array<int, array{column:string,direction:string}> $sort
     * @param array<string, mixed> $filters Stored for audit/reproduction context.
     * @param array{value?:string,columns?:array<string,string>} $search
     */
    public static function forQueryBuilder(
        int $userId,
        QueryBuilder $query,
        string $name,
        array $columns,
        string $format = self::FORMAT_CSV,
        array $sort = [],
        array $filters = [],
        array $search = []
    ): self {
        return static::forQuery(
            $userId,
            $query,
            $name,
            $columns,
            $format,
            $sort,
            $filters,
            $search,
            self::SOURCE_QUERY
        );
    }

    /**
     * Build a table download request from allow-listed report/DataTables HTTP state.
     *
     * Numeric DataTables sort indexes are resolved to stable column keys here. The
     * complete HTTP request is never persisted because it contains unrelated and
     * potentially sensitive session, header, pagination, and CSRF data.
     *
     * @param class-string<AbstractTable> $tableClass
     * @param array<string, mixed> $params
     * @param array<string, mixed>|null $filters
     */
    public static function fromHttpRequest(
        Request $request,
        int $userId,
        string $tableClass,
        string $name,
        string $format = self::FORMAT_CSV,
        array $params = [],
        ?array $filters = null
    ): self {
        $requestColumns = collect((array) $request->input('columns', []));
        $sort = collect((array) $request->input('order', []))->map(function ($order) use ($requestColumns) {
            $column = $requestColumns->get((int) ($order['column'] ?? -1), []);

            return [
                'column' => $column['data'] ?? $column['name'] ?? '',
                'direction' => strtolower($order['dir'] ?? 'asc'),
            ];
        })->filter(function ($order) {
            return $order['column'] !== '';
        })->values()->all();

        $columnSearch = $requestColumns->mapWithKeys(function ($column) {
            $key = $column['data'] ?? $column['name'] ?? '';
            $value = $column['search']['value'] ?? '';

            return $key !== '' && $value !== '' ? [$key => $value] : [];
        })->all();

        return static::forTable(
            $userId,
            $tableClass,
            $name,
            $format,
            $params,
            $filters ?? (array) $request->input('filters', []),
            $sort,
            [
                'value' => (string) $request->input('search.value', ''),
                'columns' => $columnSearch,
            ],
            array_values((array) $request->input('visible_columns', []))
        );
    }

    /**
     * Rehydrate a stored request payload.
     *
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        return new static($data);
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        return $this->data;
    }

    /**
     * @param EloquentBuilder|QueryBuilder $query
     * @param array<int|string, string> $columns
     * @param array<int, array{column:string,direction:string}> $sort
     * @param array<string, mixed> $filters
     * @param array{value?:string,columns?:array<string,string>} $search
     */
    protected static function forQuery(
        int $userId,
        $query,
        string $name,
        array $columns,
        string $format,
        array $sort,
        array $filters,
        array $search,
        string $sourceType
    ): self {
        // Eloquent's toBase() applies global scopes to both SQL and bindings.
        // Reading toSql() and getBindings() separately from the Eloquent builder
        // can otherwise produce a mismatched payload when a scope has parameters.
        $queueQuery = $query instanceof EloquentBuilder ? $query->toBase() : $query;
        $connection = $queueQuery->getConnection();

        return new static([
            'user_id' => $userId,
            'name' => $name,
            'format' => $format,
            'source' => [
                'type' => $sourceType,
                'connection' => $connection->getName(),
                'sql' => $queueQuery->toSql(),
                'bindings' => $queueQuery->getBindings(),
            ],
            'filters' => $filters,
            'sort' => $sort,
            'search' => $search,
            'visible_columns' => [],
            'columns' => static::normalizeColumns($columns),
        ]);
    }

    /**
     * Validate queue-safe data and normalize defaults.
     *
     * @param array<string, mixed> $data
     * @return array<string, mixed>
     */
    protected function validate(array $data): array
    {
        $format = strtolower((string) ($data['format'] ?? ''));
        if (! in_array($format, [self::FORMAT_CSV, self::FORMAT_XLSX], true)) {
            throw new InvalidArgumentException('Report download format must be csv or xlsx.');
        }

        $source = (array) ($data['source'] ?? []);
        $sourceType = $source['type'] ?? null;
        if ($sourceType === self::SOURCE_TABLE) {
            $class = $source['class'] ?? '';
            if (! is_string($class) || (! is_subclass_of($class, QueryTable::class)
                && ! is_subclass_of($class, EloquentTable::class))) {
                throw new InvalidArgumentException('Report download table must extend QueryTable or EloquentTable.');
            }
            $source['params'] = (array) ($source['params'] ?? []);
        } elseif (in_array($sourceType, [self::SOURCE_ELOQUENT_QUERY, self::SOURCE_QUERY], true)) {
            if (empty($source['sql']) || ! array_key_exists('bindings', $source)) {
                throw new InvalidArgumentException('Ad-hoc report downloads require SQL and bindings.');
            }
            if (empty($data['columns'])) {
                throw new InvalidArgumentException('Ad-hoc report downloads require explicit columns.');
            }
        } else {
            throw new InvalidArgumentException('Unsupported report download source.');
        }

        $columns = static::normalizeColumns((array) ($data['columns'] ?? []));
        $allowedKeys = collect($columns)->pluck('key')->all();
        $sort = array_map(function ($order) use ($allowedKeys, $sourceType) {
            $column = (string) ($order['column'] ?? '');
            $direction = strtolower((string) ($order['direction'] ?? ''));
            if ($column === '' || ! in_array($direction, ['asc', 'desc'], true)) {
                throw new InvalidArgumentException(
                    'Report download sort entries require a column and asc/desc direction.'
                );
            }
            if ($sourceType !== self::SOURCE_TABLE && ! in_array($column, $allowedKeys, true)) {
                throw new InvalidArgumentException("Unknown ad-hoc report download sort column: {$column}.");
            }

            return ['column' => $column, 'direction' => $direction];
        }, (array) ($data['sort'] ?? []));

        $search = (array) ($data['search'] ?? []);

        $userId = (int) ($data['user_id'] ?? 0);
        if ($userId < 1) {
            throw new InvalidArgumentException('Report downloads require a valid requester user ID.');
        }

        return [
            'user_id' => $userId,
            'name' => trim((string) ($data['name'] ?? 'Report Download')) ?: 'Report Download',
            'format' => $format,
            'source' => $source,
            'filters' => (array) ($data['filters'] ?? []),
            'sort' => $sort,
            'search' => [
                'value' => (string) ($search['value'] ?? ''),
                'columns' => (array) ($search['columns'] ?? []),
            ],
            'visible_columns' => array_values(array_filter((array) ($data['visible_columns'] ?? []), 'is_string')),
            'columns' => $columns,
        ];
    }

    /**
     * @param array<int|string, string|array<string, string>> $columns
     * @return array<int, array{key:string,title:string}>
     */
    protected static function normalizeColumns(array $columns): array
    {
        $normalized = [];
        foreach ($columns as $key => $column) {
            if (is_array($column)) {
                $columnKey = (string) ($column['key'] ?? $column['name'] ?? '');
                $title = (string) ($column['title'] ?? $columnKey);
            } else {
                $columnKey = is_int($key) ? (string) $column : (string) $key;
                $title = is_int($key) ? (string) $column : (string) $column;
            }

            if ($columnKey === '' || ! preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $columnKey)) {
                throw new InvalidArgumentException("Invalid report download column: {$columnKey}.");
            }

            $normalized[] = ['key' => $columnKey, 'title' => $title];
        }

        return $normalized;
    }
}
