<?php

namespace App\Services;

use App\Models\ArtistItem;
use App\Models\Customer;
use App\Models\GiftItem;
use App\Models\MariachiGroup;
use App\Models\Organization;
use App\Models\OtherCostItem;
use App\Models\PaymentMethod;
use App\Models\Serenata;
use App\Models\SerenataPackage;
use App\Models\SerenataPayment;
use App\Models\SerenataStatusHistory;
use App\Models\User;
use App\Support\OwnerWorkspace;
use App\Support\PhoneNumber;
use App\Support\Pricing\CatalogPriceResolver;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class SerenataIntegrationService
{
    private const PLANNED_PAYMENT_PREFIX = '[PLAN]';

    private const SERVICE_STATUS_PENDING = 'Pendiente';

    private const SERVICE_STATUS_CONFIRMED = 'Confirmada';

    private const SERVICE_STATUS_COMPLETED = 'Realizada';

    private const SERVICE_STATUS_CANCELLED = 'Cancelada';

    private const PAYMENT_STATUS_PENDING = 'Pendiente';

    public function catalogForUser(User $user, ?string $forDate = null, ?int $mariachiGroupId = null): array
    {
        $organizationId = (int) ($user->organization_id ?? 0);
        $this->ensureOwnerUser($user);

        $groups = OwnerWorkspace::groupsForUser($user);

        if ($mariachiGroupId && ! $groups->contains(fn ($group) => (int) $group->id === $mariachiGroupId)) {
            throw ValidationException::withMessages([
                'mariachi_group_id' => 'El usuario no tiene acceso a ese grupo mariachi.',
            ]);
        }

        return [
            'groups' => $groups
                ->map(fn ($group) => [
                    'id' => (int) $group->id,
                    'name' => (string) $group->name,
                    'is_default' => (bool) $group->is_default,
                ])->values()->all(),
            'payment_methods' => $this->paymentMethodOptions($organizationId),
            'packages' => $this->serenataPackageOptions($organizationId, $forDate, $mariachiGroupId),
            'gift_options' => $this->giftOptions($organizationId, $forDate, $mariachiGroupId),
            'artist_options' => $this->artistOptions($organizationId, $forDate, $mariachiGroupId),
            'other_cost_options' => $this->otherCostOptions($organizationId, $forDate, $mariachiGroupId),
        ];
    }

    public function findAccessibleSerenata(User $user, string $reference): ?Serenata
    {
        $this->ensureOwnerUser($user);

        $organizationId = (int) ($user->organization_id ?? 0);
        $accessibleGroupIds = OwnerWorkspace::accessibleGroupIds($user);
        $reference = trim($reference);

        if ($organizationId <= 0 || $reference === '' || empty($accessibleGroupIds)) {
            return null;
        }

        return Serenata::query()
            ->with(['customer:id,name', 'mariachiGroup:id,name', 'serenataPackage:id,name'])
            ->where('organization_id', $organizationId)
            ->whereIn('mariachi_group_id', $accessibleGroupIds)
            ->where(function ($query) use ($reference) {
                if (ctype_digit($reference)) {
                    $query->orWhereKey((int) $reference);
                }

                $query->orWhere('code', $reference)
                    ->orWhere('external_reference', $reference);
            })
            ->first();
    }

    public function searchAccessibleSerenatas(User $user, array $filters, int $limit = 10): array
    {
        $this->ensureOwnerUser($user);

        $organizationId = (int) ($user->organization_id ?? 0);
        $accessibleGroupIds = OwnerWorkspace::accessibleGroupIds($user);

        if ($organizationId <= 0 || empty($accessibleGroupIds)) {
            return [
                'serenatas' => [],
                'meta' => [
                    'count' => 0,
                    'limit' => max(1, min(50, $limit)),
                    'filters' => $filters,
                ],
            ];
        }

        $limit = max(1, min(50, $limit));
        $query = Serenata::query()
            ->with(['customer:id,name', 'mariachiGroup:id,name', 'serenataPackage:id,name'])
            ->where('organization_id', $organizationId)
            ->whereIn('mariachi_group_id', $accessibleGroupIds);

        $this->applySearchFilters($query, $filters);

        $serenatas = $query
            ->orderByDesc('event_date')
            ->orderByDesc('event_time')
            ->orderByDesc('id')
            ->limit($limit)
            ->get();

        return [
            'serenatas' => $serenatas
                ->map(fn (Serenata $serenata) => $this->serializeSerenataSummary($serenata))
                ->values()
                ->all(),
            'meta' => [
                'count' => $serenatas->count(),
                'limit' => $limit,
                'filters' => $filters,
            ],
        ];
    }

    public function createForUser(User $user, array $payload, ?string $idempotencyKey = null): array
    {
        $this->ensureOwnerUser($user);

        $organizationId = (int) ($user->organization_id ?? 0);
        $organization = Organization::query()->find($organizationId);
        $submissionToken = trim((string) ($payload['submission_token'] ?? $idempotencyKey ?? ''));
        $externalReference = trim((string) ($payload['external_reference'] ?? ''));

        if ($organizationId <= 0) {
            throw ValidationException::withMessages([
                'user' => 'El usuario no tiene organización asignada.',
            ]);
        }

        if ($submissionToken === '' && $externalReference === '') {
            throw ValidationException::withMessages([
                'submission_token' => 'Envía `submission_token`, `external_reference` o el header `Idempotency-Key`.',
            ]);
        }

        $existing = null;
        if ($externalReference !== '') {
            $existing = $this->findAccessibleSerenata($user, $externalReference);
            if ($existing) {
                return ['serenata' => $existing, 'created' => false];
            }
        }

        $mariachiGroupId = (int) ($payload['mariachi_group_id'] ?? 0);
        $mariachiGroup = MariachiGroup::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->find($mariachiGroupId);

        if (! $mariachiGroup || ! OwnerWorkspace::canAccessGroup($user, $mariachiGroupId)) {
            throw ValidationException::withMessages([
                'mariachi_group_id' => 'Selecciona un grupo musical válido.',
            ]);
        }

        $payload = $this->normalizeAddressHierarchyFields([
            'service_country' => 'Colombia',
            ...$payload,
        ], $organization);
        $payload['service_status'] = $this->normalizeServiceStatus($payload['service_status'] ?? null);
        $payload['contact_phone_1'] = PhoneNumber::normalize((string) ($payload['contact_phone_1'] ?? ''));
        $payload['contact_phone_2'] = PhoneNumber::normalize((string) ($payload['contact_phone_2'] ?? '')) ?: null;

        if ($payload['contact_phone_1'] === '') {
            throw ValidationException::withMessages([
                'contact_phone_1' => 'Ingresa un número de contacto válido.',
            ]);
        }

        if (
            $payload['service_status'] === self::SERVICE_STATUS_CANCELLED
            && empty(trim((string) ($payload['cancellation_reason'] ?? '')))
        ) {
            throw ValidationException::withMessages([
                'cancellation_reason' => 'El motivo de cancelación es obligatorio cuando el estado es Cancelada.',
            ]);
        }

        $package = null;
        $serenataPackageId = (int) ($payload['serenata_package_id'] ?? 0);
        if ($serenataPackageId > 0) {
            $package = SerenataPackage::query()
                ->where('organization_id', $organizationId)
                ->where('mariachi_group_id', $mariachiGroupId)
                ->where('is_active', true)
                ->find($serenataPackageId);

            if (! $package) {
                throw ValidationException::withMessages([
                    'serenata_package_id' => 'Selecciona un paquete válido.',
                ]);
            }
        }

        $methodOptions = collect($this->paymentMethodOptions($organizationId));
        if ($methodOptions->isEmpty()) {
            throw ValidationException::withMessages([
                'payment_method' => 'Primero crea métodos en Configuración > Pagos.',
            ]);
        }

        $selectedMethod = trim((string) ($payload['payment_method'] ?? ''));
        if ($selectedMethod === '') {
            $selectedMethod = (string) (optional($methodOptions->firstWhere('is_default', true))['name'] ?? $methodOptions->first()['name'] ?? '');
        }

        $selectedMethodExists = $methodOptions
            ->pluck('name')
            ->contains(fn ($name) => mb_strtolower((string) $name, 'UTF-8') === mb_strtolower($selectedMethod, 'UTF-8'));

        if (! $selectedMethodExists) {
            throw ValidationException::withMessages([
                'payment_method' => 'Selecciona un método de pago válido.',
            ]);
        }

        $submissionCacheKey = $submissionToken !== ''
            ? "serenata_api_submission:{$organizationId}:{$submissionToken}"
            : null;

        if ($submissionCacheKey && ! Cache::add($submissionCacheKey, true, now()->addMinutes(10))) {
            throw ValidationException::withMessages([
                'submission_token' => 'La solicitud ya se está procesando o fue enviada hace poco.',
            ]);
        }

        $giftCatalog = collect($this->giftOptions($organizationId, (string) $payload['event_date'], $mariachiGroupId))->keyBy('code');
        $artistCatalog = collect($this->artistOptions($organizationId, (string) $payload['event_date'], $mariachiGroupId))->keyBy('code');
        $otherCostCatalog = collect($this->otherCostOptions($organizationId, (string) $payload['event_date'], $mariachiGroupId))->keyBy('code');

        [$selectedGiftItems, $giftTotalFromItems] = $this->mapCatalogItems((array) ($payload['gift_items'] ?? []), $giftCatalog, true);
        [$selectedArtistItems, $artistTotalFromItems] = $this->mapCatalogItems((array) ($payload['artist_items'] ?? []), $artistCatalog, false);
        [$selectedOtherCostItems, $otherCostTotalFromItems] = $this->mapCatalogItems((array) ($payload['other_cost_items'] ?? []), $otherCostCatalog, false);

        $packageTemplate = $package
            ? collect($this->serenataPackageOptions($organizationId, (string) $payload['event_date'], $mariachiGroupId))->firstWhere('id', $package->id)
            : null;

        $base = $payload['base_price'] ?? null;
        if ($base === null && $packageTemplate) {
            $base = (float) ($packageTemplate['base_price'] ?? 0);
        }

        if ($base === null) {
            throw ValidationException::withMessages([
                'base_price' => 'Envía `base_price` o selecciona un paquete que lo defina.',
            ]);
        }

        $base = max(0, (float) $base);
        $gift = $giftTotalFromItems > 0 ? $giftTotalFromItems : max(0, (float) ($payload['gift_amount'] ?? 0));
        $artist = $artistTotalFromItems > 0 ? $artistTotalFromItems : max(0, (float) ($payload['artist_amount'] ?? 0));
        $otherCost = $otherCostTotalFromItems > 0 ? $otherCostTotalFromItems : max(0, (float) ($payload['other_cost_amount'] ?? 0));
        $discount = max(0, (float) ($payload['discount_amount'] ?? 0));
        $baseCost = max(0, $gift + $artist + $otherCost);
        $totalCharged = max(0, $base - $discount);
        $profit = $totalCharged - $baseCost;
        $commissionRate = max(0, min(100, (float) ($payload['commission_rate'] ?? 0)));
        $commissionAmount = $commissionRate > 0
            ? max(0, $profit * ($commissionRate / 100))
            : max(0, $profit);

        $serenata = DB::transaction(function () use (
            $user,
            $organizationId,
            $mariachiGroupId,
            $mariachiGroup,
            $serenataPackageId,
            $payload,
            $externalReference,
            $base,
            $gift,
            $selectedGiftItems,
            $artist,
            $selectedArtistItems,
            $otherCost,
            $selectedOtherCostItems,
            $discount,
            $baseCost,
            $totalCharged,
            $profit,
            $commissionRate,
            $commissionAmount,
            $selectedMethod
        ) {
            $customerId = $this->findOrCreateCustomerId($organizationId, $mariachiGroupId, $payload);

            $nextSequence = ((int) Serenata::query()
                ->where('organization_id', $organizationId)
                ->max('sequence')) + 1;

            if ($nextSequence < 1001) {
                $nextSequence = 1001;
            }

            $serenata = Serenata::query()->create([
                'organization_id' => $organizationId,
                'created_by_user_id' => (int) $user->id,
                'customer_id' => $customerId,
                'mariachi_group_id' => $mariachiGroupId,
                'serenata_package_id' => $serenataPackageId ?: null,
                'sequence' => $nextSequence,
                'code' => $mariachiGroup->serenataCodePrefix().'-'.$nextSequence,
                'external_reference' => $externalReference !== '' ? $externalReference : null,
                'payer_name' => trim((string) $payload['payer_name']),
                'payer_email' => trim((string) ($payload['payer_email'] ?? '')) ?: null,
                'service_address' => $payload['service_address'],
                'address_additional' => $payload['address_additional'] ?? null,
                'service_country' => $payload['service_country'],
                'service_department' => $payload['service_department'] ?: null,
                'neighborhood' => $payload['neighborhood'],
                'locality' => $payload['locality'],
                'service_locality' => $payload['service_locality'] ?: null,
                'service_sector' => $payload['service_sector'] ?: null,
                'contact_phone_1' => $payload['contact_phone_1'],
                'contact_phone_2' => $payload['contact_phone_2'],
                'reason' => trim((string) $payload['reason']),
                'from_name' => trim((string) $payload['from_name']),
                'to_name' => trim((string) $payload['to_name']),
                'event_date' => $payload['event_date'],
                'event_time' => $payload['event_time'],
                'internal_notes' => $payload['internal_notes'] ?? null,
                'client_notes' => $payload['client_notes'] ?? null,
                'base_price' => $base,
                'gift_amount' => $gift,
                'gift_items' => $selectedGiftItems,
                'artist_amount' => $artist,
                'artist_items' => $selectedArtistItems,
                'other_cost_amount' => $otherCost,
                'other_cost_items' => $selectedOtherCostItems,
                'discount_amount' => $discount,
                'base_cost' => $baseCost,
                'total_charged' => $totalCharged,
                'profit_amount' => $profit,
                'commission_rate' => $commissionRate,
                'commission_amount' => $commissionAmount,
                'payment_status' => self::PAYMENT_STATUS_PENDING,
                'service_status' => $payload['service_status'],
                'payment_method' => $selectedMethod,
                'cancellation_reason' => trim((string) ($payload['cancellation_reason'] ?? '')) ?: null,
                'ceded_amount' => 0,
                'is_ceded' => false,
                'ceded_total_cost' => 0,
                'ceded_artist_amount' => 0,
                'ceded_gift_amount' => 0,
                'ceded_transport_amount' => 0,
                'ceded_other_cost_amount' => 0,
                'ceded_excess_amount' => 0,
                'ceded_excess_status' => 'equilibrado',
            ]);

            $this->ensureInitialPaymentRecord($serenata);

            SerenataStatusHistory::query()->create([
                'organization_id' => $organizationId,
                'serenata_id' => $serenata->id,
                'changed_by_user_id' => (int) $user->id,
                'from_service_status' => null,
                'to_service_status' => (string) $serenata->service_status,
                'from_payment_status' => null,
                'to_payment_status' => (string) $serenata->payment_status,
                'note' => 'Creación de serenata vía API',
            ]);

            return $serenata;
        });

        return ['serenata' => $serenata->fresh(['customer:id,name', 'mariachiGroup:id,name', 'serenataPackage:id,name']), 'created' => true];
    }

    public function serializeSerenata(Serenata $serenata): array
    {
        return [
            'id' => (int) $serenata->id,
            'code' => (string) $serenata->code,
            'external_reference' => $serenata->external_reference,
            'organization_id' => (int) $serenata->organization_id,
            'mariachi_group' => $serenata->mariachiGroup ? [
                'id' => (int) $serenata->mariachiGroup->id,
                'name' => (string) $serenata->mariachiGroup->name,
            ] : null,
            'package' => $serenata->serenataPackage ? [
                'id' => (int) $serenata->serenataPackage->id,
                'name' => (string) $serenata->serenataPackage->name,
            ] : null,
            'customer' => $serenata->customer ? [
                'id' => (int) $serenata->customer->id,
                'name' => (string) $serenata->customer->name,
            ] : null,
            'payer_name' => (string) $serenata->payer_name,
            'payer_email' => $serenata->payer_email,
            'contact_phone_1' => (string) $serenata->contact_phone_1,
            'contact_phone_2' => $serenata->contact_phone_2,
            'service_address' => (string) $serenata->service_address,
            'address_additional' => $serenata->address_additional,
            'service_country' => (string) $serenata->service_country,
            'service_department' => $serenata->service_department,
            'neighborhood' => (string) $serenata->neighborhood,
            'locality' => (string) $serenata->locality,
            'service_locality' => $serenata->service_locality,
            'service_sector' => $serenata->service_sector,
            'reason' => (string) $serenata->reason,
            'from_name' => (string) $serenata->from_name,
            'to_name' => (string) $serenata->to_name,
            'event_date' => optional($serenata->event_date)->format('Y-m-d'),
            'event_time' => (string) $serenata->event_time,
            'payment_method' => (string) $serenata->payment_method,
            'payment_status' => (string) $serenata->payment_status,
            'service_status' => (string) $serenata->service_status,
            'base_price' => (float) $serenata->base_price,
            'gift_amount' => (float) $serenata->gift_amount,
            'artist_amount' => (float) $serenata->artist_amount,
            'other_cost_amount' => (float) $serenata->other_cost_amount,
            'discount_amount' => (float) $serenata->discount_amount,
            'base_cost' => (float) $serenata->base_cost,
            'total_charged' => (float) $serenata->total_charged,
            'profit_amount' => (float) $serenata->profit_amount,
            'commission_rate' => (float) $serenata->commission_rate,
            'commission_amount' => (float) $serenata->commission_amount,
            'gift_items' => array_values($serenata->gift_items ?? []),
            'artist_items' => array_values($serenata->artist_items ?? []),
            'other_cost_items' => array_values($serenata->other_cost_items ?? []),
            'internal_notes' => $serenata->internal_notes,
            'client_notes' => $serenata->client_notes,
            'created_at' => optional($serenata->created_at)->toIso8601String(),
            'updated_at' => optional($serenata->updated_at)->toIso8601String(),
        ];
    }

    public function serializeSerenataSummary(Serenata $serenata): array
    {
        return [
            'id' => (int) $serenata->id,
            'code' => (string) $serenata->code,
            'external_reference' => $serenata->external_reference,
            'payer_name' => (string) $serenata->payer_name,
            'payer_email' => $serenata->payer_email,
            'contact_phone_1' => (string) $serenata->contact_phone_1,
            'contact_phone_2' => $serenata->contact_phone_2,
            'event_date' => optional($serenata->event_date)->format('Y-m-d'),
            'event_time' => (string) $serenata->event_time,
            'service_status' => (string) $serenata->service_status,
            'payment_status' => (string) $serenata->payment_status,
            'payment_method' => (string) $serenata->payment_method,
            'total_charged' => (float) $serenata->total_charged,
            'mariachi_group' => $serenata->mariachiGroup ? [
                'id' => (int) $serenata->mariachiGroup->id,
                'name' => (string) $serenata->mariachiGroup->name,
            ] : null,
            'package' => $serenata->serenataPackage ? [
                'id' => (int) $serenata->serenataPackage->id,
                'name' => (string) $serenata->serenataPackage->name,
            ] : null,
            'customer' => $serenata->customer ? [
                'id' => (int) $serenata->customer->id,
                'name' => (string) $serenata->customer->name,
            ] : null,
        ];
    }

    private function ensureOwnerUser(User $user): void
    {
        if (
            ! $user->is_active
            || ! in_array($user->role, [User::ROLE_CLIENT, User::ROLE_OWNER_STAFF], true)
        ) {
            throw ValidationException::withMessages([
                'user' => 'El usuario no tiene permisos para usar esta API.',
            ]);
        }
    }

    private function applySearchFilters(Builder $query, array $filters): void
    {
        $phone = trim((string) ($filters['phone'] ?? ''));
        if ($phone !== '') {
            $candidates = $this->phoneSearchCandidates($phone);
            $query->where(function (Builder $phoneQuery) use ($candidates) {
                foreach ($candidates as $candidate) {
                    $phoneQuery
                        ->orWhere('contact_phone_1', $candidate)
                        ->orWhere('contact_phone_2', $candidate);
                }
            });
        }

        $email = trim((string) ($filters['email'] ?? ''));
        if ($email !== '') {
            $query->whereRaw('LOWER(payer_email) = ?', [mb_strtolower($email, 'UTF-8')]);
        }

        $externalReference = trim((string) ($filters['external_reference'] ?? ''));
        if ($externalReference !== '') {
            $query->where('external_reference', $externalReference);
        }

        $code = trim((string) ($filters['code'] ?? ''));
        if ($code !== '') {
            $query->where('code', $code);
        }

        $eventDate = trim((string) ($filters['event_date'] ?? ''));
        if ($eventDate !== '') {
            $query->whereDate('event_date', $eventDate);
        }
    }

    private function phoneSearchCandidates(string $phone): array
    {
        $normalized = PhoneNumber::normalize($phone);
        $digits = PhoneNumber::digits($phone);
        $localDigits = strlen($digits) > 10 ? substr($digits, -10) : $digits;

        return array_values(array_unique(array_filter([
            $normalized,
            $digits,
            $digits !== '' ? '+'.$digits : '',
            $localDigits,
            $localDigits !== '' ? '+'.$localDigits : '',
        ])));
    }

    private function findOrCreateCustomerId(int $organizationId, int $mariachiGroupId, array $payload): ?int
    {
        $payerName = trim((string) ($payload['payer_name'] ?? ''));
        if ($payerName === '') {
            return null;
        }

        $payerKey = mb_strtolower(trim(preg_replace('/\s+/', ' ', $payerName) ?? ''), 'UTF-8');

        $customer = Customer::query()
            ->where('organization_id', $organizationId)
            ->where('mariachi_group_id', $mariachiGroupId)
            ->whereRaw('LOWER(TRIM(name)) = ?', [$payerKey])
            ->first();

        if (! $customer) {
            $customer = Customer::query()->create([
                'organization_id' => $organizationId,
                'mariachi_group_id' => $mariachiGroupId,
                'name' => $payerName,
                'email' => trim((string) ($payload['payer_email'] ?? '')) ?: null,
                'mobile' => trim((string) ($payload['contact_phone_1'] ?? '')) ?: null,
                'address_line_1' => trim((string) ($payload['service_address'] ?? '')) ?: null,
                'address_line_2' => trim((string) ($payload['address_additional'] ?? '')) ?: null,
                'town' => trim((string) ($payload['locality'] ?? '')) ?: null,
                'state_province' => trim((string) ($payload['service_department'] ?? '')) ?: null,
                'post_code' => null,
                'country' => trim((string) ($payload['service_country'] ?? '')) ?: 'Colombia',
                'is_billing_same' => true,
                'is_active' => true,
            ]);
        } else {
            $updates = [];
            if (empty($customer->email) && ! empty($payload['payer_email'])) {
                $updates['email'] = trim((string) $payload['payer_email']);
            }
            if (empty($customer->mobile) && ! empty($payload['contact_phone_1'])) {
                $updates['mobile'] = trim((string) $payload['contact_phone_1']);
            }
            if (! empty($updates)) {
                $customer->update($updates);
            }
        }

        return (int) $customer->id;
    }

    private function mapCatalogItems(array $items, $catalog, bool $includeLineTotal): array
    {
        $selectedItems = [];
        $total = 0.0;

        foreach ($items as $item) {
            $code = trim((string) data_get($item, 'code', ''));
            $quantity = max(1, (int) data_get($item, 'quantity', 1));

            if ($code === '' || ! $catalog->has($code)) {
                continue;
            }

            $catalogItem = $catalog->get($code);
            $unitPrice = max(0, (float) data_get($item, 'unit_price', data_get($catalogItem, 'price', 0)));
            $lineTotal = round($unitPrice * $quantity, 2);

            $row = [
                'code' => $code,
                'name' => (string) data_get($catalogItem, 'name', $code),
                'unit_price' => $unitPrice,
                'quantity' => $quantity,
            ];

            if ($includeLineTotal) {
                $row['total'] = $lineTotal;
            } elseif (! empty($catalogItem['type'])) {
                $row['type'] = (string) $catalogItem['type'];
            }

            $selectedItems[] = $row;
            $total += $lineTotal;
        }

        return [$selectedItems, $total];
    }

    private function ensureInitialPaymentRecord(Serenata $serenata): void
    {
        if (SerenataPayment::query()->where('serenata_id', $serenata->id)->exists()) {
            return;
        }

        $methodName = trim((string) ($serenata->payment_method ?? ''));
        $method = $methodName !== ''
            ? PaymentMethod::query()
                ->where('organization_id', $serenata->organization_id)
                ->whereRaw('LOWER(name) = ?', [mb_strtolower($methodName, 'UTF-8')])
                ->first()
            : null;

        SerenataPayment::query()->create([
            'organization_id' => $serenata->organization_id,
            'serenata_id' => $serenata->id,
            'payment_method_id' => $method?->id,
            'registered_by_user_id' => $serenata->created_by_user_id,
            'paid_at' => $serenata->created_at ?? now(),
            'payment_method_name' => $method?->name ?: ($methodName !== '' ? $methodName : 'Método no definido'),
            'transaction_reference' => null,
            'amount' => (float) $serenata->total_charged,
            'note' => self::PLANNED_PAYMENT_PREFIX.' Pago acordado pendiente.',
        ]);
    }

    private function giftOptions(?int $organizationId = null, ?string $forDate = null, ?int $mariachiGroupId = null): array
    {
        if (empty($organizationId)) {
            return [];
        }

        return GiftItem::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->when($mariachiGroupId, fn ($items) => $items->where('mariachi_group_id', $mariachiGroupId))
            ->orderBy('mariachi_group_id')
            ->orderBy('type')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(function (GiftItem $item) use ($forDate) {
                $resolved = CatalogPriceResolver::resolve($item, $forDate);

                return [
                    'code' => $item->code,
                    'name' => $item->name,
                    'mariachi_group_id' => (int) $item->mariachi_group_id,
                    'price' => (float) $resolved['customer_price'],
                    'internal_cost' => (float) $resolved['internal_cost'],
                    'type' => $item->type,
                    'is_default' => (bool) $item->is_default,
                ];
            })->values()->all();
    }

    private function artistOptions(?int $organizationId = null, ?string $forDate = null, ?int $mariachiGroupId = null): array
    {
        if (empty($organizationId)) {
            return [];
        }

        return ArtistItem::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->when($mariachiGroupId, fn ($items) => $items->where('mariachi_group_id', $mariachiGroupId))
            ->orderBy('mariachi_group_id')
            ->orderBy('type')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(function (ArtistItem $item) use ($forDate) {
                $resolved = CatalogPriceResolver::resolve($item, $forDate);

                return [
                    'code' => $item->code,
                    'name' => $item->name,
                    'mariachi_group_id' => (int) $item->mariachi_group_id,
                    'price' => (float) $resolved['customer_price'],
                    'internal_cost' => (float) $resolved['internal_cost'],
                    'type' => $item->type,
                ];
            })->values()->all();
    }

    private function otherCostOptions(?int $organizationId = null, ?string $forDate = null, ?int $mariachiGroupId = null): array
    {
        if (empty($organizationId)) {
            return [];
        }

        return OtherCostItem::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->when($mariachiGroupId, fn ($items) => $items->where('mariachi_group_id', $mariachiGroupId))
            ->orderBy('mariachi_group_id')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(function (OtherCostItem $item) use ($forDate) {
                $resolved = CatalogPriceResolver::resolve($item, $forDate);

                return [
                    'code' => $item->code,
                    'name' => $item->name,
                    'mariachi_group_id' => (int) $item->mariachi_group_id,
                    'price' => (float) $resolved['customer_price'],
                    'internal_cost' => (float) $resolved['internal_cost'],
                ];
            })->values()->all();
    }

    private function paymentMethodOptions(?int $organizationId = null): array
    {
        if (empty($organizationId)) {
            return [];
        }

        return PaymentMethod::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->orderByDesc('is_default')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(fn (PaymentMethod $item) => [
                'id' => (int) $item->id,
                'name' => (string) $item->name,
                'account_holder' => $item->account_holder,
                'account_reference' => $item->account_reference,
                'payment_details' => $item->payment_details,
                'is_default' => (bool) $item->is_default,
            ])->values()->all();
    }

    private function serenataPackageOptions(?int $organizationId = null, ?string $forDate = null, ?int $mariachiGroupId = null): array
    {
        if (empty($organizationId)) {
            return [];
        }

        return SerenataPackage::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->when($mariachiGroupId, fn ($items) => $items->where('mariachi_group_id', $mariachiGroupId))
            ->orderBy('mariachi_group_id')
            ->orderByDesc('is_default')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(function (SerenataPackage $package) use ($forDate) {
                $resolved = CatalogPriceResolver::resolve($package, $forDate);

                return [
                    'id' => (int) $package->id,
                    'name' => (string) $package->name,
                    'mariachi_group_id' => (int) $package->mariachi_group_id,
                    'city' => (string) ($package->city ?: '*'),
                    'base_price' => (float) $resolved['customer_price'],
                    'internal_cost' => (float) $resolved['internal_cost'],
                    'gift_codes' => array_values(array_filter($resolved['gift_codes'] ?? $package->gift_codes ?? [])),
                    'artist_codes' => array_values(array_filter($resolved['artist_codes'] ?? $package->artist_codes ?? [])),
                    'other_cost_codes' => array_values(array_filter($resolved['other_cost_codes'] ?? $package->other_cost_codes ?? [])),
                    'is_default' => (bool) $package->is_default,
                ];
            })->values()->all();
    }

    private function normalizeServiceStatus(?string $status): string
    {
        return match (trim((string) $status)) {
            'Confirmado', self::SERVICE_STATUS_CONFIRMED => self::SERVICE_STATUS_CONFIRMED,
            'Completada', self::SERVICE_STATUS_COMPLETED => self::SERVICE_STATUS_COMPLETED,
            'Cancelado', self::SERVICE_STATUS_CANCELLED => self::SERVICE_STATUS_CANCELLED,
            'Cedida', self::SERVICE_STATUS_PENDING => self::SERVICE_STATUS_PENDING,
            default => self::SERVICE_STATUS_PENDING,
        };
    }

    private function normalizeLookupString(?string $value): string
    {
        $clean = trim(preg_replace('/\s+/', ' ', (string) $value) ?? '');
        if ($clean === '') {
            return '';
        }

        $clean = str_replace(
            ['á', 'é', 'í', 'ó', 'ú', 'Á', 'É', 'Í', 'Ó', 'Ú'],
            ['a', 'e', 'i', 'o', 'u', 'a', 'e', 'i', 'o', 'u'],
            $clean
        );

        return mb_strtolower($clean, 'UTF-8');
    }

    private function sameLocationLabel(?string $left, ?string $right): bool
    {
        $leftNormalized = $this->normalizeLookupString($left);
        $rightNormalized = $this->normalizeLookupString($right);

        return $leftNormalized !== '' && $leftNormalized === $rightNormalized;
    }

    private function isBogotaLabel(?string $value): bool
    {
        $lookup = $this->normalizeLookupString($value);

        return in_array($lookup, ['bogota', 'bogota d.c.', 'bogota d c', 'bogota dc'], true);
    }

    private function countryNameFromCode(string $countryCode): string
    {
        return match (strtolower(trim($countryCode))) {
            'co' => 'Colombia',
            'mx' => 'México',
            'us' => 'Estados Unidos',
            'es' => 'España',
            'ar' => 'Argentina',
            'cl' => 'Chile',
            'pe' => 'Perú',
            default => strtoupper(trim($countryCode)),
        };
    }

    private function pickLocationCandidate(array $candidates, array $disallowed = []): string
    {
        foreach ($candidates as $candidate) {
            $candidate = $this->normalizeLocationLabel((string) $candidate);
            if ($candidate === '') {
                continue;
            }

            $isDisallowed = collect($disallowed)->contains(fn ($value) => $this->sameLocationLabel($candidate, (string) $value));
            if (! $isDisallowed) {
                return $candidate;
            }
        }

        return '';
    }

    private function inferSectorFromAddressText(array $rawFields, array $resolvedFields): string
    {
        $prefixes = array_values(array_filter([
            $resolvedFields['service_locality'] ?? '',
            $resolvedFields['locality'] ?? '',
        ]));

        if (empty($prefixes)) {
            return '';
        }

        $disallowed = [
            $resolvedFields['locality'] ?? '',
            $resolvedFields['service_locality'] ?? '',
            $resolvedFields['neighborhood'] ?? '',
        ];

        foreach (['service_sector', 'neighborhood'] as $fieldName) {
            $candidate = $this->normalizeLocationLabel((string) ($rawFields[$fieldName] ?? ''));
            if ($candidate === '') {
                continue;
            }

            $candidate = preg_replace('/^\s*barrio\s+/iu', '', $candidate) ?? $candidate;
            $candidateLookup = $this->normalizeLookupString($candidate);

            foreach ($prefixes as $prefix) {
                $prefix = $this->normalizeLocationLabel((string) $prefix);
                $prefixLookup = $this->normalizeLookupString($prefix);

                if ($prefixLookup === '' || ! str_starts_with($candidateLookup, $prefixLookup.' ')) {
                    continue;
                }

                $fragment = preg_replace('/^\s*'.preg_quote($prefix, '/').'\s+/iu', '', $candidate) ?? '';
                $fragment = trim($this->normalizeLocationLabel((string) $fragment), " \t\n\r\0\x0B,.;:-");

                if ($fragment === '') {
                    continue;
                }

                $isDisallowed = collect($disallowed)->contains(fn ($value) => $this->sameLocationLabel($fragment, (string) $value));
                if (! $isDisallowed) {
                    return $fragment;
                }
            }
        }

        return '';
    }

    private function inferNeighborhoodFallback(array $resolvedFields, array $rawFields = []): string
    {
        $candidates = [
            $resolvedFields['neighborhood'] ?? '',
            $rawFields['neighborhood'] ?? '',
            $resolvedFields['service_sector'] ?? '',
            $rawFields['service_sector'] ?? '',
            $resolvedFields['service_locality'] ?? '',
            $rawFields['service_locality'] ?? '',
        ];

        foreach ($candidates as $candidate) {
            $candidate = $this->normalizeLocationLabel((string) $candidate);
            if ($candidate === '') {
                continue;
            }

            if ($this->sameLocationLabel($candidate, $resolvedFields['locality'] ?? '')) {
                continue;
            }

            return $candidate;
        }

        return '';
    }

    private function normalizeAddressHierarchyFields(array $fields, ?Organization $organization = null): array
    {
        $countryCode = strtolower((string) ($organization?->google_maps_country ?: 'co'));
        $normalized = $fields;
        $addressFields = [
            'service_address' => trim((string) ($fields['service_address'] ?? '')),
            'address_additional' => trim((string) ($fields['address_additional'] ?? '')),
            'service_country' => trim((string) ($fields['service_country'] ?? '')),
            'service_department' => $this->normalizeLocationLabel((string) ($fields['service_department'] ?? '')),
            'locality' => $this->normalizeLocationLabel((string) ($fields['locality'] ?? '')),
            'service_locality' => $this->normalizeLocationLabel((string) ($fields['service_locality'] ?? '')),
            'service_sector' => $this->normalizeLocationLabel((string) ($fields['service_sector'] ?? '')),
            'neighborhood' => $this->normalizeLocationLabel((string) ($fields['neighborhood'] ?? '')),
        ];

        foreach ($addressFields as $key => $value) {
            $normalized[$key] = $value;
        }

        $normalized['service_country'] = $normalized['service_country'] !== ''
            ? $this->normalizeLocationLabel($normalized['service_country'])
            : $this->countryNameFromCode($countryCode);

        if ($this->isBogotaLabel($normalized['locality']) || $this->isBogotaLabel($normalized['service_department'])) {
            $normalized['locality'] = 'Bogotá';
            if ($normalized['service_department'] === '') {
                $normalized['service_department'] = 'Bogotá D.C.';
            }
        }

        if ($normalized['service_locality'] !== '' && $this->sameLocationLabel($normalized['service_locality'], $normalized['locality'])) {
            $normalized['service_locality'] = '';
        }

        if (
            $normalized['service_sector'] !== ''
            && (
                $this->sameLocationLabel($normalized['service_sector'], $normalized['locality'])
                || $this->sameLocationLabel($normalized['service_sector'], $normalized['service_locality'])
                || $this->sameLocationLabel($normalized['service_sector'], $normalized['neighborhood'])
            )
        ) {
            $normalized['service_sector'] = '';
        }

        if ($normalized['service_sector'] === '') {
            $normalized['service_sector'] = $this->inferSectorFromAddressText($fields, $normalized);
        }

        if (($normalized['neighborhood'] ?? '') === '') {
            $normalized['neighborhood'] = $this->inferNeighborhoodFallback($normalized, $fields);
        }

        return $normalized;
    }

    private function normalizeLocationLabel(string $value): string
    {
        $clean = trim(preg_replace('/\s+/', ' ', $value) ?? '');
        if ($clean === '') {
            return '';
        }

        $normalized = mb_convert_case($clean, MB_CASE_TITLE, 'UTF-8');

        return preg_replace('/\bD\.?\s*C\.?\b/u', 'D.C.', $normalized) ?? $normalized;
    }
}
