<?php

namespace App\Http\Controllers\Owner;

use App\Http\Controllers\Controller;
use App\Models\ArtistItem;
use App\Models\Customer;
use App\Models\ExternalPartner;
use App\Models\ExternalPartnerMovement;
use App\Models\GiftItem;
use App\Models\MariachiGroup;
use App\Models\OtherCostItem;
use App\Models\Organization;
use App\Models\PaymentMethod;
use App\Models\Serenata;
use App\Models\SerenataPackage;
use App\Models\SerenataPayment;
use App\Models\SerenataStatusHistory;
use App\Support\OwnerWorkspace;
use App\Support\PhoneNumber;
use App\Support\Pricing\CatalogPriceResolver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class SerenataController extends Controller
{
    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';
    private const PAYMENT_STATUS_PARTIAL = 'Abono parcial';
    private const PAYMENT_STATUS_PAID = 'Pagado';
    private const PAYMENT_STATUS_CANCELLED_UNPAID = 'Cancelada sin cobro';

    private function mutationResponse(Request $request, string $message, array $extra = [])
    {
        if ($request->expectsJson() || $request->ajax()) {
            return response()->json(array_merge(['message' => $message], $extra));
        }

        return back()->with('status', $message);
    }

    private function mutationErrorResponse(Request $request, string $message, int $status = 422)
    {
        if ($request->expectsJson() || $request->ajax()) {
            return response()->json(['message' => $message], $status);
        }

        return back()->withErrors(['error' => $message]);
    }

    private function ensureAccessibleSerenata(Request $request, Serenata $serenata): int
    {
        $organizationId = (int) ($request->user()->organization_id ?? 0);

        if (
            $organizationId <= 0
            || $serenata->organization_id !== $organizationId
            || !OwnerWorkspace::canAccessGroup($request->user(), (int) $serenata->mariachi_group_id)
        ) {
            abort(404);
        }

        return $organizationId;
    }

    private function isRefundPaymentNote(?string $note): bool
    {
        return str_contains(trim((string) $note), '[REFUND]');
    }

    private function isPlannedPaymentNote(?string $note): bool
    {
        $normalized = trim((string) $note);

        if ($normalized === '') {
            return false;
        }

        return str_starts_with($normalized, self::PLANNED_PAYMENT_PREFIX) && !$this->isRefundPaymentNote($normalized);
    }

    private function paidAmountForSerenata(int $serenataId): float
    {
        return (float) SerenataPayment::query()
            ->where('serenata_id', $serenataId)
            ->where(function ($query) {
                $query->whereNull('note')
                    ->orWhere('note', 'not like', self::PLANNED_PAYMENT_PREFIX . '%');
            })
            ->orWhere(function ($query) use ($serenataId) {
                $query->where('serenata_id', $serenataId)
                    ->where('note', 'like', '%[REFUND]%');
            })
            ->sum('amount');
    }

    private function recalculateSerenataFinancials(Serenata $serenata): array
    {
        $giftTotal = collect($serenata->gift_items ?? [])->sum(function ($item) {
            if ((bool) data_get($item, 'is_deleted', false)) {
                return 0;
            }
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = max(0, (float) data_get($item, 'unit_price', 0));
            return (float) data_get($item, 'total', $qty * $unit);
        });

        $artistTotal = collect($serenata->artist_items ?? [])->sum(function ($item) {
            if ((bool) data_get($item, 'is_deleted', false)) {
                return 0;
            }
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = max(0, (float) data_get($item, 'unit_price', 0));
            return $qty * $unit;
        });

        $otherCostTotal = collect($serenata->other_cost_items ?? [])->sum(function ($item) {
            if ((bool) data_get($item, 'is_deleted', false)) {
                return 0;
            }
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = max(0, (float) data_get($item, 'unit_price', 0));
            return $qty * $unit;
        });

        $baseCost = max(0, $giftTotal + $artistTotal + $otherCostTotal);
        $effectiveCustomerCharge = $this->actualCustomerChargeAmount($serenata);
        $profit = $effectiveCustomerCharge - $baseCost;
        $commissionRate = max(0, min(100, (float) $serenata->commission_rate));
        $commissionAmount = $commissionRate > 0
            ? max(0, $profit * ($commissionRate / 100))
            : max(0, $profit);

        return [
            'gift_amount' => $giftTotal,
            'artist_amount' => $artistTotal,
            'other_cost_amount' => $otherCostTotal,
            'base_cost' => $baseCost,
            'profit_amount' => $profit,
            'commission_amount' => $commissionAmount,
        ];
    }

    private function getItemsBySource(Serenata $serenata, string $source): array
    {
        return match ($source) {
            'gift' => array_values($serenata->gift_items ?? []),
            'artist' => array_values($serenata->artist_items ?? []),
            'other_cost' => array_values($serenata->other_cost_items ?? []),
            default => [],
        };
    }

    private function setItemsBySource(Serenata $serenata, string $source, array $items): void
    {
        match ($source) {
            'gift' => $serenata->gift_items = array_values($items),
            'artist' => $serenata->artist_items = array_values($items),
            'other_cost' => $serenata->other_cost_items = array_values($items),
            default => null,
        };
    }

    private function itemChargeAmount(array $item): float
    {
        $qty = max(1, (int) data_get($item, 'quantity', 1));
        $unit = max(0, (float) data_get($item, 'unit_price', 0));
        return max(0, (float) data_get($item, 'charge_amount', $qty * $unit));
    }

    private function actualCustomerChargeAmount(Serenata $serenata): float
    {
        if ($serenata->actual_customer_charge !== null) {
            return max(0, (float) $serenata->actual_customer_charge);
        }

        return max(0, (float) $serenata->total_charged);
    }

    private function resolvePaymentStatus(Serenata $serenata, ?float $totalPaid = null): string
    {
        $netPaid = round($totalPaid ?? $this->paidAmountForSerenata($serenata->id), 2);
        $expectedAmount = round($this->actualCustomerChargeAmount($serenata), 2);
        $serviceStatus = $this->normalizeServiceStatus((string) $serenata->service_status);

        if ($serviceStatus === self::SERVICE_STATUS_CANCELLED && $netPaid < 1) {
            return self::PAYMENT_STATUS_CANCELLED_UNPAID;
        }

        if ($expectedAmount < 1) {
            return $netPaid < 1 ? self::PAYMENT_STATUS_PENDING : self::PAYMENT_STATUS_PAID;
        }

        if ($netPaid >= ($expectedAmount - 0.99)) {
            return self::PAYMENT_STATUS_PAID;
        }

        if ($netPaid > 0) {
            return self::PAYMENT_STATUS_PARTIAL;
        }

        return self::PAYMENT_STATUS_PENDING;
    }

    private function syncPaymentStatus(Serenata $serenata, ?float $totalPaid = null): string
    {
        $serenata->payment_status = $this->resolvePaymentStatus($serenata, $totalPaid);

        return (string) $serenata->payment_status;
    }

    private function serviceStatusOptions(): array
    {
        return [
            self::SERVICE_STATUS_PENDING,
            self::SERVICE_STATUS_CONFIRMED,
            self::SERVICE_STATUS_COMPLETED,
            self::SERVICE_STATUS_CANCELLED,
        ];
    }

    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 activeServiceItemsBySource(Serenata $serenata): array
    {
        $mapItems = static function (array $items): array {
            return collect($items)
                ->mapWithKeys(function ($item, $index) {
                    if ((bool) data_get($item, 'is_deleted', false)) {
                        return [];
                    }

                    return [
                        (int) $index => [
                            'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Ítem')),
                            'quantity' => max(1, (int) data_get($item, 'quantity', 1)),
                            'unit_price' => round(max(0, (float) data_get($item, 'unit_price', 0)), 2),
                        ],
                    ];
                })
                ->all();
        };

        return [
            'artist' => $mapItems(array_values($serenata->artist_items ?? [])),
            'gift' => $mapItems(array_values($serenata->gift_items ?? [])),
            'other_cost' => $mapItems(array_values($serenata->other_cost_items ?? [])),
        ];
    }

    private function syncCededBreakdownToCurrentCosts(Serenata $serenata, array $previousActiveItemsBySource = []): void
    {
        if (!(bool) $serenata->is_ceded) {
            return;
        }

        $savedBreakdown = $this->normalizeCededCostBreakdown($serenata->ceded_cost_breakdown ?? []);
        $savedLookup = collect($savedBreakdown)
            ->filter(fn ($item) => is_array($item))
            ->mapWithKeys(function (array $item) {
                $source = (string) ($item['source'] ?? '');
                $index = array_key_exists('index', $item) ? (string) $item['index'] : 'x';

                if ($source === '') {
                    return [];
                }

                return [$source . ':' . $index => $item];
            });

        $currentActiveItemsBySource = $this->activeServiceItemsBySource($serenata);
        $transportItem = collect($savedBreakdown)
            ->first(fn ($item) => is_array($item) && (($item['source'] ?? '') === 'transport'));

        $savedAutoTotal = round((float) collect($savedBreakdown)
            ->reject(fn ($item) => ($item['source'] ?? '') === 'transport')
            ->sum('total') + (float) ($transportItem['total'] ?? 0), 2);
        $currentStoredTotal = round(max(0, (float) $serenata->ceded_total_cost), 2);
        $hasManualTotalOverride = !empty($savedBreakdown) && abs($currentStoredTotal - $savedAutoTotal) >= 1;

        $rebuiltBreakdown = collect(['artist', 'gift', 'other_cost'])
            ->flatMap(function (string $source) use ($currentActiveItemsBySource, $previousActiveItemsBySource, $savedLookup) {
                return collect($currentActiveItemsBySource[$source] ?? [])->map(function (array $item, int $index) use ($source, $previousActiveItemsBySource, $savedLookup) {
                    $lookupKey = $source . ':' . $index;
                    $saved = $savedLookup->get($lookupKey, []);
                    $previous = $previousActiveItemsBySource[$source][$index] ?? null;

                    $quantity = (int) $item['quantity'];
                    $unitPrice = (float) $item['unit_price'];

                    if (!empty($saved)) {
                        $savedQuantity = max(1, (int) ($saved['quantity'] ?? $quantity));
                        $savedUnitPrice = round(max(0, (float) ($saved['unit_price'] ?? $unitPrice)), 2);
                        $savedMatchesPrevious = is_array($previous)
                            && $savedQuantity === max(1, (int) ($previous['quantity'] ?? $quantity))
                            && abs($savedUnitPrice - round(max(0, (float) ($previous['unit_price'] ?? $unitPrice)), 2)) < 0.01;

                        if (!$savedMatchesPrevious) {
                            $quantity = $savedQuantity;
                            $unitPrice = $savedUnitPrice;
                        }
                    }

                    $included = array_key_exists('included', $saved) ? (bool) $saved['included'] : true;

                    return [
                        'source' => $source,
                        'index' => $index,
                        'name' => (string) ($saved['name'] ?? $item['name'] ?? 'Ítem'),
                        'quantity' => $quantity,
                        'unit_price' => $unitPrice,
                        'included' => $included,
                        'total' => $included ? round($quantity * $unitPrice, 2) : 0,
                    ];
                });
            })
            ->values();

        $transportAmount = round(max(0, (float) ($transportItem['unit_price'] ?? $serenata->ceded_transport_amount ?? 0)), 2);
        if ($transportAmount > 0 || !empty($transportItem)) {
            $rebuiltBreakdown->push([
                'source' => 'transport',
                'index' => 0,
                'name' => 'Transporte manual',
                'quantity' => 1,
                'unit_price' => $transportAmount,
                'included' => $transportAmount > 0,
                'total' => $transportAmount > 0 ? $transportAmount : 0,
            ]);
        }

        $artistAmount = round((float) $rebuiltBreakdown->where('source', 'artist')->sum('total'), 2);
        $giftAmount = round((float) $rebuiltBreakdown->where('source', 'gift')->sum('total'), 2);
        $otherCostAmount = round((float) $rebuiltBreakdown->where('source', 'other_cost')->sum('total'), 2);
        $transportAmount = round((float) $rebuiltBreakdown->where('source', 'transport')->sum('total'), 2);
        $calculatedTotal = round($artistAmount + $giftAmount + $otherCostAmount + $transportAmount, 2);
        $cededTotalCost = $hasManualTotalOverride ? $currentStoredTotal : $calculatedTotal;
        $overview = $this->resolveCededOverview($this->actualCustomerChargeAmount($serenata), $cededTotalCost);

        $serenata->ceded_cost_breakdown = $rebuiltBreakdown->all();
        $serenata->ceded_artist_amount = $artistAmount;
        $serenata->ceded_gift_amount = $giftAmount;
        $serenata->ceded_other_cost_amount = $otherCostAmount;
        $serenata->ceded_transport_amount = $transportAmount;
        $serenata->ceded_total_cost = $cededTotalCost;
        $serenata->ceded_excess_amount = (float) $overview['amount'];
        $serenata->ceded_excess_status = (string) $overview['status'];
        $serenata->ceded_amount = (float) $overview['signed_amount'];
    }

    private function ensureInitialPaymentRecord(Serenata $serenata): void
    {
        $paymentsQuery = SerenataPayment::query()->where('serenata_id', $serenata->id);
        $hasPayments = (clone $paymentsQuery)->exists();

        if ($hasPayments) {
            $legacyPlaceholder = (clone $paymentsQuery)
                ->where('amount', '<=', 0)
                ->where('note', 'Registro inicial de método de pago.')
                ->latest('id')
                ->first();

            if ($legacyPlaceholder) {
                $isPaid = (string) $serenata->payment_status === self::PAYMENT_STATUS_PAID;
                $legacyPlaceholder->update([
                    'amount' => (float) $serenata->total_charged,
                    'note' => $isPaid
                        ? 'Pago inicial registrado en creación.'
                        : self::PLANNED_PAYMENT_PREFIX . ' Pago acordado pendiente.',
                ]);
            }

            return;
        }

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

        $isPaid = (string) $serenata->payment_status === self::PAYMENT_STATUS_PAID;
        $initialAmount = (float) $serenata->total_charged;

        SerenataPayment::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' => $initialAmount,
            'note' => $isPaid
                ? 'Pago inicial registrado en creación.'
                : self::PLANNED_PAYMENT_PREFIX . ' Pago acordado pendiente.',
        ]);
    }

    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'] = '';
        }

        return $normalized;
    }

    private function resolveAddressWithGoogle(?Organization $organization, array $fields, string $placeId = ''): array
    {
        $googleMapsApiKey = $organization?->google_maps_api_key;
        if (empty($googleMapsApiKey)) {
            return [
                'success' => false,
                'reason' => 'google_maps_api_not_configured',
                'fields' => $this->normalizeAddressHierarchyFields($fields, $organization),
            ];
        }

        $normalizedFields = $this->normalizeAddressHierarchyFields($fields, $organization);
        $parts = array_filter([
            $normalizedFields['service_address'],
            $normalizedFields['neighborhood'],
            $normalizedFields['service_sector'],
            $normalizedFields['service_locality'],
            $normalizedFields['locality'],
            $normalizedFields['service_department'],
            $normalizedFields['service_country'] ?: 'Colombia',
        ]);

        if (empty($parts)) {
            return [
                'success' => false,
                'reason' => 'missing_address_input',
                'fields' => $normalizedFields,
            ];
        }

        $addressQuery = implode(', ', $parts);
        $country = strtolower((string) ($organization?->google_maps_country ?: 'co'));

        $response = null;

        try {
            if ($placeId !== '') {
                $response = Http::timeout(20)->acceptJson()->get('https://maps.googleapis.com/maps/api/geocode/json', [
                    'place_id' => $placeId,
                    'language' => 'es',
                    'key' => $googleMapsApiKey,
                ]);
            }

            if (!$response || !$response->successful() || (string) data_get($response->json(), 'status', '') !== 'OK') {
                $response = Http::timeout(20)->acceptJson()->get('https://maps.googleapis.com/maps/api/geocode/json', [
                    'address' => $addressQuery,
                    'components' => "country:{$country}",
                    'language' => 'es',
                    'key' => $googleMapsApiKey,
                ]);
            }
        } catch (\Throwable $e) {
            return [
                'success' => false,
                'reason' => 'google_maps_request_error',
                'fields' => $normalizedFields,
            ];
        }

        if (!$response->successful()) {
            return [
                'success' => false,
                'reason' => 'google_maps_http_error',
                'fields' => $normalizedFields,
            ];
        }

        $status = (string) data_get($response->json(), 'status', '');
        if ($status !== 'OK') {
            return [
                'success' => false,
                'reason' => "google_maps_status_{$status}",
                'fields' => $normalizedFields,
            ];
        }

        $firstResult = data_get($response->json(), 'results.0');
        if (!is_array($firstResult)) {
            return [
                'success' => false,
                'reason' => 'google_maps_no_results',
                'fields' => $normalizedFields,
            ];
        }

        $components = collect(data_get($firstResult, 'address_components', []));
        $componentByType = static function ($type) use ($components): string {
            $component = $components->first(function ($item) use ($type) {
                return in_array($type, data_get($item, 'types', []), true);
            });

            return trim((string) data_get($component, 'long_name', ''));
        };

        $route = $componentByType('route');
        $streetNumber = $componentByType('street_number');
        $premise = $componentByType('premise');
        $subpremise = $componentByType('subpremise');

        $resolvedFields = $normalizedFields;
        $formattedAddress = trim((string) data_get($firstResult, 'formatted_address', ''));
        $shortAddressParts = array_values(array_filter([
            trim($route),
            trim($streetNumber),
        ]));

        if (count($shortAddressParts) > 0) {
            $resolvedFields['service_address'] = implode(' ', $shortAddressParts);
        } elseif ($formattedAddress !== '') {
            $addressOnly = $formattedAddress;
            foreach (array_filter([
                $componentByType('neighborhood'),
                $componentByType('sublocality_level_2'),
                $componentByType('sublocality_level_1') ?: $componentByType('sublocality'),
                $componentByType('locality'),
                $componentByType('administrative_area_level_2'),
                $componentByType('administrative_area_level_1'),
                $componentByType('country'),
                'Colombia',
            ]) as $segment) {
                $addressOnly = preg_replace('/,\s*' . preg_quote((string) $segment, '/') . '\b/i', '', (string) $addressOnly) ?? (string) $addressOnly;
            }
            $resolvedFields['service_address'] = trim((string) $addressOnly, ", \t\n\r\0\x0B");
        } elseif ($premise !== '' || $subpremise !== '') {
            $resolvedFields['service_address'] = trim(implode(' ', array_filter([$premise, $subpremise])));
        }

        $countryName = $this->normalizeLocationLabel($componentByType('country'));
        $department = $this->normalizeLocationLabel($componentByType('administrative_area_level_1'));
        $adminLevel2 = $this->normalizeLocationLabel($componentByType('administrative_area_level_2'));
        $cityCandidate = $this->normalizeLocationLabel($componentByType('locality'));
        $urbanLocality = $this->normalizeLocationLabel($componentByType('sublocality_level_1') ?: $componentByType('sublocality'));
        $sector = $this->pickLocationCandidate([
            $componentByType('sublocality_level_2'),
            $componentByType('sublocality_level_3'),
            $componentByType('sublocality_level_4'),
            $componentByType('political'),
        ], [$cityCandidate, $urbanLocality, $componentByType('neighborhood')]);
        $neighborhood = $this->normalizeLocationLabel($componentByType('neighborhood'));

        $isBogota = collect([$cityCandidate, $adminLevel2, $department, $formattedAddress])
            ->contains(fn ($value) => $this->isBogotaLabel((string) $value) || str_contains($this->normalizeLookupString((string) $value), 'bogota'));

        if ($isBogota) {
            $resolvedFields['locality'] = 'Bogotá';
            $resolvedFields['service_department'] = 'Bogotá D.C.';
            $resolvedFields['service_locality'] = $this->pickLocationCandidate([
                $urbanLocality,
                !$this->isBogotaLabel($cityCandidate) ? $cityCandidate : '',
                !$this->isBogotaLabel($adminLevel2) ? $adminLevel2 : '',
                $resolvedFields['service_locality'],
            ], [$neighborhood, 'Bogotá']);
            $resolvedFields['service_sector'] = $this->pickLocationCandidate([
                $sector,
                $componentByType('sublocality_level_2'),
                $componentByType('sublocality_level_3'),
                $resolvedFields['service_sector'],
            ], [$resolvedFields['service_locality'], $neighborhood, 'Bogotá']);
        } else {
            $resolvedFields['locality'] = $this->pickLocationCandidate([$cityCandidate, $adminLevel2, $resolvedFields['locality']]);
            $resolvedFields['service_department'] = $this->pickLocationCandidate([$department, $resolvedFields['service_department']]);
            $resolvedFields['service_locality'] = $this->pickLocationCandidate([$urbanLocality, $resolvedFields['service_locality']], [$resolvedFields['locality'], $neighborhood]);
            $resolvedFields['service_sector'] = $this->pickLocationCandidate([$sector, $resolvedFields['service_sector']], [$resolvedFields['locality'], $resolvedFields['service_locality'], $neighborhood]);
        }

        if ($neighborhood !== '') {
            $resolvedFields['neighborhood'] = $neighborhood;
        }
        if ($countryName !== '') {
            $resolvedFields['service_country'] = $countryName;
        }
        if ($resolvedFields['service_sector'] === '') {
            $resolvedFields['service_sector'] = $this->inferSectorFromAddressText($fields, $resolvedFields);
        }
        if (($resolvedFields['neighborhood'] ?? '') === '') {
            $resolvedFields['neighborhood'] = $this->inferNeighborhoodFallback($resolvedFields, $fields);
        }

        $resolvedFields = $this->normalizeAddressHierarchyFields($resolvedFields, $organization);

        $placeId = trim((string) data_get($firstResult, 'place_id', ''));
        $latitude = data_get($firstResult, 'geometry.location.lat');
        $longitude = data_get($firstResult, 'geometry.location.lng');
        $mapsQuery = trim(implode(', ', array_filter([
            $resolvedFields['service_address'] ?? '',
            $resolvedFields['neighborhood'] ?? '',
            $resolvedFields['service_sector'] ?? '',
            $resolvedFields['service_locality'] ?? '',
            $resolvedFields['locality'] ?? '',
            $resolvedFields['service_department'] ?? '',
            $resolvedFields['service_country'] ?? '',
        ])));

        if (is_numeric($latitude) && is_numeric($longitude)) {
            $googleMapsUrl = 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($latitude . ',' . $longitude);
        } elseif ($placeId !== '' && $mapsQuery !== '') {
            $googleMapsUrl = 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($mapsQuery) . '&query_place_id=' . rawurlencode($placeId);
        } elseif ($mapsQuery !== '') {
            $googleMapsUrl = 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode($mapsQuery);
        } else {
            $googleMapsUrl = '';
        }

        return [
            'success' => true,
            'reason' => 'resolved',
            'fields' => $resolvedFields,
            'google_maps_url' => $googleMapsUrl,
        ];
    }

    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;
    }

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

        $query = 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');

        return $query
            ->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,
                'sort_order' => (int) $item->sort_order,
                ];
            })->all();
    }

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

        $query = 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');

        return $query
            ->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,
                'sort_order' => (int) $item->sort_order,
                ];
            })->all();
    }

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

        $query = 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');

        return $query
            ->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'],
                'sort_order' => (int) $item->sort_order,
                ];
            })->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' => $item->id,
                'name' => $item->name,
                'account_holder' => $item->account_holder,
                'account_reference' => $item->account_reference,
                'payment_details' => $item->payment_details,
                'is_default' => (bool) $item->is_default,
            ])->all();
    }

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

        return ExternalPartner::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->map(fn (ExternalPartner $item) => [
                'id' => (int) $item->id,
                'name' => (string) $item->name,
                'contact_name' => (string) ($item->contact_name ?? ''),
                'phone' => (string) ($item->phone ?? ''),
                'email' => (string) ($item->email ?? ''),
                'city' => (string) ($item->city ?? ''),
            ])->all();
    }

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

        MariachiGroup::ensureDefaultForOrganization($organizationId);

        $groups = $user ? OwnerWorkspace::groupsForUser($user) : MariachiGroup::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->orderByDesc('is_default')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get();

        return $groups
            ->map(fn (MariachiGroup $group) => [
                'id' => (int) $group->id,
                'name' => (string) $group->name,
                'is_default' => (bool) $group->is_default,
            ])->all();
    }

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

        $query = 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');

        return $query
            ->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,
                ];
            })->all();
    }

    private function resolveCededOverview(float $totalCharged, float $cededTotalCost): array
    {
        $diff = round($totalCharged - $cededTotalCost, 2);

        if ($diff > 0) {
            $status = 'por_cobrar';
            $label = 'Esperando excedente';
        } elseif ($diff < 0) {
            $status = 'por_pagar';
            $label = 'Debes excedente';
        } else {
            $status = 'equilibrado';
            $label = 'Sin excedente';
        }

        return [
            'status' => $status,
            'label' => $label,
            'amount' => abs($diff),
            'signed_amount' => $diff,
        ];
    }

    private function normalizeCededCostBreakdown(mixed $rawValue): array
    {
        $decoded = is_string($rawValue)
            ? json_decode($rawValue, true)
            : $rawValue;

        if (!is_array($decoded)) {
            return [];
        }

        return collect($decoded)
            ->filter(fn ($item) => is_array($item))
            ->map(function (array $item) {
                $source = trim((string) ($item['source'] ?? ''));
                $name = trim((string) ($item['name'] ?? ''));
                $quantity = max(1, (int) ($item['quantity'] ?? 1));
                $unitPrice = round(max(0, (float) ($item['unit_price'] ?? 0)), 2);
                $included = (bool) ($item['included'] ?? true);

                return [
                    'source' => $source,
                    'index' => isset($item['index']) ? (int) $item['index'] : null,
                    'name' => $name,
                    'quantity' => $quantity,
                    'unit_price' => $unitPrice,
                    'included' => $included,
                    'total' => $included ? round($quantity * $unitPrice, 2) : 0,
                ];
            })
            ->filter(fn ($item) => $item['source'] !== '' && $item['name'] !== '')
            ->values()
            ->all();
    }

    private function summarizeExternalPartnerBalances(int $organizationId, array $partnerIds): array
    {
        $partnerIds = collect($partnerIds)
            ->map(fn ($id) => (int) $id)
            ->filter(fn ($id) => $id > 0)
            ->unique()
            ->values();

        if ($partnerIds->isEmpty()) {
            return [];
        }

        $summaries = $partnerIds->mapWithKeys(fn ($partnerId) => [
            $partnerId => [
                'status' => 'equilibrado',
                'label' => 'Saldo en cero',
                'amount' => 0.0,
            ],
        ])->all();

        $computedSummaries = ExternalPartnerMovement::query()
            ->where('organization_id', $organizationId)
            ->where('status', 'activo')
            ->whereIn('external_partner_id', $partnerIds->all())
            ->get()
            ->groupBy('external_partner_id')
            ->map(fn ($movements) => ExternalPartnerMovement::summarizeBalance($movements))
            ->all();

        foreach ($computedSummaries as $partnerId => $summary) {
            $summaries[(int) $partnerId] = $summary;
        }

        return $summaries;
    }

    public function index(Request $request)
    {
        $owner = $request->user();
        $organization = null;
        $organizationId = null;
        if (!empty($owner->organization_id)) {
            $organizationId = (int) $owner->organization_id;
            $organization = Organization::query()->find($owner->organization_id);
        }
        $activeGroupId = !empty($organizationId)
            ? OwnerWorkspace::activeGroupId($request, (int) $organizationId)
            : null;

        $orders = collect();
        if (!empty($organizationId)) {
            MariachiGroup::ensureDefaultForOrganization($organizationId);
            $orders = OwnerWorkspace::applyScope(
                Serenata::query()
                    ->where('organization_id', $organizationId)
                    ->with('mariachiGroup:id,name'),
                $request
            )
                ->latest('event_date')
                ->latest('event_time')
                ->latest('id')
                ->get();
        }

        $stats = [
            'pending' => $orders->filter(fn (Serenata $order) => in_array((string) $order->payment_status, [self::PAYMENT_STATUS_PENDING, self::PAYMENT_STATUS_PARTIAL], true))->count(),
            'completed' => $orders->filter(fn (Serenata $order) => $this->normalizeServiceStatus((string) $order->service_status) === self::SERVICE_STATUS_COMPLETED)->count(),
            'ceded' => $orders->where('is_ceded', true)->count(),
            'cancelled' => $orders->filter(fn (Serenata $order) => $this->normalizeServiceStatus((string) $order->service_status) === self::SERVICE_STATUS_CANCELLED)->count(),
        ];

        return view('content.owner.serenatas.index', [
            'orders' => $orders,
            'stats' => $stats,
            'googleMapsApiKey' => $organization?->google_maps_api_key,
            'googleMapsCountry' => $organization?->google_maps_country ?: 'co',
            'giftOptions' => $this->giftOptions($organizationId),
            'artistOptions' => $this->artistOptions($organizationId),
            'otherCostOptions' => $this->otherCostOptions($organizationId),
            'paymentMethodOptions' => $this->paymentMethodOptions($organizationId),
            'mariachiGroupOptions' => $this->mariachiGroupOptions($organizationId, $owner),
            'packageOptions' => $this->serenataPackageOptions($organizationId),
            'locationOptionsSeed' => $this->locationOptionsData($organizationId, $request),
            'selectedWorkspaceGroupId' => $activeGroupId,
        ]);
    }

    public function packageTemplate(Request $request)
    {
        $organizationId = (int) ($request->user()->organization_id ?? 0);
        if ($organizationId <= 0) {
            return response()->json(['message' => 'Organización no encontrada.'], 404);
        }

        $data = $request->validate([
            'package_id' => ['required', 'integer', 'min:1'],
            'event_date' => ['nullable', 'date_format:Y-m-d'],
        ]);

        $package = SerenataPackage::query()
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->whereKey((int) $data['package_id'])
            ->first();

        if (!$package) {
            return response()->json(['message' => 'Paquete no encontrado.'], 404);
        }

        $forDate = !empty($data['event_date']) ? (string) $data['event_date'] : null;
        $template = collect($this->serenataPackageOptions($organizationId, $forDate))
            ->firstWhere('id', (int) $package->id);

        return response()->json([
            'template' => $template,
            'gift_options' => $this->giftOptions($organizationId, $forDate, (int) $package->mariachi_group_id),
            'artist_options' => $this->artistOptions($organizationId, $forDate, (int) $package->mariachi_group_id),
            'other_cost_options' => $this->otherCostOptions($organizationId, $forDate, (int) $package->mariachi_group_id),
        ]);
    }

    public function locationOptions(Request $request)
    {
        $organizationId = (int) ($request->user()->organization_id ?? 0);
        return response()->json($this->locationOptionsData($organizationId, $request));
    }

    private function locationOptionsData(int $organizationId, ?Request $request = null): array
    {
        if ($organizationId <= 0) {
            return [
                'countries' => [],
                'departments' => [],
                'neighborhoods' => [],
                'localities' => [],
                'service_localities' => [],
                'service_sectors' => [],
            ];
        }

        $buildOptions = function (string $column) use ($organizationId, $request) {
            $query = Serenata::query()
                ->where('organization_id', $organizationId)
                ->whereNotNull($column);
            if ($request) {
                OwnerWorkspace::applyScope($query, $request);
            }

            return $query
                ->pluck($column)
                ->map(fn ($value) => $this->normalizeLocationLabel((string) $value))
                ->filter()
                ->unique(fn ($value) => mb_strtolower($value, 'UTF-8'))
                ->sort()
                ->values()
                ->all();
        };

        return [
            'countries' => $buildOptions('service_country'),
            'departments' => $buildOptions('service_department'),
            'neighborhoods' => $buildOptions('neighborhood'),
            'localities' => $buildOptions('locality'),
            'service_localities' => $buildOptions('service_locality'),
            'service_sectors' => $buildOptions('service_sector'),
        ];
    }

    public function show(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);
        $this->ensureInitialPaymentRecord($serenata);
        $serenata->loadMissing(['externalPartner', 'mariachiGroup', 'serenataPackage']);
        $organization = Organization::query()->find($organizationId);
        $externalPartnerOptions = $this->externalPartnerOptions($organizationId);

        $giftItems = collect($serenata->gift_items ?? [])->values()->map(function ($item, $index) {
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = (float) data_get($item, 'unit_price', 0);
            return [
                'category' => 'Obsequio',
                'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Obsequio')),
                'unit_price' => $unit,
                'quantity' => $qty,
                'total' => (float) data_get($item, 'total', $unit * $qty),
                'charge_amount' => max(0, (float) data_get($item, 'charge_amount', data_get($item, 'total', $unit * $qty))),
                'source' => 'gift',
                'index' => (int) $index,
                'is_deleted' => (bool) data_get($item, 'is_deleted', false),
                'delete_reason' => (string) data_get($item, 'delete_reason', ''),
            ];
        });

        $artistItems = collect($serenata->artist_items ?? [])->values()->map(function ($item, $index) {
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = (float) data_get($item, 'unit_price', 0);
            return [
                'category' => 'Artista',
                'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Artista')),
                'unit_price' => $unit,
                'quantity' => $qty,
                'total' => $unit * $qty,
                'charge_amount' => max(0, (float) data_get($item, 'charge_amount', $unit * $qty)),
                'source' => 'artist',
                'index' => (int) $index,
                'is_deleted' => (bool) data_get($item, 'is_deleted', false),
                'delete_reason' => (string) data_get($item, 'delete_reason', ''),
            ];
        });

        $otherCostItems = collect($serenata->other_cost_items ?? [])->values()->map(function ($item, $index) {
            $qty = max(1, (int) data_get($item, 'quantity', 1));
            $unit = (float) data_get($item, 'unit_price', 0);
            return [
                'category' => 'Otro costo',
                'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Costo')),
                'unit_price' => $unit,
                'quantity' => $qty,
                'total' => $unit * $qty,
                'charge_amount' => max(0, (float) data_get($item, 'charge_amount', $unit * $qty)),
                'source' => 'other_cost',
                'index' => (int) $index,
                'is_deleted' => (bool) data_get($item, 'is_deleted', false),
                'delete_reason' => (string) data_get($item, 'delete_reason', ''),
            ];
        });

        $lineItems = $artistItems->concat($giftItems)->concat($otherCostItems)->values();
        $statusHistories = SerenataStatusHistory::query()
            ->with('changedByUser:id,name,email')
            ->where('serenata_id', $serenata->id)
            ->orderByDesc('id')
            ->get();
        $serviceStatusHistories = $statusHistories
            ->filter(fn (SerenataStatusHistory $history) => (string) $history->from_service_status !== (string) $history->to_service_status)
            ->values();
        $payments = SerenataPayment::query()
            ->with('registeredBy:id,name,email')
            ->where('serenata_id', $serenata->id)
            ->orderByDesc('paid_at')
            ->orderByDesc('id')
            ->get();
        $externalPartnerMovements = ExternalPartnerMovement::query()
            ->with([
                'externalPartner:id,name,city',
                'createdByUser:id,name,email',
            ])
            ->where('organization_id', $organizationId)
            ->where('serenata_id', $serenata->id)
            ->orderByDesc('id')
            ->get();

        $addressCandidatesQuery = Serenata::query()
            ->where('organization_id', $organizationId)
            ->whereNotNull('service_address');
        OwnerWorkspace::applyScope($addressCandidatesQuery, $request);

        if (!empty($serenata->customer_id)) {
            $addressCandidatesQuery->where('customer_id', $serenata->customer_id);
        } else {
            $addressCandidatesQuery->where('payer_name', $serenata->payer_name);
        }

        $addressOptions = $addressCandidatesQuery
            ->select([
                'service_address',
                'address_additional',
                'service_country',
                'service_department',
                'locality',
                'service_locality',
                'service_sector',
                'neighborhood',
            ])
            ->orderByDesc('id')
            ->get()
            ->map(function (Serenata $item) {
                $serviceAddress = trim((string) ($item->service_address ?? ''));
                $addressAdditional = trim((string) ($item->address_additional ?? ''));
                $serviceCountry = trim((string) ($item->service_country ?? ''));
                $serviceDepartment = trim((string) ($item->service_department ?? ''));
                $city = trim((string) ($item->locality ?? ''));
                $serviceLocality = trim((string) ($item->service_locality ?? ''));
                $serviceSector = trim((string) ($item->service_sector ?? ''));
                $neighborhood = trim((string) ($item->neighborhood ?? ''));

                return [
                    'service_address' => $serviceAddress,
                    'address_additional' => $addressAdditional,
                    'service_country' => $serviceCountry,
                    'service_department' => $serviceDepartment,
                    'city' => $city,
                    'service_locality' => $serviceLocality,
                    'service_sector' => $serviceSector,
                    'neighborhood' => $neighborhood,
                    'label' => trim(implode(' | ', array_filter([
                        $serviceAddress,
                        $addressAdditional,
                        $serviceSector ? "Sector: {$serviceSector}" : '',
                        $neighborhood ? "Barrio: {$neighborhood}" : '',
                        $serviceLocality ? "Localidad: {$serviceLocality}" : '',
                        $city ? "Ciudad: {$city}" : '',
                        $serviceDepartment ? "Depto: {$serviceDepartment}" : '',
                    ]))),
                ];
            })
            ->filter(fn ($item) => !empty($item['service_address']))
            ->unique(fn ($item) => mb_strtolower(implode('|', [
                $item['service_address'],
                $item['address_additional'],
                $item['service_country'],
                $item['service_department'],
                $item['city'],
                $item['service_locality'],
                $item['service_sector'],
                $item['neighborhood'],
            ]), 'UTF-8'))
            ->values();

        $viewName = $request->boolean('embed')
            ? 'content.owner.serenatas.show-embed'
            : 'content.owner.serenatas.show';

        $cededOverview = $this->resolveCededOverview(
            $this->actualCustomerChargeAmount($serenata),
            (float) $serenata->ceded_total_cost
        );
        $resolvedAddress = $this->resolveAddressWithGoogle($organization, [
            'service_address' => trim((string) ($serenata->service_address ?? '')),
            'address_additional' => trim((string) ($serenata->address_additional ?? '')),
            'service_country' => trim((string) ($serenata->service_country ?? '')),
            'service_department' => trim((string) ($serenata->service_department ?? '')),
            'neighborhood' => trim((string) ($serenata->neighborhood ?? '')),
            'locality' => trim((string) ($serenata->locality ?? '')),
            'service_locality' => trim((string) ($serenata->service_locality ?? '')),
            'service_sector' => trim((string) ($serenata->service_sector ?? '')),
        ]);
        $balancePartnerIds = $externalPartnerMovements
            ->pluck('external_partner_id')
            ->push((int) $serenata->external_partner_id)
            ->filter()
            ->unique()
            ->values()
            ->all();
        $externalPartnerBalanceSummary = $this->summarizeExternalPartnerBalances($organizationId, $balancePartnerIds);

        return view($viewName, [
            'serenata' => $serenata,
            'lineItems' => $lineItems,
            'statusHistories' => $statusHistories,
            'serviceStatusHistories' => $serviceStatusHistories,
            'payments' => $payments,
            'paymentMethodOptions' => $this->paymentMethodOptions($organizationId),
            'externalPartnerOptions' => $externalPartnerOptions,
            'giftOptions' => $this->giftOptions($organizationId),
            'artistOptions' => $this->artistOptions($organizationId),
            'otherCostOptions' => $this->otherCostOptions($organizationId),
            'addressOptions' => $addressOptions,
            'cededOverview' => $cededOverview,
            'externalPartnerMovements' => $externalPartnerMovements,
            'externalPartnerBalanceSummary' => $externalPartnerBalanceSummary,
            'externalPartnerMovementTypeOptions' => ExternalPartnerMovement::typeOptions(),
            'externalPartnerMovementDirectionOptions' => ExternalPartnerMovement::directionOptions(),
            'externalPartnerMovementSettlementOptions' => ExternalPartnerMovement::settlementOptions(),
            'googleMapsAddressUrl' => (string) ($resolvedAddress['google_maps_url'] ?? ''),
            'googleMapsApiKey' => $organization?->google_maps_api_key,
            'googleMapsCountry' => $organization?->google_maps_country ?: 'co',
        ]);
    }

    public function addServiceAdjustment(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);
        $previousActiveItemsBySource = $this->activeServiceItemsBySource($serenata);

        $validated = $request->validate([
            'item_key' => ['required', 'string', 'max:120'],
            'quantity' => ['nullable', 'integer', 'min:1', 'max:100'],
            'unit_price' => ['nullable', 'numeric', 'min:0'],
        ]);

        $itemKey = trim((string) $validated['item_key']);
        [$category, $code] = array_pad(explode(':', $itemKey, 2), 2, null);
        $category = trim((string) $category);
        $code = trim((string) $code);
        if ($category === '' || $code === '') {
            return $this->mutationErrorResponse($request, 'Selecciona un ítem válido.');
        }

        $quantity = max(1, (int) ($validated['quantity'] ?? 1));
        $unitPriceOverride = isset($validated['unit_price']) ? (float) $validated['unit_price'] : null;

        $itemName = '';
        $defaultUnitPrice = 0.0;
        $chargeAmount = 0.0;

        if ($category === 'gift') {
            $catalog = collect($this->giftOptions($organizationId, (string) $serenata->event_date))->keyBy('code');
            if (!$catalog->has($code)) {
                return $this->mutationErrorResponse($request, 'El obsequio no existe o está inactivo.');
            }
            $catalogItem = $catalog->get($code);
            $itemName = (string) data_get($catalogItem, 'name', $code);
            $defaultUnitPrice = (float) data_get($catalogItem, 'price', 0);
            $items = collect($serenata->gift_items ?? [])->values()->all();
            $unitPrice = (float) round(max(0, $unitPriceOverride ?? $defaultUnitPrice));
            $lineTotal = $unitPrice * $quantity;
            $chargeAmount = $lineTotal;
            $items[] = [
                'code' => $code,
                'name' => $itemName,
                'unit_price' => $unitPrice,
                'quantity' => $quantity,
                'total' => $lineTotal,
                'charge_amount' => $chargeAmount,
                'is_deleted' => false,
            ];
            $serenata->gift_items = $items;
        } elseif ($category === 'artist') {
            $catalog = collect($this->artistOptions($organizationId, (string) $serenata->event_date))->keyBy('code');
            if (!$catalog->has($code)) {
                return $this->mutationErrorResponse($request, 'El artista no existe o está inactivo.');
            }
            $catalogItem = $catalog->get($code);
            $itemName = (string) data_get($catalogItem, 'name', $code);
            $defaultUnitPrice = (float) data_get($catalogItem, 'price', 0);
            $items = collect($serenata->artist_items ?? [])->values()->all();
            $unitPrice = (float) round(max(0, $unitPriceOverride ?? $defaultUnitPrice));
            $chargeAmount = $unitPrice * $quantity;
            $items[] = [
                'code' => $code,
                'name' => $itemName,
                'unit_price' => $unitPrice,
                'quantity' => $quantity,
                'type' => (string) data_get($catalogItem, 'type', 'included'),
                'charge_amount' => $chargeAmount,
                'is_deleted' => false,
            ];
            $serenata->artist_items = $items;
        } elseif ($category === 'other_cost') {
            $catalog = collect($this->otherCostOptions($organizationId, (string) $serenata->event_date))->keyBy('code');
            if (!$catalog->has($code)) {
                return $this->mutationErrorResponse($request, 'El costo adicional no existe o está inactivo.');
            }
            $catalogItem = $catalog->get($code);
            $itemName = (string) data_get($catalogItem, 'name', $code);
            $defaultUnitPrice = (float) data_get($catalogItem, 'price', 0);
            $items = collect($serenata->other_cost_items ?? [])->values()->all();
            $unitPrice = (float) round(max(0, $unitPriceOverride ?? $defaultUnitPrice));
            $chargeAmount = $unitPrice * $quantity;
            $items[] = [
                'code' => $code,
                'name' => $itemName,
                'unit_price' => $unitPrice,
                'quantity' => $quantity,
                'charge_amount' => $chargeAmount,
                'is_deleted' => false,
            ];
            $serenata->other_cost_items = $items;
        } else {
            return $this->mutationErrorResponse($request, 'Categoría de ajuste inválida.');
        }

        $serenata->total_charged = max(0, (float) $serenata->total_charged + $chargeAmount);
        $serenata->base_price = max(0, (float) $serenata->base_price + $chargeAmount);

        $financials = $this->recalculateSerenataFinancials($serenata);
        $serenata->fill($financials);
        $this->syncCededBreakdownToCurrentCosts($serenata, $previousActiveItemsBySource);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => "Ajuste de servicio: {$itemName}",
        ]);

        $source = match ($category) {
            'gift' => 'gift',
            'artist' => 'artist',
            'other_cost' => 'other_cost',
            default => 'gift',
        };
        $items = $this->getItemsBySource($serenata, $source);
        $newIndex = max(0, count($items) - 1);
        $newItem = $items[$newIndex] ?? null;

        return $this->mutationResponse($request, 'Producto agregado correctamente.', [
            'financials' => [
                'base_price' => (float) $serenata->base_price,
                'base_cost' => (float) $serenata->base_cost,
                'discount_amount' => (float) $serenata->discount_amount,
                'commission_amount' => (float) $serenata->commission_amount,
                'profit_amount' => (float) $serenata->profit_amount,
                'total_charged' => (float) $serenata->total_charged,
            ],
            'refresh_required' => (bool) $serenata->is_ceded,
            'added_item' => [
                'source' => $source,
                'index' => $newIndex,
                'category' => $source === 'gift' ? 'Obsequio' : ($source === 'artist' ? 'Artista' : 'Otro costo'),
                'name' => (string) data_get($newItem, 'name', $itemName),
                'unit_price' => (float) data_get($newItem, 'unit_price', $defaultUnitPrice),
                'quantity' => (int) data_get($newItem, 'quantity', $quantity),
                'total' => (float) data_get($newItem, 'total', ((float) data_get($newItem, 'unit_price', 0) * (int) data_get($newItem, 'quantity', 1))),
                'charge_amount' => (float) data_get($newItem, 'charge_amount', $chargeAmount),
            ],
        ]);
    }

    public function updateCostItem(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);
        $previousActiveItemsBySource = $this->activeServiceItemsBySource($serenata);

        $validated = $request->validate([
            'source' => ['required', 'in:gift,artist,other_cost'],
            'index' => ['required', 'integer', 'min:0'],
            'quantity' => ['nullable', 'integer', 'min:1', 'max:100'],
            'unit_price' => ['required', 'numeric', 'min:0'],
        ]);

        $source = (string) $validated['source'];
        $index = (int) $validated['index'];
        $items = $this->getItemsBySource($serenata, $source);

        if (!array_key_exists($index, $items)) {
            return $this->mutationErrorResponse($request, 'Ítem no encontrado.');
        }

        $item = $items[$index];
        if ((bool) data_get($item, 'is_deleted', false)) {
            return $this->mutationErrorResponse($request, 'No puedes editar un ítem eliminado.');
        }

        $quantity = max(1, (int) ($validated['quantity'] ?? data_get($item, 'quantity', 1)));
        $unitPrice = (float) round(max(0, (float) $validated['unit_price']));
        $chargeAmount = $quantity * $unitPrice;

        $item['quantity'] = $quantity;
        $item['unit_price'] = $unitPrice;
        $item['charge_amount'] = $chargeAmount;
        $item['total'] = $quantity * $unitPrice;
        $items[$index] = $item;

        $this->setItemsBySource($serenata, $source, $items);
        $serenata->fill($this->recalculateSerenataFinancials($serenata));
        $this->syncCededBreakdownToCurrentCosts($serenata, $previousActiveItemsBySource);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Ítem editado: ' . ((string) data_get($item, 'name', data_get($item, 'code', 'Ítem'))),
        ]);

        return $this->mutationResponse($request, 'Ítem actualizado correctamente.', [
            'updated_item' => [
                'source' => $source,
                'index' => $index,
                'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Ítem')),
                'unit_price' => (float) data_get($item, 'unit_price', $unitPrice),
                'quantity' => (int) data_get($item, 'quantity', $quantity),
                'total' => (float) data_get($item, 'total', ($quantity * $unitPrice)),
            ],
            'financials' => [
                'base_price' => (float) $serenata->base_price,
                'base_cost' => (float) $serenata->base_cost,
                'discount_amount' => (float) $serenata->discount_amount,
                'commission_amount' => (float) $serenata->commission_amount,
                'profit_amount' => (float) $serenata->profit_amount,
                'total_charged' => (float) $serenata->total_charged,
            ],
            'refresh_required' => (bool) $serenata->is_ceded,
        ]);
    }

    public function removeCostItem(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);
        $previousActiveItemsBySource = $this->activeServiceItemsBySource($serenata);

        $validated = $request->validate([
            'source' => ['required', 'in:gift,artist,other_cost'],
            'index' => ['required', 'integer', 'min:0'],
            'reason' => ['nullable', 'string', 'max:600'],
        ]);

        $source = (string) $validated['source'];
        $index = (int) $validated['index'];
        $items = $this->getItemsBySource($serenata, $source);

        if (!array_key_exists($index, $items)) {
            return $this->mutationErrorResponse($request, 'Ítem no encontrado.');
        }

        $item = $items[$index];
        if ((bool) data_get($item, 'is_deleted', false)) {
            return $this->mutationResponse($request, 'El ítem ya estaba eliminado.');
        }

        $item['is_deleted'] = true;
        $item['delete_reason'] = trim((string) ($validated['reason'] ?? '')) ?: null;
        $item['deleted_at'] = now()->toDateTimeString();
        $item['deleted_by_user_id'] = (int) $request->user()->id;
        $items[$index] = $item;

        $this->setItemsBySource($serenata, $source, $items);
        $serenata->fill($this->recalculateSerenataFinancials($serenata));
        $this->syncCededBreakdownToCurrentCosts($serenata, $previousActiveItemsBySource);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Ítem eliminado: ' . ((string) data_get($item, 'name', data_get($item, 'code', 'Ítem'))),
        ]);

        return $this->mutationResponse($request, 'Producto eliminado.', [
            'removed_item' => [
                'source' => $source,
                'index' => $index,
                'delete_reason' => $item['delete_reason'],
            ],
            'financials' => [
                'base_price' => (float) $serenata->base_price,
                'base_cost' => (float) $serenata->base_cost,
                'discount_amount' => (float) $serenata->discount_amount,
                'commission_amount' => (float) $serenata->commission_amount,
                'profit_amount' => (float) $serenata->profit_amount,
                'total_charged' => (float) $serenata->total_charged,
            ],
            'refresh_required' => (bool) $serenata->is_ceded,
        ]);
    }

    public function restoreCostItem(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);
        $previousActiveItemsBySource = $this->activeServiceItemsBySource($serenata);

        $validated = $request->validate([
            'source' => ['required', 'in:gift,artist,other_cost'],
            'index' => ['required', 'integer', 'min:0'],
        ]);

        $source = (string) $validated['source'];
        $index = (int) $validated['index'];
        $items = $this->getItemsBySource($serenata, $source);

        if (!array_key_exists($index, $items)) {
            return back()->withErrors(['index' => 'Ítem no encontrado.']);
        }

        $item = $items[$index];
        if (!(bool) data_get($item, 'is_deleted', false)) {
            return $this->mutationResponse($request, 'El ítem ya está activo.');
        }

        $item['is_deleted'] = false;
        $item['delete_reason'] = null;
        $item['deleted_at'] = null;
        $item['deleted_by_user_id'] = null;
        $items[$index] = $item;

        $this->setItemsBySource($serenata, $source, $items);
        $serenata->fill($this->recalculateSerenataFinancials($serenata));
        $this->syncCededBreakdownToCurrentCosts($serenata, $previousActiveItemsBySource);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Ítem restaurado: ' . ((string) data_get($item, 'name', data_get($item, 'code', 'Ítem'))),
        ]);

        return $this->mutationResponse($request, 'Ítem restaurado correctamente.', [
            'restored_item' => [
                'source' => $source,
                'index' => $index,
                'category' => $source === 'gift' ? 'Obsequio' : ($source === 'artist' ? 'Artista' : 'Otro costo'),
                'name' => (string) data_get($item, 'name', data_get($item, 'code', 'Ítem')),
                'unit_price' => (float) data_get($item, 'unit_price', 0),
                'quantity' => (int) data_get($item, 'quantity', 1),
                'total' => (float) data_get($item, 'total', ((float) data_get($item, 'unit_price', 0) * (int) data_get($item, 'quantity', 1))),
                'charge_amount' => (float) data_get($item, 'charge_amount', ((float) data_get($item, 'unit_price', 0) * (int) data_get($item, 'quantity', 1))),
            ],
            'financials' => [
                'base_price' => (float) $serenata->base_price,
                'base_cost' => (float) $serenata->base_cost,
                'discount_amount' => (float) $serenata->discount_amount,
                'commission_amount' => (float) $serenata->commission_amount,
                'profit_amount' => (float) $serenata->profit_amount,
                'total_charged' => (float) $serenata->total_charged,
            ],
            'refresh_required' => (bool) $serenata->is_ceded,
        ]);
    }

    public function updateAddress(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'service_address' => ['required', 'string', 'max:255'],
            'address_additional' => ['nullable', 'string', 'max:255'],
            'service_country' => ['required', 'string', 'max:80'],
            'service_department' => ['nullable', 'string', 'max:120'],
            'neighborhood' => ['required', 'string', 'max:120'],
            'locality' => ['required', 'string', 'max:120'],
            'service_locality' => ['nullable', 'string', 'max:120'],
            'service_sector' => ['nullable', 'string', 'max:120'],
        ]);

        $organization = Organization::query()->find($organizationId);
        $validated = $this->normalizeAddressHierarchyFields($validated, $organization);

        $serenata->update([
            'service_address' => trim((string) $validated['service_address']),
            'address_additional' => trim((string) ($validated['address_additional'] ?? '')) ?: null,
            'service_country' => trim((string) ($validated['service_country'] ?? '')) ?: 'Colombia',
            'service_department' => trim((string) ($validated['service_department'] ?? '')) ?: null,
            'neighborhood' => trim((string) $validated['neighborhood']),
            'locality' => trim((string) $validated['locality']),
            'service_locality' => trim((string) ($validated['service_locality'] ?? '')) ?: null,
            'service_sector' => trim((string) ($validated['service_sector'] ?? '')) ?: null,
        ]);

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Dirección actualizada',
        ]);

        return back()->with('status', 'Dirección actualizada correctamente.');
    }

    public function updatePaymentMethod(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'payment_method_id' => ['required', 'integer'],
        ]);

        $method = PaymentMethod::query()
            ->where('organization_id', $organizationId)
            ->where('id', (int) $validated['payment_method_id'])
            ->where('is_active', true)
            ->first();

        if (!$method) {
            return back()->withErrors([
                'payment_method_id' => 'Selecciona un método de pago activo válido.',
            ]);
        }

        $serenata->update([
            'payment_method' => (string) $method->name,
        ]);

        $plannedPayment = SerenataPayment::query()
            ->where('serenata_id', $serenata->id)
            ->where('note', 'like', self::PLANNED_PAYMENT_PREFIX . '%')
            ->latest('id')
            ->first();

        if ($plannedPayment) {
            $plannedPayment->update([
                'payment_method_id' => $method->id,
                'payment_method_name' => (string) $method->name,
            ]);
        }

        return back()->with('status', 'Método de pago actualizado correctamente.');
    }

    public function updateActualCustomerCharge(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'actual_customer_charge' => $this->normalizeNullableCopInput($request->input('actual_customer_charge')),
        ]);

        $validated = $request->validate([
            'actual_customer_charge' => ['nullable', 'numeric', 'min:0'],
            'actual_customer_charge_notes' => ['nullable', 'string', 'max:1200'],
        ]);

        $fromPaymentStatus = (string) $serenata->payment_status;
        $fromActualCharge = $serenata->actual_customer_charge;
        $fromNotes = trim((string) ($serenata->actual_customer_charge_notes ?? ''));
        $actualCharge = array_key_exists('actual_customer_charge', $validated) && $validated['actual_customer_charge'] !== null
            ? max(0, (float) $validated['actual_customer_charge'])
            : null;
        $chargeNotes = trim((string) ($validated['actual_customer_charge_notes'] ?? ''));

        $hasActualChargeChange = (float) ($fromActualCharge ?? -1) !== (float) ($actualCharge ?? -1);
        $hasNotesChange = $fromNotes !== $chargeNotes;

        if (!$hasActualChargeChange && !$hasNotesChange) {
            return back()->with('status', 'No hubo cambios en el cobro real.');
        }

        $serenata->actual_customer_charge = $actualCharge;
        $serenata->actual_customer_charge_notes = $chargeNotes !== '' ? $chargeNotes : null;
        $serenata->actual_customer_charge_recorded_at = $actualCharge !== null || $chargeNotes !== '' ? now() : null;
        $serenata->fill($this->recalculateSerenataFinancials($serenata));
        $this->syncCededBreakdownToCurrentCosts($serenata);
        $totalPaid = $this->paidAmountForSerenata($serenata->id);
        $this->syncPaymentStatus($serenata, $totalPaid);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => $actualCharge !== null
                ? 'Cobro real al cliente actualizado a $' . number_format($actualCharge, 0, ',', '.')
                : 'Cobro real al cliente revertido al valor pactado',
        ]);

        return back()->with('status', 'Cobro real al cliente actualizado correctamente.');
    }

    public function updatePricing(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'base_price' => $this->normalizeCopInput($request->input('base_price')),
            'discount_amount' => $this->normalizeCopInput($request->input('discount_amount')),
        ]);

        $validated = $request->validate([
            'base_price' => ['required', 'numeric', 'min:0'],
            'discount_amount' => ['nullable', 'numeric', 'min:0'],
        ]);

        $basePrice = max(0, (float) ($validated['base_price'] ?? 0));
        $discountAmount = max(0, (float) ($validated['discount_amount'] ?? 0));
        $discountAmount = min($basePrice, $discountAmount);
        $fromPaymentStatus = (string) $serenata->payment_status;

        $serenata->base_price = $basePrice;
        $serenata->discount_amount = $discountAmount;
        $serenata->total_charged = max(0, $basePrice - $discountAmount);
        $serenata->fill($this->recalculateSerenataFinancials($serenata));
        $this->syncCededBreakdownToCurrentCosts($serenata);

        $totalPaid = $this->paidAmountForSerenata($serenata->id);
        $this->syncPaymentStatus($serenata, $totalPaid);
        $serenata->save();

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Resumen económico actualizado: cobro ' . '$' . number_format($basePrice, 0, ',', '.') . ', descuento ' . '$' . number_format($discountAmount, 0, ',', '.'),
        ]);

        return back()->with('status', 'Resumen económico actualizado correctamente.');
    }

    public function updateDiscount(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'discount_name' => ['nullable', 'string', 'max:120'],
            'discount_type' => ['required', 'in:amount,percent'],
            'discount_value' => ['required', 'numeric', 'min:0'],
        ]);

        $discountType = (string) $validated['discount_type'];
        $discountValue = max(0, (float) $validated['discount_value']);
        $basePrice = max(0, (float) $serenata->base_price);

        $discountAmount = $discountType === 'percent'
            ? round($basePrice * (max(0, min(100, $discountValue)) / 100))
            : round($discountValue);
        $discountAmount = max(0, min($basePrice, $discountAmount));

        $fromPaymentStatus = (string) $serenata->payment_status;

        $serenata->discount_amount = $discountAmount;
        $serenata->total_charged = max(0, $basePrice - $discountAmount);
        $serenata->fill($this->recalculateSerenataFinancials($serenata));

        $totalPaid = $this->paidAmountForSerenata($serenata->id);
        $this->syncPaymentStatus($serenata, $totalPaid);
        $serenata->save();

        $discountLabel = trim((string) ($validated['discount_name'] ?? '')) ?: 'Sin nombre';
        $appliedLabel = $discountType === 'percent'
            ? rtrim(rtrim(number_format($discountValue, 2, '.', ''), '0'), '.') . '%'
            : '$' . number_format($discountValue, 0, ',', '.');

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => "Descuento aplicado: {$discountLabel} ({$appliedLabel})",
        ]);

        return back()->with('status', 'Descuento actualizado correctamente.');
    }

    public function addPayment(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'amount' => $this->normalizeCopInput($request->input('amount')),
        ]);

        $validated = $request->validate([
            'paid_at' => ['required', 'date'],
            'payment_method_id' => ['nullable', 'integer'],
            'transaction_reference' => ['nullable', 'string', 'max:190'],
            'amount' => ['required', 'numeric', 'min:0'],
        ]);

        $method = null;
        if (!empty($validated['payment_method_id'])) {
            $method = PaymentMethod::query()
                ->where('organization_id', $organizationId)
                ->where('id', (int) $validated['payment_method_id'])
                ->first();
        }

        $methodName = $method?->name ?: (string) $serenata->payment_method;
        if (trim($methodName) === '') {
            $methodName = 'Método no definido';
        }

        SerenataPayment::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'payment_method_id' => $method?->id,
            'registered_by_user_id' => (int) $request->user()->id,
            'paid_at' => $validated['paid_at'],
            'payment_method_name' => $methodName,
            'transaction_reference' => trim((string) ($validated['transaction_reference'] ?? '')) ?: null,
            'amount' => (float) $validated['amount'],
            'note' => null,
        ]);

        $totalPaid = $this->paidAmountForSerenata($serenata->id);
        $fromPaymentStatus = (string) $serenata->payment_status;
        $newPaymentStatus = $this->resolvePaymentStatus($serenata, $totalPaid);

        $serenata->update([
            'payment_method' => $methodName,
            'payment_status' => $newPaymentStatus,
        ]);

        if ($fromPaymentStatus !== $newPaymentStatus) {
            SerenataStatusHistory::create([
                'organization_id' => $organizationId,
                'serenata_id' => $serenata->id,
                'changed_by_user_id' => (int) $request->user()->id,
                'from_service_status' => (string) $serenata->service_status,
                'to_service_status' => (string) $serenata->service_status,
                'from_payment_status' => $fromPaymentStatus,
                'to_payment_status' => $newPaymentStatus,
                'note' => 'Registro de pago',
            ]);
        }

        return back()->with('status', 'Pago registrado correctamente.');
    }

    public function addRefund(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'amount' => $this->normalizeCopInput($request->input('amount')),
        ]);

        $validated = $request->validate([
            'refunded_at' => ['required', 'date'],
            'payment_method_id' => ['nullable', 'integer'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'reason' => ['nullable', 'string', 'max:600'],
        ]);

        $paidAvailable = max(0, $this->paidAmountForSerenata($serenata->id));
        $refundAmount = (float) $validated['amount'];
        if ($refundAmount > $paidAvailable) {
            return back()->withErrors([
                'amount' => 'El reembolso supera el total pagado disponible.',
            ])->withInput();
        }

        $method = null;
        if (!empty($validated['payment_method_id'])) {
            $method = PaymentMethod::query()
                ->where('organization_id', $organizationId)
                ->where('id', (int) $validated['payment_method_id'])
                ->first();
        }

        $methodName = $method?->name ?: (string) $serenata->payment_method;
        if (trim($methodName) === '') {
            $methodName = 'Método no definido';
        }

        SerenataPayment::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'payment_method_id' => $method?->id,
            'registered_by_user_id' => (int) $request->user()->id,
            'paid_at' => $validated['refunded_at'],
            'payment_method_name' => $methodName,
            'transaction_reference' => null,
            'amount' => -1 * $refundAmount,
            'note' => '[REFUND] ' . (trim((string) ($validated['reason'] ?? '')) ?: 'Reembolso parcial'),
        ]);

        $totalPaid = $this->paidAmountForSerenata($serenata->id);
        $newPaymentStatus = $this->resolvePaymentStatus($serenata, $totalPaid);
        $fromPaymentStatus = (string) $serenata->payment_status;
        $serenata->update([
            'payment_status' => $newPaymentStatus,
        ]);

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => $newPaymentStatus,
            'note' => 'Reembolso registrado',
        ]);

        return back()->with('status', 'Reembolso registrado correctamente.');
    }

    public function updateCeded(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'ceded_artist_amount' => $this->normalizeCopInput($request->input('ceded_artist_amount')),
            'ceded_gift_amount' => $this->normalizeCopInput($request->input('ceded_gift_amount')),
            'ceded_transport_amount' => $this->normalizeCopInput($request->input('ceded_transport_amount')),
            'ceded_other_cost_amount' => $this->normalizeCopInput($request->input('ceded_other_cost_amount')),
            'ceded_total_cost' => $this->normalizeCopInput($request->input('ceded_total_cost')),
        ]);

        $validated = $request->validate([
            'is_ceded' => ['nullable', 'boolean'],
            'external_partner_id' => ['nullable', 'integer'],
            'ceded_artist_amount' => ['nullable', 'numeric', 'min:0'],
            'ceded_gift_amount' => ['nullable', 'numeric', 'min:0'],
            'ceded_transport_amount' => ['nullable', 'numeric', 'min:0'],
            'ceded_other_cost_amount' => ['nullable', 'numeric', 'min:0'],
            'ceded_total_cost' => ['nullable', 'numeric', 'min:0'],
            'ceded_notes' => ['nullable', 'string', 'max:1200'],
            'ceded_cost_breakdown' => ['nullable', 'json'],
            'ceded_mark_settled' => ['nullable', 'boolean'],
        ]);

        $isCeded = (bool) ($validated['is_ceded'] ?? false);
        $partnerId = !empty($validated['external_partner_id']) ? (int) $validated['external_partner_id'] : null;

        if ($isCeded && empty($partnerId)) {
            return back()->withErrors([
                'external_partner_id' => 'Selecciona un externo cuando la serenata esté cedida.',
            ])->withInput();
        }

        $partner = null;
        if (!empty($partnerId)) {
            $partner = ExternalPartner::query()
                ->where('organization_id', $organizationId)
                ->where('id', $partnerId)
                ->first();
            if (!$partner) {
                return back()->withErrors([
                    'external_partner_id' => 'El externo seleccionado no existe.',
                ])->withInput();
            }
        }

        $cededCostBreakdown = $this->normalizeCededCostBreakdown($validated['ceded_cost_breakdown'] ?? null);

        $artistAmount = !empty($cededCostBreakdown)
            ? round((float) collect($cededCostBreakdown)->where('source', 'artist')->sum('total'), 2)
            : max(0, (float) ($validated['ceded_artist_amount'] ?? 0));
        $giftAmount = !empty($cededCostBreakdown)
            ? round((float) collect($cededCostBreakdown)->where('source', 'gift')->sum('total'), 2)
            : max(0, (float) ($validated['ceded_gift_amount'] ?? 0));
        $transportAmount = !empty($cededCostBreakdown)
            ? round((float) collect($cededCostBreakdown)->where('source', 'transport')->sum('total'), 2)
            : max(0, (float) ($validated['ceded_transport_amount'] ?? 0));
        $otherCostAmount = !empty($cededCostBreakdown)
            ? round((float) collect($cededCostBreakdown)->where('source', 'other_cost')->sum('total'), 2)
            : max(0, (float) ($validated['ceded_other_cost_amount'] ?? 0));

        $manualTotal = array_key_exists('ceded_total_cost', $validated)
            ? max(0, (float) $validated['ceded_total_cost'])
            : 0.0;
        $calculatedTotal = round($artistAmount + $giftAmount + $transportAmount + $otherCostAmount, 2);
        $cededTotalCost = $manualTotal > 0 ? $manualTotal : $calculatedTotal;

        $overview = $this->resolveCededOverview($this->actualCustomerChargeAmount($serenata), $cededTotalCost);
        $isSettled = (bool) ($validated['ceded_mark_settled'] ?? false);

        $fromServiceStatus = (string) $serenata->service_status;
        $fromPaymentStatus = (string) $serenata->payment_status;

        $serenata->is_ceded = $isCeded;
        $serenata->external_partner_id = $isCeded ? $partner?->id : null;
        $serenata->ceded_artist_amount = $isCeded ? $artistAmount : 0;
        $serenata->ceded_gift_amount = $isCeded ? $giftAmount : 0;
        $serenata->ceded_transport_amount = $isCeded ? $transportAmount : 0;
        $serenata->ceded_other_cost_amount = $isCeded ? $otherCostAmount : 0;
        $serenata->ceded_total_cost = $isCeded ? $cededTotalCost : 0;
        $serenata->ceded_excess_amount = $isCeded ? (float) $overview['amount'] : 0;
        $serenata->ceded_excess_status = $isCeded ? (string) $overview['status'] : 'equilibrado';
        $serenata->ceded_amount = $isCeded ? (float) $overview['signed_amount'] : 0;
        $serenata->ceded_settled_at = $isCeded && $isSettled ? now() : null;
        $serenata->ceded_notes = $isCeded ? (trim((string) ($validated['ceded_notes'] ?? '')) ?: null) : null;
        $serenata->ceded_cost_breakdown = $isCeded ? $cededCostBreakdown : null;

        $serenata->save();

        $partnerName = $partner?->name ?: 'Sin externo';
        $excessText = $isCeded
            ? (" | {$overview['label']}: $" . number_format((float) $overview['amount'], 0, ',', '.'))
            : '';

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => $fromServiceStatus,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => $isCeded
                ? "Cedida a: {$partnerName}{$excessText}"
                : 'Flujo de cedida desactivado',
        ]);

        return back()->with('status', 'Flujo de cedida actualizado.');
    }

    public function addExternalPartnerMovement(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'amount' => $this->normalizeCopInput($request->input('amount')),
        ]);

        $typeOptions = ExternalPartnerMovement::typeOptions();
        $directionOptions = ExternalPartnerMovement::directionOptions();
        $settlementOptions = ExternalPartnerMovement::settlementOptions();

        $validated = $request->validate([
            'external_partner_id' => ['required', 'integer'],
            'movement_type' => ['required', 'in:' . implode(',', array_keys($typeOptions))],
            'direction' => ['required', 'in:' . implode(',', array_keys($directionOptions))],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'settlement_kind' => ['required', 'in:' . implode(',', array_keys($settlementOptions))],
            'counterpart_reference' => ['nullable', 'string', 'max:120'],
            'due_date' => ['nullable', 'date'],
            'notes' => ['nullable', 'string', 'max:1200'],
        ]);

        $partner = ExternalPartner::query()
            ->where('organization_id', $organizationId)
            ->where('id', (int) $validated['external_partner_id'])
            ->where('is_active', true)
            ->first();

        if (!$partner) {
            return back()->withErrors([
                'external_partner_id' => 'Selecciona un externo activo válido.',
            ])->withInput();
        }

        $amount = round(max(0.01, (float) $validated['amount']), 2);
        $movementType = (string) $validated['movement_type'];
        $direction = (string) $validated['direction'];
        $settlementKind = (string) $validated['settlement_kind'];

        ExternalPartnerMovement::query()->create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'external_partner_id' => (int) $partner->id,
            'created_by_user_id' => (int) $request->user()->id,
            'movement_type' => $movementType,
            'settlement_kind' => $settlementKind,
            'direction' => $direction,
            'amount' => $amount,
            'signed_amount' => ExternalPartnerMovement::signedAmountFromDirection($direction, $amount),
            'counterpart_reference' => trim((string) ($validated['counterpart_reference'] ?? '')) ?: null,
            'due_date' => !empty($validated['due_date']) ? $validated['due_date'] : null,
            'notes' => trim((string) ($validated['notes'] ?? '')) ?: null,
            'status' => 'activo',
        ]);

        if ($movementType === ExternalPartnerMovement::TYPE_CEDIDA && !$serenata->is_ceded) {
            $serenata->is_ceded = true;
            $serenata->external_partner_id = (int) $partner->id;
            $serenata->save();
        }

        $typeLabel = $typeOptions[$movementType] ?? ucfirst(str_replace('_', ' ', $movementType));
        $directionLabel = $directionOptions[$direction] ?? ucfirst(str_replace('_', ' ', $direction));

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => "Movimiento externo registrado: {$typeLabel} · {$partner->name} · {$directionLabel} · $" . number_format($amount, 0, ',', '.'),
        ]);

        return back()->with('status', 'Movimiento externo registrado correctamente.');
    }

    public function updateStatus(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $request->merge([
            'service_status' => $this->normalizeServiceStatus($request->input('service_status')),
        ]);

        $validated = $request->validate([
            'service_status' => ['required', 'in:' . implode(',', $this->serviceStatusOptions())],
            'cancellation_reason' => ['nullable', 'string', 'max:600'],
        ]);

        $requestedCancellationReason = trim((string) ($validated['cancellation_reason'] ?? ''));
        $existingCancellationReason = trim((string) ($serenata->cancellation_reason ?? ''));

        if (($validated['service_status'] ?? '') === self::SERVICE_STATUS_CANCELLED
            && $requestedCancellationReason === ''
            && $existingCancellationReason === '') {
            return back()
                ->withErrors(['cancellation_reason' => 'El motivo de cancelación es obligatorio cuando el estado es Cancelada.'])
                ->withInput();
        }

        $fromServiceStatus = $this->normalizeServiceStatus((string) $serenata->service_status);
        $fromPaymentStatus = (string) $serenata->payment_status;
        $toServiceStatus = (string) $validated['service_status'];
        $fromCancellationReason = trim((string) ($serenata->cancellation_reason ?? ''));
        $toCancellationReason = $requestedCancellationReason !== ''
            ? $requestedCancellationReason
            : ($toServiceStatus === self::SERVICE_STATUS_CANCELLED ? $existingCancellationReason : '');

        $hasStatusChange = $fromServiceStatus !== $toServiceStatus;
        $hasCancellationReasonChange = $toServiceStatus === self::SERVICE_STATUS_CANCELLED
            && $fromCancellationReason !== $toCancellationReason;

        if (!$hasStatusChange && !$hasCancellationReasonChange) {
            return back()->with('status', 'No hubo cambios en el estado.');
        }

        $serenata->service_status = $toServiceStatus;
        $serenata->cancellation_reason = $toServiceStatus === self::SERVICE_STATUS_CANCELLED
            ? ($toCancellationReason ?: null)
            : null;
        $this->syncPaymentStatus($serenata);
        $serenata->save();

        $note = $hasStatusChange
            ? 'Cambio de estado'
            : 'Motivo de cancelación actualizado';
        $toPaymentStatus = (string) $serenata->payment_status;

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => $fromServiceStatus,
            'to_service_status' => $toServiceStatus,
            'from_payment_status' => $fromPaymentStatus,
            'to_payment_status' => $toPaymentStatus,
            'note' => $note,
        ]);

        return back()->with('status', 'Estado actualizado correctamente.');
    }

    public function updateEventInfo(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'reason' => ['required', 'string', 'max:255'],
            'from_name' => ['required', 'string', 'max:255'],
            'to_name' => ['required', 'string', 'max:255'],
            'event_date' => ['required', 'date'],
            'event_time' => ['required', 'date_format:H:i'],
        ]);

        $payload = [
            'reason' => trim((string) $validated['reason']),
            'from_name' => trim((string) $validated['from_name']),
            'to_name' => trim((string) $validated['to_name']),
            'event_date' => $validated['event_date'],
            'event_time' => $validated['event_time'],
        ];

        $currentEventDate = $serenata->event_date?->format('Y-m-d');
        $hasChanges = $payload['reason'] !== (string) $serenata->reason
            || $payload['from_name'] !== (string) $serenata->from_name
            || $payload['to_name'] !== (string) $serenata->to_name
            || $payload['event_time'] !== (string) $serenata->event_time
            || $payload['event_date'] !== $currentEventDate;

        if (!$hasChanges) {
            return back()->with('status', 'No hubo cambios en la información de la serenata.');
        }

        $serenata->update($payload);

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Información de serenata actualizada',
        ]);

        return back()->with('status', 'Información de la serenata actualizada.');
    }

    public function updateNotes(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'internal_notes' => ['nullable', 'string', 'max:1200'],
        ]);

        $fromInternalNotes = trim((string) ($serenata->internal_notes ?? ''));
        $toInternalNotes = trim((string) ($validated['internal_notes'] ?? ''));

        $hasNotesChange = $fromInternalNotes !== $toInternalNotes;
        if (!$hasNotesChange) {
            return back()->with('status', 'No hubo cambios en notas.');
        }

        $serenata->update([
            'internal_notes' => $toInternalNotes !== '' ? $toInternalNotes : null,
        ]);

        SerenataStatusHistory::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->user()->id,
            'from_service_status' => (string) $serenata->service_status,
            'to_service_status' => (string) $serenata->service_status,
            'from_payment_status' => (string) $serenata->payment_status,
            'to_payment_status' => (string) $serenata->payment_status,
            'note' => 'Notas actualizadas',
        ]);

        return back()->with('status', 'Notas guardadas correctamente.');
    }

    public function updateServicePrivateNote(Request $request, Serenata $serenata)
    {
        $organizationId = $this->ensureAccessibleSerenata($request, $serenata);

        $validated = $request->validate([
            'service_private_note' => ['nullable', 'string', 'max:1200'],
        ]);

        $newNote = trim((string) ($validated['service_private_note'] ?? ''));
        $currentNote = trim((string) ($serenata->service_private_note ?? ''));
        if ($newNote === $currentNote) {
            return back()->with('status', 'No hubo cambios en la nota privada.');
        }

        $serenata->update([
            'service_private_note' => $newNote !== '' ? $newNote : null,
        ]);

        return back()->with('status', 'Nota privada del servicio guardada correctamente.');
    }

    public function aiParse(Request $request)
    {
        $validated = $request->validate([
            'raw_text' => ['required', 'string', 'max:12000'],
        ]);

        $owner = $request->user();
        $organization = null;
        if (!empty($owner->organization_id)) {
            $organization = Organization::query()->find($owner->organization_id);
        }

        $geminiApiKey = $organization?->gemini_api_key;
        if (empty($geminiApiKey)) {
            return response()->json([
                'success' => false,
                'message' => 'Gemini API no configurada en Configuración > General.',
            ], 422);
        }

        $today = now()->toDateString();
        $currentYear = now()->year;
        $prompt = <<<PROMPT
Eres un extractor de datos para un formulario de serenatas. Devuelve SOLO JSON válido.

Fecha base de hoy: {$today}
Año de referencia por defecto si no aparece año: {$currentYear}

Extrae estos campos si existen:
- payer_name
- package_name
- service_address
- address_additional
- service_country
- service_department
- locality
- service_locality
- service_sector
- neighborhood
- contact_phone_1
- contact_phone_2
- reason
- from_name
- to_name
- event_date (formato YYYY-MM-DD)
- event_time (formato HH:MM 24h)
- internal_notes
- client_notes
- payment_method (solo uno de: Efectivo al finalizar, Transferencia, Tarjeta, Nequi, Daviplata)

Reglas:
- Si el texto trae "Hola <Nombre>", úsalo como payer_name.
- Si la primera línea parece un nombre de persona y no hay otra etiqueta clara, úsala como payer_name.
- "Homenajeada", "Para quién" -> to_name.
- "Paquete" o "Plan" -> package_name, incluso si viene como línea suelta: "Paquete 1".
- "Lugar" o "Dirección" -> service_address.
- Si hay apto/apartamento/casa/torre/interior/oficina/piso, llévalo a address_additional.
- Si aparece "Ciudad", "Municipio" o una ciudad clara, llévalo a locality.
- Si aparece "Localidad", llévalo a service_locality.
- Si aparece "Sector", "UPZ", "Urbanización" o un sub-sector claro, llévalo a service_sector.
- Si aparece "Departamento", llévalo a service_department.
- Si aparece "Barrio X", extrae neighborhood = X.
- Si ves una línea con 10 o más dígitos de teléfono, úsala como contact_phone_1 o contact_phone_2.
- Si el teléfono viene con indicativo internacional como +1, +34, +57 o con prefijo 00, consérvalo y responde el número en formato +<solo_dígitos>.
- Si detectas dos teléfonos, sepáralos entre contact_phone_1 y contact_phone_2.
- Si ves un texto tipo "De sus hijos / para Gloria", extrae from_name = "sus hijos" y to_name = "Gloria".
- Si ves una línea de ocasión como "Celebrar cumpleaños de mi madre", úsala como reason.
- Si ves fecha en formato natural como "16 mayo", conviértela a event_date.
- Si ves hora como "8pm", "8 pm", "20:00", conviértela a event_time.
- Si ves formatos tipo checklist como "Hora: 1:00 PM (ventana de llegada de 30 min)", responde SOLO la hora base en formato 24h. Ejemplo: "13:00".
- Ignora cualquier texto explicativo después de la hora, como "ventana de llegada", "lapso", "aprox", etc.
- Si la fecha viene como "19 de mayo de 2026", conviértela exactamente a "2026-05-19".
- Si aparece "Forma de pago", normaliza a payment_method.
- Si un campo no se detecta, ponlo como cadena vacía.

Respuesta esperada (sin markdown, sin texto adicional):
{
  "payer_name": "",
  "package_name": "",
  "service_address": "",
  "address_additional": "",
  "service_country": "",
  "service_department": "",
  "locality": "",
  "service_locality": "",
  "service_sector": "",
  "neighborhood": "",
  "contact_phone_1": "",
  "contact_phone_2": "",
  "reason": "",
  "from_name": "",
  "to_name": "",
  "event_date": "",
  "event_time": "",
  "internal_notes": "",
  "client_notes": "",
  "payment_method": ""
}

Texto a analizar:
{$validated['raw_text']}
PROMPT;

        $payload = [
            'contents' => [
                [
                    'role' => 'user',
                    'parts' => [
                        ['text' => $prompt],
                    ],
                ],
            ],
            'generationConfig' => [
                'temperature' => 0.1,
                'responseMimeType' => 'application/json',
            ],
        ];

        $models = ['gemini-2.5-flash', 'gemini-2.5-flash-lite'];
        $text = '';
        $lastError = '';
        $errors = [];

        try {
            foreach ($models as $model) {
                $response = Http::timeout(25)
                    ->acceptJson()
                    ->post("https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$geminiApiKey}", $payload);

                if ($response->successful()) {
                    $text = (string) data_get($response->json(), 'candidates.0.content.parts.0.text', '');
                    if (trim($text) !== '') {
                        break;
                    }
                } else {
                    $errorMessage = (string) data_get($response->json(), 'error.message', $response->body());
                    $errors[] = "{$model}: {$errorMessage}";
                }
            }
        } catch (\Throwable $e) {
            $lastError = $e->getMessage();
        }

        if ($lastError === '' && !empty($errors)) {
            $lastError = implode(' | ', $errors);
        }

        if (trim($text) === '') {
            return response()->json([
                'success' => false,
                'message' => 'Gemini no disponible ahora. Se usará parser local.',
                'provider_error' => $lastError,
            ]);
        }

        $text = trim((string) $text);
        $text = preg_replace('/^```json\s*|\s*```$/i', '', $text) ?: $text;

        $json = json_decode($text, true);
        if (!is_array($json)) {
            return response()->json([
                'success' => false,
                'message' => 'La respuesta de Gemini no fue JSON válido. Se usará parser local.',
                'provider_raw' => $text,
            ]);
        }

        $normalize = static fn ($value) => is_string($value) ? trim($value) : '';
        $fields = [
            'payer_name' => $normalize($json['payer_name'] ?? ''),
            'package_name' => $normalize($json['package_name'] ?? ''),
            'service_address' => $normalize($json['service_address'] ?? ''),
            'address_additional' => $normalize($json['address_additional'] ?? ''),
            'service_country' => $normalize($json['service_country'] ?? ''),
            'service_department' => $normalize($json['service_department'] ?? ''),
            'neighborhood' => $normalize($json['neighborhood'] ?? ''),
            'locality' => $normalize($json['locality'] ?? ''),
            'service_locality' => $normalize($json['service_locality'] ?? ''),
            'service_sector' => $normalize($json['service_sector'] ?? ''),
            'contact_phone_1' => $this->normalizePhoneValue($json['contact_phone_1'] ?? ''),
            'contact_phone_2' => $this->normalizePhoneValue($json['contact_phone_2'] ?? ''),
            'reason' => $normalize($json['reason'] ?? ''),
            'from_name' => $normalize($json['from_name'] ?? ''),
            'to_name' => $normalize($json['to_name'] ?? ''),
            'event_date' => $normalize($json['event_date'] ?? ''),
            'event_time' => $normalize($json['event_time'] ?? ''),
            'internal_notes' => $normalize($json['internal_notes'] ?? ''),
            'client_notes' => $normalize($json['client_notes'] ?? ''),
            'payment_method' => $normalize($json['payment_method'] ?? ''),
        ];

        $allowedPaymentMethods = ['Efectivo al finalizar', 'Transferencia', 'Tarjeta', 'Nequi', 'Daviplata'];
        if (!in_array($fields['payment_method'], $allowedPaymentMethods, true)) {
            $fields['payment_method'] = '';
        }

        $geocoded = $this->resolveAddressWithGoogle($organization, $fields);
        if (($geocoded['success'] ?? false) === true && is_array($geocoded['fields'] ?? null)) {
            $fields = $geocoded['fields'];
        }

        $applied = collect($fields)->filter(fn ($v) => $v !== '')->count();

        return response()->json([
            'success' => true,
            'fields' => $fields,
            'applied_count' => $applied,
            'google_address_resolved' => (bool) ($geocoded['success'] ?? false),
            'google_maps_url' => (string) ($geocoded['google_maps_url'] ?? ''),
        ]);
    }

    public function resolveAddress(Request $request)
    {
        $validated = $request->validate([
            'service_address' => ['nullable', 'string', 'max:255'],
            'address_additional' => ['nullable', 'string', 'max:255'],
            'place_id' => ['nullable', 'string', 'max:255'],
            'service_country' => ['nullable', 'string', 'max:80'],
            'service_department' => ['nullable', 'string', 'max:120'],
            'neighborhood' => ['nullable', 'string', 'max:120'],
            'locality' => ['nullable', 'string', 'max:120'],
            'service_locality' => ['nullable', 'string', 'max:120'],
            'service_sector' => ['nullable', 'string', 'max:120'],
        ]);

        $owner = $request->user();
        $organization = null;
        if (!empty($owner->organization_id)) {
            $organization = Organization::query()->find($owner->organization_id);
        }

        $resolved = $this->resolveAddressWithGoogle($organization, [
            'service_address' => trim((string) ($validated['service_address'] ?? '')),
            'address_additional' => trim((string) ($validated['address_additional'] ?? '')),
            'service_country' => trim((string) ($validated['service_country'] ?? '')),
            'service_department' => trim((string) ($validated['service_department'] ?? '')),
            'neighborhood' => trim((string) ($validated['neighborhood'] ?? '')),
            'locality' => trim((string) ($validated['locality'] ?? '')),
            'service_locality' => trim((string) ($validated['service_locality'] ?? '')),
            'service_sector' => trim((string) ($validated['service_sector'] ?? '')),
        ], trim((string) ($validated['place_id'] ?? '')));

        return response()->json([
            'success' => (bool) ($resolved['success'] ?? false),
            'reason' => (string) ($resolved['reason'] ?? ''),
            'fields' => $resolved['fields'] ?? [],
            'google_maps_url' => (string) ($resolved['google_maps_url'] ?? ''),
        ]);
    }

    public function addressSuggestions(Request $request)
    {
        $validated = $request->validate([
            'query' => ['required', 'string', 'min:3', 'max:255'],
            'country' => ['nullable', 'string', 'size:2'],
        ]);

        $owner = $request->user();
        $organization = null;
        if (!empty($owner->organization_id)) {
            $organization = Organization::query()->find($owner->organization_id);
        }

        $apiKey = trim((string) ($organization?->google_maps_api_key ?? ''));
        if ($apiKey === '') {
            return response()->json([
                'success' => false,
                'reason' => 'google_maps_api_not_configured',
                'suggestions' => [],
            ]);
        }

        $query = trim((string) $validated['query']);
        $country = strtoupper((string) ($validated['country'] ?? ($organization?->google_maps_country ?: 'co')));
        $suggestions = [];

        try {
            $placesResponse = Http::timeout(20)
                ->acceptJson()
                ->withHeaders([
                    'X-Goog-Api-Key' => $apiKey,
                    'X-Goog-FieldMask' => 'suggestions.placePrediction.placeId,suggestions.placePrediction.text.text',
                ])
                ->post('https://places.googleapis.com/v1/places:autocomplete', [
                    'input' => $query,
                    'languageCode' => 'es',
                    'regionCode' => $country,
                    'includedRegionCodes' => [$country],
                ]);

            if ($placesResponse->successful()) {
                $rawSuggestions = data_get($placesResponse->json(), 'suggestions', []);
                $suggestions = collect(is_array($rawSuggestions) ? $rawSuggestions : [])
                    ->map(function ($item) {
                        $prediction = data_get($item, 'placePrediction', []);
                        return [
                            'place_id' => trim((string) data_get($prediction, 'placeId', '')),
                            'description' => trim((string) data_get($prediction, 'text.text', '')),
                        ];
                    })
                    ->filter(fn ($item) => $item['description'] !== '')
                    ->take(6)
                    ->values()
                    ->all();
            }
        } catch (\Throwable $e) {
            $suggestions = [];
        }

        if (empty($suggestions)) {
            try {
                $geocodeResponse = Http::timeout(20)
                    ->acceptJson()
                    ->get('https://maps.googleapis.com/maps/api/geocode/json', [
                        'address' => $query,
                        'components' => "country:{$country}",
                        'key' => $apiKey,
                    ]);

                if ($geocodeResponse->successful() && (string) data_get($geocodeResponse->json(), 'status') === 'OK') {
                    $results = data_get($geocodeResponse->json(), 'results', []);
                    $suggestions = collect(is_array($results) ? $results : [])
                        ->map(fn ($result) => [
                            'place_id' => trim((string) data_get($result, 'place_id', '')),
                            'description' => trim((string) data_get($result, 'formatted_address', '')),
                        ])
                        ->filter(fn ($item) => $item['description'] !== '')
                        ->take(6)
                        ->values()
                        ->all();
                }
            } catch (\Throwable $e) {
                $suggestions = [];
            }
        }

        return response()->json([
            'success' => !empty($suggestions),
            'reason' => !empty($suggestions) ? 'ok' : 'no_results',
            'suggestions' => $suggestions,
        ]);
    }

    public function store(Request $request)
    {
        $fallbackPhone1 = $this->normalizePhoneValue((string) $request->input('contact_phone_1_display', ''));
        $fallbackPhone2 = $this->normalizePhoneValue((string) $request->input('contact_phone_2_display', ''));

        $request->merge([
            'contact_phone_1' => $this->normalizePhoneValue((string) $request->input('contact_phone_1', '')) ?: $fallbackPhone1,
            'contact_phone_2' => $this->normalizePhoneValue((string) $request->input('contact_phone_2', '')) ?: $fallbackPhone2,
            'base_price' => $this->normalizeCopInput($request->input('base_price')),
            'gift_amount' => $this->normalizeCopInput($request->input('gift_amount')),
            'artist_amount' => $this->normalizeCopInput($request->input('artist_amount')),
            'other_cost_amount' => $this->normalizeCopInput($request->input('other_cost_amount')),
            'discount_amount' => $this->normalizeCopInput($request->input('discount_amount')),
            'service_status' => $this->normalizeServiceStatus($request->input('service_status')),
            'payment_status' => 'Pendiente',
        ]);

        if (
            $this->normalizePhoneValue((string) $request->input('contact_phone_1', '')) === ''
            && $this->normalizePhoneValue((string) $request->input('contact_phone_2', '')) !== ''
        ) {
            $request->merge([
                'contact_phone_1' => $this->normalizePhoneValue((string) $request->input('contact_phone_2', '')),
                'contact_phone_2' => '',
            ]);
        }

        if ($this->normalizePhoneValue((string) $request->input('contact_phone_1', '')) === '') {
            Log::warning('serenata.store.phone1_empty_before_validation', [
                'user_id' => (int) ($request->user()->id ?? 0),
                'contact_phone_1_raw' => $request->input('contact_phone_1'),
                'contact_phone_1_display_raw' => $request->input('contact_phone_1_display'),
                'contact_phone_2_raw' => $request->input('contact_phone_2'),
                'contact_phone_2_display_raw' => $request->input('contact_phone_2_display'),
                'contact_phone_1_normalized' => $this->normalizePhoneValue((string) $request->input('contact_phone_1', '')),
                'contact_phone_2_normalized' => $this->normalizePhoneValue((string) $request->input('contact_phone_2', '')),
            ]);
        }

        $validated = $request->validate([
            'submission_token' => ['required', 'string', 'max:120'],
            'mariachi_group_id' => ['required', 'integer', 'min:1'],
            'serenata_package_id' => ['nullable', 'integer', 'min:1'],
            'payer_name' => ['required', 'string', 'max:255'],
            'payer_email' => ['nullable', 'string', 'lowercase', 'email', 'max:255'],
            'service_address' => ['required', 'string', 'max:255'],
            'address_additional' => ['nullable', 'string', 'max:255'],
            'service_country' => ['required', 'string', 'max:80'],
            'service_department' => ['nullable', 'string', 'max:120'],
            'neighborhood' => ['required', 'string', 'max:120'],
            'locality' => ['required', 'string', 'max:120'],
            'service_locality' => ['nullable', 'string', 'max:120'],
            'service_sector' => ['nullable', 'string', 'max:120'],
            'contact_phone_1' => ['required', 'string', 'max:30'],
            'contact_phone_2' => ['nullable', 'string', 'max:30'],
            'reason' => ['required', 'string', 'max:255'],
            'from_name' => ['required', 'string', 'max:255'],
            'to_name' => ['required', 'string', 'max:255'],
            'event_date' => ['required', 'date'],
            'event_time' => ['required', 'date_format:H:i'],
            'internal_notes' => ['nullable', 'string', 'max:1200'],
            'client_notes' => ['nullable', 'string', 'max:1200'],
            'base_price' => ['required', 'numeric', 'min:0'],
            'gift_amount' => ['nullable', 'numeric', 'min:0'],
            'gift_items' => ['nullable', 'string', 'max:5000'],
            'artist_amount' => ['nullable', 'numeric', 'min:0'],
            'artist_items' => ['nullable', 'string', 'max:5000'],
            'other_cost_amount' => ['nullable', 'numeric', 'min:0'],
            'other_cost_items' => ['nullable', 'string', 'max:5000'],
            'discount_amount' => ['nullable', 'numeric', 'min:0'],
            'commission_rate' => ['nullable', 'numeric', 'min:0', 'max:100'],
            'payment_status' => ['required', 'in:Pendiente'],
            'service_status' => ['required', 'in:' . implode(',', $this->serviceStatusOptions())],
            'payment_method' => ['nullable', 'string', 'max:120'],
            'cancellation_reason' => ['nullable', 'string', 'max:600'],
            'ceded_amount' => ['nullable', 'numeric'],
        ]);

        if (($validated['service_status'] ?? '') === self::SERVICE_STATUS_CANCELLED && empty(trim((string) ($validated['cancellation_reason'] ?? '')))) {
            return back()
                ->withErrors(['cancellation_reason' => 'El motivo de cancelación es obligatorio cuando el estado es Cancelada.'])
                ->withInput();
        }

        $validated = $this->normalizeAddressHierarchyFields($validated, Organization::query()->find((int) ($request->user()->organization_id ?? 0)));
        $validated['contact_phone_1'] = $this->normalizePhoneValue($validated['contact_phone_1'] ?? '');
        $validated['contact_phone_2'] = $this->normalizePhoneValue($validated['contact_phone_2'] ?? '');

        if ($validated['contact_phone_1'] === '') {
            Log::warning('serenata.store.phone1_empty_after_validation', [
                'user_id' => (int) ($request->user()->id ?? 0),
                'validated_contact_phone_1' => $validated['contact_phone_1'] ?? null,
                'validated_contact_phone_2' => $validated['contact_phone_2'] ?? null,
                'contact_phone_1_raw' => $request->input('contact_phone_1'),
                'contact_phone_1_display_raw' => $request->input('contact_phone_1_display'),
                'contact_phone_2_raw' => $request->input('contact_phone_2'),
                'contact_phone_2_display_raw' => $request->input('contact_phone_2_display'),
            ]);
            return back()->withErrors([
                'contact_phone_1' => 'Ingresa un número de contacto válido.',
            ])->withInput();
        }

        if ($validated['contact_phone_2'] === '') {
            $validated['contact_phone_2'] = null;
        }

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

        if (!$mariachiGroup || !OwnerWorkspace::canAccessGroup($request->user(), $mariachiGroupId)) {
            return back()->withErrors([
                'mariachi_group_id' => 'Selecciona un grupo musical válido.',
            ])->withInput();
        }
        $serenataPackageId = (int) ($validated['serenata_package_id'] ?? 0);
        if ($serenataPackageId > 0) {
            $package = SerenataPackage::query()
                ->where('organization_id', $organizationId)
                ->where('mariachi_group_id', $mariachiGroupId)
                ->where('is_active', true)
                ->whereKey($serenataPackageId)
                ->first();
            if (!$package) {
                return back()->withErrors([
                    'serenata_package_id' => 'Selecciona un paquete válido.',
                ])->withInput();
            }
        } else {
            $serenataPackageId = null;
        }
        $giftCatalog = collect($this->giftOptions($organizationId, (string) $validated['event_date'], $mariachiGroupId))->keyBy('code');
        $artistCatalog = collect($this->artistOptions($organizationId, (string) $validated['event_date'], $mariachiGroupId))->keyBy('code');
        $otherCostCatalog = collect($this->otherCostOptions($organizationId, (string) $validated['event_date'], $mariachiGroupId))->keyBy('code');
        $selectedGiftItems = [];
        $selectedArtistItems = [];
        $selectedOtherCostItems = [];
        $giftTotalFromItems = 0.0;
        $artistTotalFromItems = 0.0;
        $otherCostTotalFromItems = 0.0;

        $rawGiftItems = trim((string) ($validated['gift_items'] ?? ''));
        if ($rawGiftItems !== '') {
            $decoded = json_decode($rawGiftItems, true);
            if (is_array($decoded)) {
                foreach ($decoded as $item) {
                    $code = trim((string) data_get($item, 'code', ''));
                    $quantity = (int) data_get($item, 'quantity', 0);
                    if ($code === '' || $quantity < 1 || !$giftCatalog->has($code)) {
                        continue;
                    }

                    $catalogItem = $giftCatalog->get($code);
                    $unitPrice = (float) data_get($catalogItem, 'price', 0);
                    $lineTotal = $unitPrice * $quantity;

                    $selectedGiftItems[] = [
                        'code' => $code,
                        'name' => (string) data_get($catalogItem, 'name', $code),
                        'unit_price' => $unitPrice,
                        'quantity' => $quantity,
                        'total' => $lineTotal,
                    ];
                    $giftTotalFromItems += $lineTotal;
                }
            }
        }

        $rawArtistItems = trim((string) ($validated['artist_items'] ?? ''));
        if ($rawArtistItems !== '') {
            $decoded = json_decode($rawArtistItems, true);
            if (is_array($decoded)) {
                foreach ($decoded as $item) {
                    $code = trim((string) data_get($item, 'code', ''));
                    if ($code === '' || !$artistCatalog->has($code)) {
                        continue;
                    }

                    $catalogItem = $artistCatalog->get($code);
                    $unitPrice = max(0, (float) data_get($item, 'unit_price', data_get($catalogItem, 'price', 0)));

                    $selectedArtistItems[] = [
                        'code' => $code,
                        'name' => (string) data_get($catalogItem, 'name', $code),
                        'unit_price' => $unitPrice,
                        'type' => (string) data_get($catalogItem, 'type', 'included'),
                    ];
                    $artistTotalFromItems += $unitPrice;
                }
            }
        }

        $rawOtherCostItems = trim((string) ($validated['other_cost_items'] ?? ''));
        if ($rawOtherCostItems !== '') {
            $decoded = json_decode($rawOtherCostItems, true);
            if (is_array($decoded)) {
                foreach ($decoded as $item) {
                    $code = trim((string) data_get($item, 'code', ''));
                    if ($code === '' || !$otherCostCatalog->has($code)) {
                        continue;
                    }

                    $catalogItem = $otherCostCatalog->get($code);
                    $unitPrice = max(0, (float) data_get($item, 'unit_price', data_get($catalogItem, 'price', 0)));

                    $selectedOtherCostItems[] = [
                        'code' => $code,
                        'name' => (string) data_get($catalogItem, 'name', $code),
                        'unit_price' => $unitPrice,
                    ];
                    $otherCostTotalFromItems += $unitPrice;
                }
            }
        }

        $base = (float) $validated['base_price'];
        $gift = $giftTotalFromItems > 0
            ? $giftTotalFromItems
            : (float) ($validated['gift_amount'] ?? 0);
        $artist = $artistTotalFromItems > 0
            ? $artistTotalFromItems
            : (float) ($validated['artist_amount'] ?? 0);
        $otherCost = $otherCostTotalFromItems > 0
            ? $otherCostTotalFromItems
            : (float) ($validated['other_cost_amount'] ?? 0);
        $discount = (float) ($validated['discount_amount'] ?? 0);
        $baseCost = max(0, $gift + $artist + $otherCost);
        $totalCharged = max(0, $base - $discount);
        $profit = $totalCharged - $baseCost;
        $commissionRate = max(0, min(100, (float) ($validated['commission_rate'] ?? 0)));
        $commissionAmount = $commissionRate > 0
            ? max(0, $profit * ($commissionRate / 100))
            : max(0, $profit);
        $organizationId = (int) ($request->user()->organization_id ?? 0);
        if ($organizationId <= 0) {
            return back()->withErrors([
                'payer_name' => 'Tu usuario no tiene organización asignada.',
            ])->withInput();
        }

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

        $selectedMethod = trim((string) ($validated['payment_method'] ?? ''));
        $selectedMethodExists = $methodOptions
            ->pluck('name')
            ->contains(fn ($name) => mb_strtolower((string) $name, 'UTF-8') === mb_strtolower($selectedMethod, 'UTF-8'));

        if (!$selectedMethodExists) {
            return back()->withErrors([
                'payment_method' => 'Selecciona un método válido de Configuración > Pagos.',
            ])->withInput();
        }

        $submissionToken = trim((string) ($validated['submission_token'] ?? ''));
        $submissionCacheKey = "serenata_submission:{$organizationId}:{$submissionToken}";
        $isFirstSubmission = Cache::add($submissionCacheKey, true, now()->addMinutes(10));

        if (!$isFirstSubmission) {
            return back()->with('status', 'La serenata ya se estaba procesando. Evitamos un registro duplicado.');
        }

        $customerId = null;
        $payerName = trim((string) ($validated['payer_name'] ?? ''));
        if ($payerName !== '') {
            $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::create([
                    'organization_id' => $organizationId,
                    'mariachi_group_id' => $mariachiGroupId,
                    'name' => $payerName,
                    'email' => trim((string) ($validated['payer_email'] ?? '')) ?: null,
                    'mobile' => trim((string) ($validated['contact_phone_1'] ?? '')) ?: null,
                    'address_line_1' => trim((string) ($validated['service_address'] ?? '')) ?: null,
                    'address_line_2' => trim((string) ($validated['address_additional'] ?? '')) ?: null,
                    'town' => trim((string) ($validated['locality'] ?? '')) ?: null,
                    'state_province' => trim((string) ($validated['service_department'] ?? '')) ?: null,
                    'post_code' => null,
                    'country' => trim((string) ($validated['service_country'] ?? '')) ?: 'Colombia',
                    'is_billing_same' => true,
                    'is_active' => true,
                ]);
            }

            if ($customer) {
                $customerUpdates = [];
                if (empty($customer->email) && !empty($validated['payer_email'])) {
                    $customerUpdates['email'] = trim((string) $validated['payer_email']);
                }
                if (empty($customer->mobile) && !empty($validated['contact_phone_1'])) {
                    $customerUpdates['mobile'] = trim((string) $validated['contact_phone_1']);
                }
                if (!empty($customerUpdates)) {
                    $customer->update($customerUpdates);
                }
                $customerId = (int) $customer->id;
            }
        }

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

        $serenataCode = $mariachiGroup->serenataCodePrefix() . '-' . $nextSequence;

        $serenata = Serenata::create([
            'organization_id' => $organizationId,
            'created_by_user_id' => (int) $request->user()->id,
            'customer_id' => $customerId,
            'mariachi_group_id' => $mariachiGroupId,
            'serenata_package_id' => $serenataPackageId,
            'sequence' => $nextSequence,
            'code' => $serenataCode,
            'payer_name' => $validated['payer_name'],
            'payer_email' => trim((string) ($validated['payer_email'] ?? '')) ?: null,
            'service_address' => $validated['service_address'],
            'address_additional' => $validated['address_additional'] ?? null,
            'service_country' => $validated['service_country'],
            'service_department' => $validated['service_department'] ?: null,
            'neighborhood' => $validated['neighborhood'],
            'locality' => $validated['locality'],
            'service_locality' => $validated['service_locality'] ?: null,
            'service_sector' => $validated['service_sector'] ?: null,
            'contact_phone_1' => $validated['contact_phone_1'],
            'contact_phone_2' => $validated['contact_phone_2'] ?? null,
            'reason' => $validated['reason'],
            'from_name' => $validated['from_name'],
            'to_name' => $validated['to_name'],
            'event_date' => $validated['event_date'],
            'event_time' => $validated['event_time'],
            'internal_notes' => $validated['internal_notes'] ?? null,
            'client_notes' => $validated['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' => $validated['service_status'],
            'payment_method' => $validated['payment_method'],
            'cancellation_reason' => $validated['cancellation_reason'] ?? null,
            'ceded_amount' => (float) ($validated['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::create([
            'organization_id' => $organizationId,
            'serenata_id' => $serenata->id,
            'changed_by_user_id' => (int) $request->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',
        ]);

        return redirect()->route('owner.serenatas.index')->with('status', 'Serenata registrada correctamente.');
    }

    private function normalizePhoneValue(mixed $value): string
    {
        return PhoneNumber::normalize(is_scalar($value) ? (string) $value : '');
    }

    private function normalizeCopInput(mixed $value): string
    {
        if (is_int($value) || is_float($value)) {
            return (string) $value;
        }

        $raw = is_scalar($value) ? trim((string) $value) : '';
        if ($raw === '') {
            return '0';
        }

        $normalized = preg_replace('/[^\d]/', '', $raw) ?? '';

        return $normalized !== '' ? $normalized : '0';
    }

    private function normalizeNullableCopInput(mixed $value): ?string
    {
        if (is_int($value) || is_float($value)) {
            return (string) $value;
        }

        $raw = is_scalar($value) ? trim((string) $value) : '';
        if ($raw === '') {
            return null;
        }

        $normalized = preg_replace('/[^\d]/', '', $raw) ?? '';

        return $normalized !== '' ? $normalized : null;
    }
}
