<?php

namespace App\Exports;

use App\Enums\ExportColumnJsonType;
use App\Models\ExportMap;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;

class JsonMapExport
{
    public const TOKEN_RESULTS = '__RESULTS__';
    public const TOKEN_COUNT = '__COUNT__';
    public const TOKEN_GENERATED_AT = '__GENERATED_AT__';
    public const TOKEN_TENANT_ID = '__TENANT_ID__';
    public const TOKEN_VALUE = '__VALUE__';

    public function __construct(protected Collection $data, protected ExportMap $exportMap)
    {
    }

    /**
     * Render the export map data as the JSON payload to send to the client.
     */
    public function toArray(): array
    {
        $rows = $this->buildRows();

        $envelope = $this->exportMap->getJsonEnvelope();
        if ($envelope === null) {
            return $rows;
        }

        $template = json_decode($envelope, true);
        if (!is_array($template)) {
            return $rows;
        }

        return $this->substituteTokens($template, $rows);
    }

    /**
     * Build the list of typed row arrays from the export map data.
     */
    protected function buildRows(): array
    {
        $columns = $this->exportMap->columns ?? [];
        $headers = $this->exportMap->headers ?? [];
        $types = $this->exportMap->getColumnJsonTypes();
        $paths = $this->exportMap->getColumnJsonPaths();

        $keys = [];
        foreach ($columns as $index => $column) {
            $header = $headers[$index] ?? null;
            $keys[$index] = (is_string($header) && trim($header) !== '') ? $header : (string) $column;
        }

        return $this->data->map(function ($row) use ($columns, $keys, $types, $paths) {
            $out = [];
            $values = is_array($row) ? array_values($row) : array_values((array) $row);
            foreach ($columns as $index => $_column) {
                $type = $types[$index] ?? ExportColumnJsonType::STRING->value;
                $path = $paths[$index] ?? null;
                $key = $keys[$index];
                $value = $this->castValue($values[$index] ?? null, $type, $path);
                if (str_contains($key, '.')) {
                    Arr::set($out, $key, $value);
                } else {
                    $out[$key] = $value;
                }
            }
            return $out;
        })->all();
    }

    /**
     * Cast a single raw value according to the configured per-column JSON type.
     */
    protected function castValue($value, string $type, ?string $pathKey)
    {
        return match ($type) {
            ExportColumnJsonType::NUMBER->value => $this->toNumber($value),
            ExportColumnJsonType::BOOLEAN->value => $this->toBoolean($value),
            ExportColumnJsonType::ARRAY_OF_STRINGS->value => $this->toStringArray($value),
            ExportColumnJsonType::ARRAY_OF_OBJECTS->value => $this->toObjectArray($value, $pathKey ?: 'path'),
            default => $this->toScalarString($value),
        };
    }

