<?php

namespace App\Domains\Tenant\Services;

use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Attachable\Services\AttachmentService;
use App\Domains\Shared\Rules\CroppedImageOrBase64Rule;
use App\Domains\Shared\Rules\ImageOrBase64Rule;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Models\WebsiteExtra;
use App\Domains\Tenant\Models\WebsiteType;
use App\Domains\Tenant\Models\WebsiteTypeExtra;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;

class WebsiteExtraService
{
    public function __construct(protected AttachmentService $attachmentService) {}

    /**
     * Build the request rules declared by the selected website type.
     *
     * @return array<string, mixed>
     */
    public function requestRules(?string $websiteTypeCode): array
    {
        if (! is_string($websiteTypeCode) || $websiteTypeCode === '') {
            return [
                'extras' => ['prohibited'],
            ];
        }

        $definitions = $this->definitionsFor($websiteTypeCode);
        $allowedCodes = $definitions->pluck('codigo')->all();
        $hasRequiredExtras = $definitions->contains(
            fn (WebsiteTypeExtra $definition): bool => $definition->is_required
        );

        $rules = [
            'extras' => [
                $hasRequiredExtras ? 'required' : 'sometimes',
                'array',
                function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void {
                    if (! is_array($value)) {
                        return;
                    }

                    $unknownCodes = array_diff(array_keys($value), $allowedCodes);

                    if ($unknownCodes !== []) {
                        $fail(
                            'The '.$attribute.' field contains extras not supported by the website type: '
                            .implode(', ', $unknownCodes).'.'
                        );
                    }
                },
            ],
        ];

        foreach ($definitions as $definition) {
            $schemaRules = $definition->config_schema['request_rules'] ?? [];
            $rootRules = $this->compileRules($schemaRules['$'] ?? []);
            $rootRules = array_values(array_filter(
                $rootRules,
                fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
            ));
            array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes');

            $rules["extras.{$definition->codigo}"] = $rootRules;

            foreach ($schemaRules as $path => $pathRules) {
                if ($path === '$') {
                    continue;
                }

                $rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules);
            }
        }