    protected function toScalarString($value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }
        if (is_bool($value)) {
            return $value ? 'true' : 'false';
        }
        if ($value instanceof \DateTimeInterface) {
            return $value->format('Y-m-d');
        }
        if (is_array($value)) {
            return implode(', ', array_map(fn ($v) => (string) $v, $value));
        }
        return (string) $value;
    }

    protected function toNumber($value)
    {
        if ($value === null || $value === '') {
            return null;
        }
        if (is_int($value) || is_float($value)) {
            return $value;
        }
        if (is_bool($value)) {
            return $value ? 1 : 0;
        }
        $string = is_string($value) ? $value : (string) $value;
        $cleaned = preg_replace('/[^\d.\-]+/', '', $string);
        if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
            return null;
        }
        return str_contains($cleaned, '.') ? (float) $cleaned : (int) $cleaned;
    }

    protected function toBoolean($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if ($value === null) {
            return false;
        }
        if (is_numeric($value)) {
            return (float) $value !== 0.0;
        }
        if (is_string($value)) {
            $normalised = strtolower(trim($value));
            return !in_array($normalised, ['', '0', 'n', 'no', 'false', 'off'], true);
        }
        return (bool) $value;
    }

    protected function toStringArray($value): array
    {
        return $this->splitListValue($value);
    }

    protected function toObjectArray($value, ?string $pathConfig): array
    {
        $items = $this->splitListValue($value);
        $template = $this->parsePathTemplate($pathConfig);

        if (is_array($template)) {
            return array_map(
                fn ($item) => $this->substituteValueToken($template, $item),
                $items
            );
        }

        $key = (is_string($template) && $template !== '') ? $template : 'path';
        return array_map(fn ($item) => [$key => $item], $items);
    }

    /**
     * Path config is either a single key name like "path" (single-key mode)
     * or a JSON object literal like {"url": "__VALUE__", "alt": "..."} (template mode).
     */
    protected function parsePathTemplate(?string $pathConfig)
    {
        if ($pathConfig === null || trim($pathConfig) === '') {
            return null;
        }

        $trimmed = trim($pathConfig);
        if ($trimmed[0] !== '{') {
            return $pathConfig;
        }

        $decoded = json_decode($trimmed, true);
        return (is_array($decoded) && !array_is_list($decoded)) ? $decoded : $pathConfig;
    }

    /**
     * Recursively walk the template; replace any "__VALUE__" string with the item.
     */
    protected function substituteValueToken(array $template, $item): array
    {
        $walker = function ($node) use (&$walker, $item) {
            if (is_array($node)) {
                $result = [];
                foreach ($node as $key => $value) {
                    $result[$key] = $walker($value);
                }
                return $result;
            }
            if (is_string($node) && $node === self::TOKEN_VALUE) {
                return $item;
            }
            return $node;
        };

        return $walker($template);
    }

    protected function splitListValue($value): array
    {
        if ($value === null || $value === '') {
            return [];
        }
        if (is_array($value)) {
            return array_values(array_filter(
                array_map(fn ($v) => is_scalar($v) ? trim((string) $v) : null, $value),
                fn ($v) => $v !== null && $v !== ''
            ));
        }
        $parts = preg_split('/\s*,\s*/', (string) $value, -1, PREG_SPLIT_NO_EMPTY);
        return $parts === false ? [] : array_values(array_filter($parts, fn ($v) => $v !== ''));
    }

    /**
     * Recursively walk the parsed envelope template and replace reserved tokens
     * with typed substitutions (rows array, integer count, ISO timestamp, tenant id).
     */
    protected function substituteTokens(array $template, array $rows): array
    {
        $tokens = $this->tokenValues($rows);

        $walker = function ($node) use (&$walker, $tokens) {
            if (is_array($node)) {
                $result = [];
                foreach ($node as $key => $value) {
                    $result[$key] = $walker($value);
                }
                return $result;
            }
            if (is_string($node) && array_key_exists($node, $tokens)) {
                return $tokens[$node];
            }
            return $node;
        };

        return $walker($template);
    }

    protected function tokenValues(array $rows): array
    {
        return [
            self::TOKEN_RESULTS => $rows,
            self::TOKEN_COUNT => count($rows),
            self::TOKEN_GENERATED_AT => Carbon::now()->toIso8601String(),
            self::TOKEN_TENANT_ID => function_exists('tenant') && tenant() ? tenant('id') : null,
        ];
    }

    /**
     * Token catalogue exposed to the frontend for UI hints.
     */
    public static function envelopeTokenCatalogue(): array
    {
        return [
            ['token' => self::TOKEN_RESULTS, 'description' => 'The rows array (required somewhere in the envelope).'],
            ['token' => self::TOKEN_COUNT, 'description' => 'Total row count (integer).'],
            ['token' => self::TOKEN_GENERATED_AT, 'description' => 'ISO 8601 timestamp of generation.'],
            ['token' => self::TOKEN_TENANT_ID, 'description' => 'Current tenant identifier.'],
        ];
    }
}