        return $rules;
    }

    /**
     * Transform and persist each extra selected for a tenant.
     *
     * @param  array<string, mixed>  $extras
     */
    public function createForTenant(Tenant $tenant, array $extras): void
    {
        if ($extras === []) {
            return;
        }

        $definitions = $this->definitionsFor((string) $tenant->website_type_code)
            ->keyBy('codigo');

        foreach ($extras as $name => $config) {
            /** @var WebsiteTypeExtra|null $definition */
            $definition = $definitions->get($name);

            if (! $definition) {
                throw ValidationException::withMessages([
                    'extras' => ["The extra {$name} is not supported by the selected website type."],
                ]);
            }

            $requestRoot = "extras.{$definition->codigo}";
            $transformedConfig = $this->applyTransforms(
                $tenant,
                $definition,
                $config,
                $requestRoot
            );
            $tenant->websiteExtras()->create([
                'website_type_extra_id' => $definition->id,
                'config' => $transformedConfig,
            ]);
        }

        $tenant->unsetRelation('websiteExtras');
    }

    /**
     * Build request rules for one extra addressed by its stable code.
     *
     * @return array<string, mixed>
     */
    public function requestRulesForExtra(Tenant $tenant, string $extraCode): array
    {
        $definition = $this->definitionForTenant($tenant, $extraCode);
        $schemaRules = $definition->config_schema['request_rules'] ?? [];
        $rootRules = $this->compileRules($schemaRules['$'] ?? []);
        $rootRules = array_values(array_filter(
            $rootRules,
            fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
        ));
        array_unshift($rootRules, 'required');

        $rules = ['config' => $rootRules];

        foreach ($schemaRules as $path => $pathRules) {
            if ($path === '$') {
                continue;
            }

            $rules[$this->configAttribute('config', $path)] = $this->compileRules($pathRules);
        }

        return $rules;
    }

    public function updateForTenant(Tenant $tenant, string $extraCode, mixed $config): WebsiteExtra
    {
        $definition = $this->definitionForTenant($tenant, $extraCode);

        return DB::transaction(function () use ($tenant, $definition, $config): WebsiteExtra {
            $transformedConfig = $this->applyTransforms($tenant, $definition, $config, 'config');

            return $tenant->websiteExtras()->updateOrCreate(
                ['website_type_extra_id' => $definition->id],
                ['config' => $transformedConfig]
            );
        });
    }

    public function toggleForTenant(Tenant $tenant, string $extraCode): WebsiteExtra
    {
        $definition = $this->definitionForTenant($tenant, $extraCode);

        return DB::transaction(function () use ($tenant, $definition): WebsiteExtra {
            $websiteExtra = $tenant->websiteExtras()
                ->where('website_type_extra_id', $definition->id)
                ->lockForUpdate()
                ->first();

            if (! $websiteExtra) {
                return $tenant->websiteExtras()->create([
                    'website_type_extra_id' => $definition->id,
                    'config' => [],
                    'is_enabled' => true,
                ]);
            }

            $websiteExtra->update([
                'is_enabled' => ! $websiteExtra->is_enabled,
            ]);

            return $websiteExtra;
        });
    }

    public function definitionForTenant(Tenant $tenant, string $extraCode): WebsiteTypeExtra
    {
        return WebsiteTypeExtra::query()
            ->where('website_type_code', $tenant->website_type_code)
            ->where('codigo', $extraCode)
            ->firstOrFail();
    }

    /**
     * @return Collection<int, WebsiteTypeExtra>
     */
    private function definitionsFor(string $websiteTypeCode): Collection
    {
        $websiteType = WebsiteType::query()
            ->where('codigo', $websiteTypeCode)
            ->with('extras')
            ->first();

        return $websiteType?->extras ?? collect();
    }

    /**
     * @param  string|array<int, mixed>  $rules
     * @return array<int, mixed>
     */
    private function compileRules(string|array $rules): array
    {
        $compiled = is_string($rules) ? explode('|', $rules) : $rules;

        return array_map(
            fn (mixed $rule): mixed => match ($rule) {
                'image_or_base64' => new ImageOrBase64Rule,
                'cropped_image_or_base64' => new CroppedImageOrBase64Rule,
                default => $rule,
            },
            $compiled
        );
    }

    private function requestAttribute(string $extraCode, string $path): string
    {
        return $this->configAttribute("extras.{$extraCode}", $path);
    }

    private function configAttribute(string $root, string $path): string
    {
        if ($path === '$') {
            return $root;
        }

        if (str_starts_with($path, '$.')) {
            $path = substr($path, 2);
        }

        return "{$root}.{$path}";
    }

    private function applyTransforms(
        Tenant $tenant,
        WebsiteTypeExtra $definition,
        mixed $config,
        string $requestRoot
    ): mixed {
        foreach ($definition->config_schema['transforms'] ?? [] as $path => $transform) {
            $segments = $this->pathSegments($path);
            $config = $this->transformAtPath(
                $config,
                $segments,
                fn (mixed $value): mixed => $this->transformValue(
                    $tenant,
                    $definition,
                    $path,
                    $value,
                    $transform,
                    $requestRoot
                )
            );
        }

        return $config;
    }

    /**
     * @return array<int, string>
     */
    private function pathSegments(string $path): array
    {
        $path = ltrim($path, '$');
        $path = ltrim($path, '.');

        return $path === '' ? [] : explode('.', $path);
    }

    private function transformAtPath(mixed $value, array $segments, callable $transform): mixed
    {
        if ($segments === []) {
            return $transform($value);
        }

        if (! is_array($value)) {
            return $value;
        }

        $segment = array_shift($segments);

        if ($segment === '*') {
            foreach ($value as $key => $item) {
                $value[$key] = $this->transformAtPath($item, $segments, $transform);
            }

            return $value;
        }

        if (array_key_exists($segment, $value)) {
            $value[$segment] = $this->transformAtPath($value[$segment], $segments, $transform);
        }

        return $value;
    }

    /**
     * @param  array<string, mixed>  $transform
     */
    private function transformValue(
        Tenant $tenant,
        WebsiteTypeExtra $definition,
        string $path,
        mixed $value,
        array $transform,
        string $requestRoot
    ): mixed {
        if ($value === null) {
            return null;
        }

        if (($transform['handler'] ?? null) !== 'attachment') {
            throw new InvalidArgumentException(
                "Unsupported transform handler for {$definition->codigo}: ".($transform['handler'] ?? 'null')
            );
        }

        $crops = $this->cropVariants($value);
        $image = is_array($value) ? $value['image'] ?? null : $value;

        if (is_string($image) && Str::isUuid($image)) {
            $attachment = Attachment::query()->where('key', $image)->first();

            if (! $attachment) {
                throw ValidationException::withMessages([
                    $this->configAttribute($requestRoot, $path) => [
                        'The selected attachment does not exist.',
                    ],
                ]);
            }

            if ($crops !== null) {
                $attachment = $this->attachmentService->updateImageCropVariants(
                    $attachment,
                    $crops,
                );
            }
        } elseif ($crops !== null) {
            $attachment = $this->attachmentService->storeCroppedImageVariants(
                $image,
                "tenants/{$tenant->codigo}/extras/{$definition->codigo}",
                $crops,
            );
        } else {
            $attachment = $this->attachmentService->store(
                $image,
                "tenants/{$tenant->codigo}/extras/{$definition->codigo}"
            );
        }

        $expectedType = $transform['attachment_type'] ?? null;

        if (
            is_string($expectedType)
            && $attachment->type instanceof AttachmentType
            && $attachment->type->value !== $expectedType
        ) {
            throw ValidationException::withMessages([
                $this->configAttribute($requestRoot, $path) => [
                    "The attachment must be of type {$expectedType}.",
                ],
            ]);
        }

        return $attachment->id;
    }

    /**
     * Normalize the current variants payload and the original single-crop contract.
     *
     * @return array<string, array<string, mixed>>|null
     */
    private function cropVariants(mixed $value): ?array
    {
        if (! is_array($value)) {
            return null;
        }

        if (isset($value['crops']) && is_array($value['crops'])) {
            return $value['crops'];
        }

        $horizontal = $value['crop_horizontal'] ?? null;
        $vertical = $value['crop_vertical'] ?? null;

        if (! is_array($horizontal) || ! is_array($vertical)) {
            return null;
        }

        $crop = [
            'crop_horizontal' => $horizontal,
            'crop_vertical' => $vertical,
        ];

        return [
            'desktop' => $crop,
            'mobile' => $crop,
        ];
    }
}
