<?php

namespace App\Http\Controllers;

use App\Mail\PaymentRequestMail;
use App\Models\Booking;
use App\Models\Communication;
use App\Models\Customer;
use App\Models\Deposit;
use App\Models\Job;
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
use Stripe\StripeClient;
use Throwable;

class PaymentController extends Controller
{
    /* =========================================================================
     | Stripe client
     * ========================================================================= */
    public function stripe(): StripeClient
    {
        return new StripeClient((string) config('services.stripe.secret'));
    }

    /* =========================================================================
     | ----------------------------- COMMUNICATIONS ----------------------------
     | Email payment request flow (SendGrid headers added in the Mailable)
     * ========================================================================= */

    public function sendPaymentRequest(Job $job)
    {
        $to = $job->customer?->email ?? null;
        abort_unless($to, 422, 'Customer email missing.');

        return DB::transaction(function () use ($job, $to) {
            $comm = Communication::create([
                'job_id'   => $job->id,
                'channel'  => 'email',
                'type'     => 'payment_request',
                'to_email' => $to,
                'subject'  => "Payment request for job #{$job->id}",
                'status'   => 'queued',
                'meta'     => ['job_id' => $job->id],
            ]);

            Mail::to($to)->send(new PaymentRequestMail($comm, ['job' => $job]));

            $comm->update(['status' => 'sent']);
            $comm->events()->create([
                'event'       => 'sent',
                'occurred_at' => now(),
                'payload'     => null,
            ]);

            if (class_exists(\App\Models\Event::class)) {
                \App\Models\Event::create([
                    'job_id'  => $job->id,
                    'type'    => 'communication.sent',
                    'message' => "Payment request email sent to {$comm->to_email}",
                    'meta'    => ['communication_id' => $comm->id],
                ]);
            }

            return response()->json(['ok' => true, 'communication_id' => $comm->id]);
        });
    }

    /* =========================================================================
     | ------------------------------- ADMIN -----------------------------------
     * ========================================================================= */

    public function deposit(Request $request, Booking $booking)
    {
        $customer = $booking->customer;
        abort_unless($customer, 422, 'Customer not found for booking.');

        $fallback = (int) ($booking->deposit_amount ?? round(((int) ($booking->total_amount ?? 0)) * 0.5));
        $amount   = (int) $request->integer('amount_cents', $fallback);
        abort_if($amount <= 0, 422, 'Invalid deposit amount.');

        $currency = strtoupper($booking->currency ?? 'NZD');
        $pmId     = trim((string) $request->input('payment_method', ''));

        $payment = $this->firstOrCreatePayment($booking, $customer, 'booking_deposit', $amount, $currency, 'card');

        $stripe  = $this->stripe();
        $idemKey = "deposit:{$booking->id}:{$amount}";

        try {
            $params = [
                'amount'   => $amount,
                'currency' => strtolower($currency),
                'metadata' => [
                    'booking_id' => (string) $booking->id,
                    'reference'  => (string) $booking->reference,
                    'purpose'    => 'booking_deposit',
                    'payment_id' => (string) $payment->id,
                ],
                'automatic_payment_methods' => ['enabled' => true],
            ];
            if ($customer->stripe_customer_id) $params['customer'] = $customer->stripe_customer_id;
            if ($pmId !== '') {
                $params['payment_method'] = $pmId;
                $params['confirm']        = true;
                $params['off_session']    = true;
            }

            $pi = $stripe->paymentIntents->create($params, ['idempotency_key' => $idemKey]);

            $this->syncPaymentFromPI($payment, $pi, savePm: true);
            $this->maybeSetDefaultPm($pi);

            return back()->with('status', "Deposit payment created (PI {$pi->id}).");
        } catch (Throwable $e) {
            Log::error('[payment.deposit] error', ['booking' => $booking->id, 'amount' => $amount, 'error' => $e->getMessage()]);
            $payment->update(['status' => 'failed']);
            return back()->withErrors('Could not create deposit: ' . $e->getMessage());
        }
    }

    public function balance(Request $request, Booking $booking)
    {
        $customer = $booking->customer;
        abort_unless($customer, 422, 'Customer not found for booking.');

        [$computed, $currency] = $this->calcBalance($booking);
        $amount = (int) $request->integer('amount_cents', $computed);
        abort_if($amount <= 0, 422, 'No balance due.');

        $pmId = trim((string) $request->input('payment_method', ''));

        $payment = $this->firstOrCreatePayment($booking, $customer, 'booking_balance', $amount, $currency, 'card');

        $stripe  = $this->stripe();
        $idemKey = "balance:{$booking->id}:{$amount}";

        try {
            $params = [
                'amount'   => $amount,
                'currency' => strtolower($currency),
                'metadata' => [
                    'booking_id' => (string) $booking->id,
                    'reference'  => (string) $booking->reference,
                    'purpose'    => 'booking_balance',
                    'payment_id' => (string) $payment->id,
                ],
                'automatic_payment_methods' => ['enabled' => true],
            ];
            if ($customer->stripe_customer_id) $params['customer'] = $customer->stripe_customer_id;
            if ($pmId !== '') {
                $params['payment_method'] = $pmId;
                $params['confirm']        = true;
                $params['off_session']    = true;
            }

            $pi = $stripe->paymentIntents->create($params, ['idempotency_key' => $idemKey]);

            $this->syncPaymentFromPI($payment, $pi, savePm: true);
            $this->maybeSetDefaultPm($pi);

            return back()->with('status', "Balance payment created (PI {$pi->id}).");
        } catch (Throwable $e) {
            Log::error('[payment.balance] error', ['booking' => $booking->id, 'amount' => $amount, 'error' => $e->getMessage()]);
            $payment->update(['status' => 'failed']);
            return back()->withErrors('Could not create balance: ' . $e->getMessage());
        }
    }

    public function postHireCharge(Booking $booking, Request $req)
    {
        $amount   = (int) $req->integer('amount_cents');
        $desc     = (string) $req->string('description', 'Post-hire charge');
        $customer = $booking->customer;

        abort_unless($customer, 400, 'No customer on this booking.');
        abort_if($amount <= 0, 400, 'Amount must be > 0.');

        $stripeCustomerId = $this->ensureStripeCustomer($customer);
        $pmId = $this->pickPaymentMethodId($stripeCustomerId, $booking);
        if (!$pmId) {
            return back()->with('claim_error', 'No saved card for this customer.');
        }

        $stripe = $this->stripe();
        try {
            $pi = $stripe->paymentIntents->create([
                'amount'         => $amount,
                'currency'       => strtolower($booking->currency ?? 'nzd'),
                'customer'       => $stripeCustomerId,
                'payment_method' => $pmId,
                'off_session'    => true,
                'confirm'        => true,
                'description'    => "Post-hire charge for booking {$booking->reference}: {$desc}",
                'metadata'       => [
                    'booking_id' => (string) $booking->id,
                    'purpose'    => 'post_charge',
                    'reason'     => $desc,
                ],
            ]);
        } catch (\Stripe\Exception\CardException $e) {
            $pi = $e->getError()->payment_intent ?? null;
            if ($pi && $pi->status === 'requires_action') {
                $payment = $this->firstOrCreatePayment($booking, $customer, 'post_charge', $amount, strtoupper($booking->currency ?? 'NZD'), 'card');
                $payment->update([
                    'status'                    => 'requires_action',
                    'stripe_payment_intent_id'  => $pi->id,
                    'stripe_payment_method_id'  => $pmId,
                ]);
                return back()->with('claim_error', 'Card needs authentication.');
            }
            throw $e;
        }

        $payment = $this->firstOrCreatePayment(
            $booking,
            $customer,
            'post_charge',
            (int) ($pi->amount_received ?? $pi->amount ?? $amount),
            strtoupper($booking->currency ?? 'NZD'),
            'card'
        );

        $payment->update([
            'status'                   => $pi->status,
            'stripe_payment_intent_id' => $pi->id,
            'stripe_payment_method_id' => $pmId,
        ]);

        $this->maybeSetDefaultPm($pi);
        return back()->with('claim_ok', 'Post-hire charge succeeded.');
    }

    /* =========================================================================
     | ------------------------ ADMIN: holds (bond) ----------------------------
     * ========================================================================= */

    public function captureHold(Request $request, Payment $payment)
{
    $request->validate(['amount' => 'nullable|integer|min:1']);
    $piId = $payment->stripe_payment_intent_id ?? null;
    abort_if(!$piId, 400, 'This payment has no Stripe PI id.');

    $args = $request->filled('amount') ? ['amount_to_capture' => (int) $request->integer('amount')] : [];
    $pi = $this->stripe()->paymentIntents->capture($piId, $args);

    // For holds, when capture succeeds, mark as 'captured' and set the received amount
    $payment->status = 'captured';
    $this->setPaymentAmount($payment, (int) ($pi->amount_received ?? 0) ?: ($this->getPaymentAmount($payment) ?? 0));
    $payment->save();

    return back()->with('status', 'Hold captured.');
}


    public function releaseHold(Payment $payment)
{
    $piId = $payment->stripe_payment_intent_id ?? null;
    abort_if(!$piId, 400, 'This payment has no Stripe PI id.');

    $pi = $this->stripe()->paymentIntents->cancel($piId);

    // Stripe returns 'canceled'; keep that or map to a custom 'voided' if you prefer
    $payment->status = 'canceled';
    $payment->save();

    return back()->with('status', 'Hold released.');
}


    /* =========================================================================
     | ------------------------------ PORTAL (JOBS) ----------------------------
     * ========================================================================= */

    private function jobChargeCentsResolved(Job $job): int
    {
        foreach ([
            $job->charge_cents       ?? null,
            $job->total_cents        ?? null,
            $job->amount_due_cents   ?? null,
        ] as $v) {
            if ($v !== null && (int) $v > 0) return (int) $v;
        }
        $dollar = $job->charge_amount ?? $job->total_amount ?? null;
        if (!is_null($dollar) && (float) $dollar > 0) {
            return (int) round(((float) $dollar) * 100);
        }
        return 0;
    }

  private function jobPaidCentsResolved(Job $job): int
{
    if (Schema::hasColumn('jobs', 'paid_cents') && !is_null($job->paid_cents)) {
        return (int) $job->paid_cents;
    }

    try {
        if (method_exists($job, 'payments') && Schema::hasTable('payments')) {
            $col = Schema::hasColumn('payments','amount_cents')
                ? 'amount_cents'
                : (Schema::hasColumn('payments','amount') ? 'amount' : null);

            if ($col) {
                return (int) $job->payments()
                    ->whereIn('status', ['succeeded','paid','captured','completed'])
                    ->where(function ($q) {
                        $q->whereNull('purpose')
                          ->orWhereNotIn('purpose', ['hold','bond_hold']);
                    })
                    ->where(function ($q) {
                        $q->whereNull('type')
                          ->orWhere('type','!=','hold');
                    })
                    // Optional: also exclude mechanism='hold' if that column exists
                    ->when(Schema::hasColumn('payments', 'mechanism'), function ($q) {
                        $q->where(function ($q) {
                            $q->whereNull('mechanism')
                              ->orWhere('mechanism','!=','hold');
                        });
                    })
                    ->sum($col);
            }
        }
    } catch (Throwable $e) {
        // swallow and return 0 below
    }

    return 0;
}


    private function jobHoldCentsResolved(Job $job): int
    {
        foreach ([
            optional($job->flow)->hold_cents ?? null,
            $job->hold_cents                 ?? null,
        ] as $v) {
            if ($v !== null && (int) $v > 0) return (int) $v;
        }
        $dollar = $job->hold_amount ?? optional($job->flow)->hold_amount ?? null;
        if (!is_null($dollar) && (float) $dollar > 0) {
            return (int) round(((float) $dollar) * 100);
        }
        return 0;
    }

    private function jobCurrency(Job $job): string
    {
        $c = $job->currency ?? optional($job->flow)->currency ?? 'NZD';
        return strtoupper((string) $c);
    }

    private function jobStripeCustomerIdFor(Job $job): ?string
    {
        $cust = $job->customer ?? null;
        if ($cust instanceof Customer) {
            return $this->ensureStripeCustomer($cust);
        }

        $email = $job->customer_email ?? null;
        if (!$email) return null;

        try {
            $sc = $this->stripe()->customers->create([
                'email' => $email,
                'name'  => $job->customer_name ?? null,
                'metadata' => ['job_id' => (string) $job->id],
            ]);
            if (Schema::hasColumn('jobs', 'psp_customer_id')) {
                $job->psp_customer_id = $sc->id;
                $job->save();
            }
            return $sc->id;
        } catch (Throwable $e) {
            Log::warning('Stripe: could not create customer for job', ['job' => $job->id, 'err' => $e->getMessage()]);
            return null;
        }
    }

    public function portalBundleForJob(Request $request, Job $job)
    {
        $stripe   = $this->stripe();
        $currency = strtolower($this->jobCurrency($job));
        $custId   = $this->jobStripeCustomerIdFor($job);

        $requestedCharge = (int) $request->integer('requested_charge_cents', 0);
        $requestedHold   = (int) $request->integer('requested_hold_cents', 0);

        $total   = $this->jobChargeCentsResolved($job);
        $paid    = $this->jobPaidCentsResolved($job);
        $remain  = max(0, $total - $paid);
        $holdCts = $this->jobHoldCentsResolved($job);

        $chargeCents = $requestedCharge > 0 ? $requestedCharge : $remain;
        $holdCents   = $requestedHold   > 0 ? $requestedHold   : $holdCts;

        $out = ['ok' => true, 'payment' => null, 'hold' => null];

        if ($chargeCents > 0) {
            $pi = $stripe->paymentIntents->create([
                'amount'                    => $chargeCents,
                'currency'                  => $currency,
                'customer'                  => $custId,
                'automatic_payment_methods' => ['enabled' => true],
                'setup_future_usage'        => 'off_session',
                'description'               => "Job {$job->id} - balance",
                'metadata'                  => [
                    'job_id'  => (string) $job->id,
                    'purpose' => 'job_balance',
                ],
            ], [
                'idempotency_key' => "job-{$job->id}-balance-{$chargeCents}",
            ]);

            if (Schema::hasTable('payments')) {
                $p = new Payment();
                if (Schema::hasColumn('payments','job_id'))    $p->job_id    = $job->id;
                if (Schema::hasColumn('payments','purpose'))   $p->purpose   = 'job_balance';
                if (Schema::hasColumn('payments','status'))    $p->status    = 'pending';
                if (Schema::hasColumn('payments','currency'))  $p->currency  = strtoupper($currency);
                $amountCol = Schema::hasColumn('payments','amount_cents') ? 'amount_cents' : (Schema::hasColumn('payments','amount') ? 'amount' : null);
                if ($amountCol) $p->{$amountCol} = $chargeCents;
                $piCol = Schema::hasColumn('payments','stripe_payment_intent_id') ? 'stripe_payment_intent_id'
                        : (Schema::hasColumn('payments','stripe_pi_id') ? 'stripe_pi_id' : null);
                if ($piCol) $p->{$piCol} = $pi->id;
                $p->save();
            }

            $out['payment'] = ['intent_id' => $pi->id, 'client_secret' => $pi->client_secret];
        }

        if ($holdCents > 0) {
            $bond = $stripe->paymentIntents->create([
    'amount'                 => $holdCents,
    'currency'               => $currency,
    'customer'               => $custId,
    'payment_method_types'   => ['card'],   // required for manual capture
    'capture_method'         => 'manual',   // authorise now, capture later
    'setup_future_usage'     => 'off_session', // save card for later off-session charges
    'description'            => "Job {$job->id} - bond hold",
    'metadata'               => [
        'job_id'   => (string) $job->id,
        'purpose'  => 'bond_hold',
                ],
            ], [
    'idempotency_key' => "job-{$job->id}-hold-{$holdCents}",
            ]);

            if (Schema::hasTable('deposits')) {
                try {
                    $this->upsertDepositFor($job, $bond, false, ['from' => 'portal.bundle']);
                } catch (Throwable $e) {
                    Log::warning('Could not seed Deposit for hold', ['job' => $job->id, 'err' => $e->getMessage()]);
                }
            }

            $out['hold'] = ['intent_id' => $bond->id, 'client_secret' => $bond->client_secret];
        }

        return response()->json($out);
    }

    public function recordJobPaid(Request $request, Job $job)
    {
        $pi       = (string) $request->input('payment_intent');
        $pm       = (string) $request->input('payment_method');
        $chargeId = (string) $request->input('charge');
        $amount   = (int)    $request->integer('amount_cents', 0);
        $currency = (string) $request->input('currency', $this->jobCurrency($job));
        $meta     = (array)  $request->input('meta', []);

        if ($amount <= 0 && $pi) {
            try {
                $spi = $this->stripe()->paymentIntents->retrieve($pi);
                $amount = (int) ($spi->amount_received ?? $spi->amount ?? 0);
                $currency = strtoupper($spi->currency ?? $currency);
            } catch (Throwable $e) {}
        }

        abort_if($amount <= 0, 422, 'amount_cents required');

        return DB::transaction(function () use ($job, $amount, $currency, $pi, $pm, $chargeId, $meta, $request) {

            // Build Payment (if table present)
            $payment = null;
            if (Schema::hasTable('payments')) {
                $p = new Payment();
                if (Schema::hasColumn('payments','job_id'))   $p->job_id   = $job->id;
                if (Schema::hasColumn('payments','status'))   $p->status   = 'succeeded';
                if (Schema::hasColumn('payments','type'))     $p->type     = 'charge';
                if (Schema::hasColumn('payments','purpose'))  $p->purpose  = 'job_balance';
                if (Schema::hasColumn('payments','currency')) $p->currency = strtoupper($currency);

                $this->setPaymentAmount($p, $amount);

                // PI / PM / charge columns (with aliases)
                $piCol = Schema::hasColumn('payments','stripe_payment_intent_id') ? 'stripe_payment_intent_id'
                       : (Schema::hasColumn('payments','stripe_payment_intent')   ? 'stripe_payment_intent' : null);
                if ($piCol && $pi) $p->{$piCol} = $pi;

                $pmCol = Schema::hasColumn('payments','stripe_payment_method_id') ? 'stripe_payment_method_id'
                       : (Schema::hasColumn('payments','stripe_payment_method')   ? 'stripe_payment_method' : null);
                if ($pmCol && $pm) $p->{$pmCol} = $pm;

                $chCol = Schema::hasColumn('payments','stripe_charge_id') ? 'stripe_charge_id'
                       : (Schema::hasColumn('payments','stripe_charge')    ? 'stripe_charge' : null);
                if ($chCol && $chargeId) $p->{$chCol} = $chargeId;

                if (Schema::hasColumn('payments','meta')) $p->meta = $meta;

                $p->save();
                $payment = $p;
            }

            // Single, guarded afterCommit: send receipt
            if (! $payment instanceof \App\Models\Payment) {
                Log::info('Skipping receipt: no Payment record available', ['job_id' => $job->id]);
            } else {
                DB::afterCommit(function () use ($job, $payment, $request, $pi) {
                    try {
                        $postedEmail = (string) $request->input('receipt_email');

                        $spi = null;
                        if (!empty($pi)) {
                            try {
                                $spi = $this->stripe()->paymentIntents->retrieve($pi);
                            } catch (\Throwable $e) {
                                Log::info('Non-fatal: unable to retrieve PaymentIntent for receipt', [
                                    'job_id' => $job->id,
                                    'pi'     => $pi,
                                    'err'    => $e->getMessage(),
                                ]);
                            }
                        }

                        $this->sendPaymentReceipt($job, $payment, $spi, $postedEmail);
                    } catch (\Throwable $e) {
                        Log::warning('sendPaymentReceipt failed (afterCommit)', [
                            'job_id' => $job->id,
                            'err'    => $e->getMessage(),
                        ]);
                    }
                });
            }

            // ---- Update Job aggregates / status
            if (Schema::hasColumn('jobs', 'paid_cents')) {
                $job->paid_cents = (int) ($job->paid_cents ?? 0) + $amount;
            }
            if (Schema::hasColumn('jobs', 'currency') && empty($job->currency)) {
                // preserve existing currency if already set
                $job->currency = $job->currency ?: ($payment->currency ?? 'NZD');
            }

            $total   = $this->jobChargeCentsResolved($job);
            $paidNow = Schema::hasColumn('jobs', 'paid_cents') ? (int) $job->paid_cents : $this->jobPaidCentsResolved($job);
            $balance = max(0, $total - $paidNow);

            if (Schema::hasColumn('jobs', 'balance_cents')) {
                $job->balance_cents = $balance;
            }
            if (Schema::hasColumn('jobs', 'status')) {
                $job->status = ($balance === 0 && $total > 0) ? 'paid' : ($job->status ?? 'unpaid');
            }

            $job->save();

            if (class_exists(\App\Models\Event::class)) {
                \App\Models\Event::create([
                    'job_id'  => $job->id,
                    'type'    => 'payment.succeeded',
                    'message' => "Payment of " . number_format($amount / 100, 2) . " " . strtoupper($currency) . " succeeded",
                    'meta'    => compact('pi','pm','chargeId'),
                ]);
            }

            return response()->json(['ok' => true, 'balance_cents' => $balance]);
        });
    }

    public function portalJobComplete(Job $job)
    {
        $job->refresh();
        $total   = $this->jobChargeCentsResolved($job);
        $paid    = $this->jobPaidCentsResolved($job);
        $balance = max(0, $total - $paid);
        $isPaid  = ($total > 0 && $balance === 0);

        if (view()->exists('portal.pay-complete')) {
            return view('portal.pay-complete', compact('job', 'total', 'paid', 'balance', 'isPaid'));
        }

        return redirect()->route('portal.pay.bundle.job', ['job' => $job->id])
            ->with('claim_ok', $isPaid ? 'Paid in full. Thank you!' : 'Payment received.');
    }

    /* =========================================================================
     | ------------------------------ LEGACY (BOOKINGS) ------------------------
     * ========================================================================= */

    public function showPortalPay(string $token)
    {
        $booking = Booking::query()
            ->where('portal_token', $token)
            ->with(['customer','payments'])
            ->firstOrFail();

        $secrets     = $this->ensurePortalIntents($booking);
        $outstanding = $this->computeBalanceCents($booking);

        $holdCents = (int) ($booking->hold_amount ?? 0);
        if ($holdCents > 0 && !$this->hasActiveHold($booking)) {
            session()->flash('needs_hold', true);
        }

        return view('portal.pay', [
            'booking'             => $booking,
            'user'                => $booking->customer,
            'stripeKey'           => config('services.stripe.key'),
            'outstanding'         => $outstanding,
            'balanceClientSecret' => $secrets['balance'],
            'bondClientSecret'    => $secrets['bond'],
            'needsHold'           => session('needs_hold', false),
        ]);
    }

    public function createOrReuseBalanceIntent(Request $request, string $token)
    {
        $booking = Booking::with(['customer','payments'])->where('portal_token', $token)->firstOrFail();

        $outstanding = $this->computeBalanceCents($booking);
        if ($outstanding <= 0) {
            return response()->json(['ok' => true, 'nothingToPay' => true]);
        }

        $stripe = $this->stripe();
        $pi     = null;

        if (Schema::hasColumn('bookings','stripe_balance_pi_id') && !empty($booking->stripe_balance_pi_id)) {
            try {
                $existing = $stripe->paymentIntents->retrieve($booking->stripe_balance_pi_id);
                if (in_array($existing->status, ['requires_payment_method','requires_confirmation','requires_action'], true)) {
                    $pi = $existing;
                }
            } catch (Throwable $e) {}
        }

        if (!$pi) {
            $pi = $stripe->paymentIntents->create([
                'amount'                    => $outstanding,
                'currency'                  => strtolower($booking->currency ?? 'nzd'),
                'customer'                  => $booking->customer?->stripe_customer_id,
                'automatic_payment_methods' => ['enabled' => true],
                'setup_future_usage'        => 'off_session',
                'metadata'                  => [
                    'purpose'    => 'booking_balance',
                    'booking_id' => $booking->id,
                    'reference'  => (string) $booking->reference,
                ],
            ]);

            if (Schema::hasColumn('bookings','stripe_balance_pi_id')) {
                $booking->forceFill(['stripe_balance_pi_id' => $pi->id])->save();
            }
        }

        return response()->json(['clientSecret' => $pi->client_secret]);
    }

    public function markPaid(Request $request, string $token)
    {
        $booking = Booking::where('portal_token', $token)->firstOrFail();

        if (Schema::hasColumn('bookings','last_payment_at')) {
            $booking->forceFill(['last_payment_at' => now()])->save();
        }

        return response()->json(['ok' => true]);
    }

    /* =========================================================================
     | ------------------------------ PUBLIC JSON ------------------------------
     * ========================================================================= */

    public function createBalancePI(Request $request, Booking $booking)
    {
        $customer = $booking->customer;
        abort_unless($customer, 422, 'Customer not found.');

        [$amount, $currency] = $this->calcBalance($booking);
        abort_if($amount <= 0, 422, 'Nothing to pay.');

        $stripe = $this->stripe();
        $custId = $this->ensureStripeCustomer($customer);

        $payment = $this->firstOrCreatePayment(
            booking:  $booking,
            customer: $customer,
            purpose:  'booking_balance',
            amount:   $amount,
            currency: $currency,
            mechanism:'card'
        );

        $pi = $stripe->paymentIntents->create([
            'amount'                    => $amount,
            'currency'                  => strtolower($currency),
            'customer'                  => $custId,
            'automatic_payment_methods' => ['enabled' => true],
            'setup_future_usage'        => 'off_session',
            'description'               => "Booking balance {$booking->reference}",
            'metadata'                  => [
                'booking_id' => (string) $booking->id,
                'payment_id' => (string) $payment->id,
                'purpose'    => 'booking_balance',
            ],
        ]);

        if (Schema::hasColumn('payments','stripe_payment_intent_id')) {
            $payment->stripe_payment_intent_id = $pi->id;
        } elseif (Schema::hasColumn('payments','stripe_pi_id')) {
            $payment->stripe_pi_id = $pi->id;
        }
        $payment->save();

        return response()->json(['client_secret' => $pi->client_secret]);
    }
    
    
    /* =========================================================================
 | ------------------------------ CORE HELPERS -----------------------------
 * ========================================================================= */

protected function isBondPayment(Payment $payment): bool
{
    $p = strtolower((string) ($payment->purpose ?? ''));
    $t = strtolower((string) ($payment->type ?? ''));
    $m = strtolower((string) ($payment->mechanism ?? ''));

    return in_array($p, ['bond_hold','hold'], true)
        || in_array($t, ['hold'], true)
        || in_array($m, ['hold'], true);
}

protected function assignPurposeAndType(Payment $payment, string $purpose, ?string $mechanism = null): void
{
    $normalized = $purpose === 'hold' ? 'bond_hold' : $purpose;
    if (Schema::hasColumn('payments','purpose'))   $payment->purpose   = $normalized;
    if ($mechanism && Schema::hasColumn('payments','mechanism')) $payment->mechanism = $mechanism;
}

protected function firstOrCreatePayment(
    Booking $booking,
    ?Customer $customer,
    string $purpose,
    int $amount,
    string $currency,
    ?string $mechanism = 'card'
): Payment {
    $normalizedPurpose = $purpose === 'hold' ? 'bond_hold' : $purpose;

    $attrs = ['booking_id' => $booking->id];
    if (Schema::hasColumn('payments','purpose')) $attrs['purpose'] = $normalizedPurpose;

    $defaults = [
        'customer_id' => $customer?->id,
        'amount'      => $amount,
        'currency'    => strtoupper($currency),
        'status'      => 'pending',
    ];
    if (Schema::hasColumn('payments','mechanism')) $defaults['mechanism'] = $mechanism;

    $payment = Payment::firstOrCreate($attrs, $defaults);

    if ($mechanism && Schema::hasColumn('payments','mechanism') && $payment->mechanism !== $mechanism) {
        $payment->mechanism = $mechanism;
    }
    $this->setPaymentAmount($payment, $amount);
    if (Schema::hasColumn('payments','currency')) $payment->currency = strtoupper($currency);
    if (Schema::hasColumn('payments','purpose') && $payment->purpose !== $normalizedPurpose) {
        $payment->purpose = $normalizedPurpose;
    }
    $payment->save();

    return $payment;
}

protected function upsertPI(
    Payment $payment,
    int $amount,
    string $currency,
    string $customerId,
    string $description,
    array $metadata,
    string $idempotencyKey,
    bool $manualCapture = false
) {
    $stripe   = $this->stripe();
    $currency = strtolower($currency);

    if (!empty($payment->stripe_payment_intent_id)) {
        $existing = $stripe->paymentIntents->retrieve($payment->stripe_payment_intent_id);

        $update = [
            'amount'             => $amount,
            'currency'           => $currency,
            'customer'           => $customerId,
            'description'        => $description,
            'metadata'           => $metadata,
            'setup_future_usage' => 'off_session',
        ];

        if ($manualCapture) {
            $update['payment_method_types'] = ['card'];
            $update['capture_method']       = 'manual';
            unset($update['automatic_payment_methods']);
        } else {
            $update['automatic_payment_methods'] = ['enabled' => true];
            unset($update['payment_method_types'], $update['capture_method']);
        }

        return $stripe->paymentIntents->update($existing->id, $update);
    }

    $create = [
        'amount'             => $amount,
        'currency'           => $currency,
        'customer'           => $customerId,
        'description'        => $description,
        'metadata'           => $metadata,
        'setup_future_usage' => 'off_session',
    ];

    if ($manualCapture) {
        $create['payment_method_types'] = ['card'];
        $create['capture_method']       = 'manual';
    } else {
        $create['automatic_payment_methods'] = ['enabled' => true];
    }

    $pi = $stripe->paymentIntents->create($create, ['idempotency_key' => $idempotencyKey]);

    if (Schema::hasColumn('payments','stripe_payment_intent_id')) {
        $payment->stripe_payment_intent_id = $pi->id;
    }
    if (Schema::hasColumn('payments','status')) {
        $payment->status = 'pending';
    }
    $payment->save();

    return $pi;
}

protected function syncPaymentFromPI(Payment $payment, $pi, bool $savePm = false): void
{
    $status = (string) $pi->status;
    $isBond = $this->isBondPayment($payment);

    $mapped = match ($status) {
        'requires_capture'        => $isBond ? 'authorized' : 'requires_capture',
        'succeeded'               => $isBond ? 'captured'   : 'succeeded',
        'requires_payment_method' => 'requires_payment_method',
        'requires_confirmation'   => 'requires_confirmation',
        'requires_action'         => 'requires_action',
        'processing'              => 'processing',
        'canceled'                => 'canceled',
        default                   => $status,
    };

    if (Schema::hasColumn('payments','status'))   $payment->status   = $mapped;
    if (Schema::hasColumn('payments','currency')) $payment->currency = strtoupper($pi->currency ?? $payment->currency);

    $cents = (int) ($pi->amount_received ?? $pi->amount ?? 0);
    if ($cents > 0) $this->setPaymentAmount($payment, $cents);

    if ($savePm && Schema::hasColumn('payments','stripe_payment_method_id')) {
        if (!empty($pi->payment_method)) {
            $payment->stripe_payment_method_id = $pi->payment_method;
        } elseif (!empty($pi->latest_charge?->payment_method)) {
            $payment->stripe_payment_method_id = $pi->latest_charge->payment_method;
        }
    }

    $payment->save();
}


/* =========================================================================
 | ----------- MISSING HELPERS USED BY DEPOSIT/BALANCE/PORTAL --------------
 * ========================================================================= */

protected function calcBalance(Booking $booking): array
{
    $currency = strtoupper($booking->currency ?? 'NZD');
    $balance  = $this->computeBalanceCents($booking);
    return [$balance, $currency];
}

protected function ensureStripeCustomer(Customer $customer): string
{
    $existing = trim((string) ($customer->stripe_customer_id ?? $customer->stripe_id ?? ''));
    if ($existing !== '') return $existing;

    $sc = $this->stripe()->customers->create([
        'email'    => $customer->email,
        'name'     => trim(($customer->first_name ?? '') . ' ' . ($customer->last_name ?? ''))
                      ?: ($customer->company ?? $customer->name ?? null),
        'metadata' => ['customer_id' => (string) $customer->id],
    ]);

    $data = [];
    if (Schema::hasColumn($customer->getTable(), 'stripe_customer_id')) $data['stripe_customer_id'] = $sc->id;
    if (Schema::hasColumn($customer->getTable(), 'stripe_id'))         $data['stripe_id']         = $sc->id;
    if ($data) $customer->forceFill($data)->save();

    return $sc->id;
}

protected function maybeSetDefaultPm(object $pi): void
{
    $pmId = null;
    if (!empty($pi->payment_method)) {
        $pmId = $pi->payment_method;
    } elseif (!empty($pi->latest_charge?->payment_method)) {
        $pmId = $pi->latest_charge->payment_method;
    }

    if (!empty($pi->customer) && $pmId) {
        try {
            $this->stripe()->customers->update($pi->customer, [
                'invoice_settings' => ['default_payment_method' => $pmId],
            ]);
        } catch (Throwable $e) {
            Log::warning('Stripe: could not set default PM', ['err' => $e->getMessage()]);
        }
    }
}

protected function portalCustomer(): Customer
{
    if (Auth::guard('customer')->check()) {
        /** @var Customer $c */
        $c = Auth::guard('customer')->user();
        return $c;
    }
    $id = (int) request()->session()->get('portal_customer_id', 0);
    if ($id > 0) {
        /** @var Customer $c */
        $c = Customer::findOrFail($id);
        return $c;
    }
    abort(401, 'Please log in to continue.');
}

protected function pickPaymentMethodId(string $stripeCustomerId, Booking $booking): ?string
{
    $pmId = null;
    try {
        $cust = $this->stripe()->customers->retrieve($stripeCustomerId);
        $pmId = $cust->invoice_settings->default_payment_method ?? null;
    } catch (Throwable $e) { /* ignore */ }

    if (!$pmId) {
        $pmId = optional(
            $booking->payments()
                ->whereNotNull('stripe_payment_method_id')
                ->whereIn('status', ['succeeded','paid','captured','completed'])
                ->latest('id')
                ->first()
        )->stripe_payment_method_id;
    }

    return $pmId ?: null;
}

protected function ensurePortalToken(Booking $booking): string
{
    $tok = trim((string) ($booking->portal_token ?? ''));
    if ($tok !== '') return $tok;

    $tok = Str::random(40);
    $booking->forceFill(['portal_token' => $tok])->save();
    return $tok;
}



    /* =========================================================================
     | ------------------------------ TOKEN HELPERS ----------------------------
     * ========================================================================= */

    public function paymentLink(Booking $booking)
    {
        $token = $this->ensurePortalToken($booking);
        return response()->json([
            'url' => route('portal.pay.token', ['token' => $token]),
        ]);
    }

    protected function ensurePortalIntents(Booking $booking): array
{
    $stripe   = $this->stripe();
    $currency = strtolower($booking->currency ?? 'nzd');

    // Ensure we have a Stripe customer
    $customer = $booking->customer ?: null;
    if ($customer) {
        $stripeCustomerId = $this->ensureStripeCustomer($customer);
    } else {
        $stripeCustomerId = null;
    }

    $secrets = ['balance' => null, 'bond' => null];

    /* --------------------------- Balance PI (charge) --------------------------- */
    $outstanding = $this->computeBalanceCents($booking);
    if ($outstanding > 0) {
        $balancePiIdCol = Schema::hasColumn('bookings', 'stripe_balance_pi_id') ? 'stripe_balance_pi_id' : null;
        $balancePi = null;

        if ($balancePiIdCol && !empty($booking->{$balancePiIdCol})) {
            try {
                $existing = $stripe->paymentIntents->retrieve($booking->{$balancePiIdCol});
                if (in_array($existing->status, ['requires_payment_method','requires_confirmation','requires_action'], true)) {
                    // Update amount/currency if they changed
                    if ((int) $existing->amount !== (int) $outstanding || strtolower($existing->currency) !== $currency) {
                        $existing = $stripe->paymentIntents->update($existing->id, [
                            'amount'                    => $outstanding,
                            'currency'                  => $currency,
                            'customer'                  => $stripeCustomerId,
                            'automatic_payment_methods' => ['enabled' => true],
                            'setup_future_usage'        => 'off_session',
                        ]);
                    }
                    $balancePi = $existing;
                }
            } catch (\Throwable $e) { /* ignore and create new */ }
        }

        if (!$balancePi) {
            $balancePi = $stripe->paymentIntents->create([
                'amount'                    => $outstanding,
                'currency'                  => $currency,
                'customer'                  => $stripeCustomerId,
                'automatic_payment_methods' => ['enabled' => true],
                'setup_future_usage'        => 'off_session',
                'description'               => 'Booking balance ' . ($booking->reference ?? $booking->id),
                'metadata'                  => [
                    'booking_id' => (string) $booking->id,
                    'purpose'    => 'booking_balance',
                    'reference'  => (string) ($booking->reference ?? ''),
                ],
            ]);
            if ($balancePiIdCol) {
                $booking->forceFill([$balancePiIdCol => $balancePi->id])->save();
            }
        }

        $secrets['balance'] = $balancePi->client_secret ?? null;
    }

    /* ----------------------------- Bond PI (hold) ----------------------------- */
    $holdCents = (int) ($booking->hold_amount ?? 0);
    $bondCol   = Schema::hasColumn('bookings', 'stripe_bond_pi_id') ? 'stripe_bond_pi_id' : null;
    $bondAlreadyFinalized = !empty($booking->bond_released_at) || !empty($booking->bond_captured_at);

    if ($holdCents > 0 && !$bondAlreadyFinalized) {
        $bondPi = null;

        if ($bondCol && !empty($booking->{$bondCol})) {
            try {
                $existing = $stripe->paymentIntents->retrieve($booking->{$bondCol});
                if ($existing && $existing->status !== 'canceled') {
                    if ((int) $existing->amount !== (int) $holdCents || strtolower($existing->currency) !== $currency) {
                        $bondPi = $stripe->paymentIntents->update($existing->id, [
                            'amount'               => $holdCents,
                            'currency'             => $currency,
                            'payment_method_types' => ['card'],
                            'capture_method'       => 'manual',
                            'setup_future_usage'   => 'off_session',
                        ]);
                    } else {
                        $bondPi = $existing;
                    }
                }
            } catch (\Throwable $e) { /* ignore and create new */ }
        }

        if (!$bondPi) {
            $bondPi = $stripe->paymentIntents->create([
                'amount'               => $holdCents,
                'currency'             => $currency,
                'customer'             => $stripeCustomerId,
                'payment_method_types' => ['card'],     // required for manual capture
                'capture_method'       => 'manual',     // authorise now, capture later
                'setup_future_usage'   => 'off_session',
                'description'          => 'Bond hold: ' . ($booking->reference ?? $booking->id),
                'metadata'             => [
                    'booking_id' => (string) $booking->id,
                    'purpose'    => 'bond_hold',
                ],
            ]);
            if ($bondCol) {
                $booking->forceFill([$bondCol => $bondPi->id])->save();
            }
        }

        // Fresh client_secret
        $bondPi = $stripe->paymentIntents->retrieve($bondPi->id);
        $secrets['bond'] = $bondPi->client_secret ?? null;
    }

    return $secrets;
}


    private function computeBalanceCents(Booking $booking): int
{
    if (isset($booking->balance_due) && $booking->balance_due !== null) {
        return max(0, (int) $booking->balance_due);
    }

    $total = (int) ($booking->total_amount ?? 0);

    $paid = (int) $booking->payments()
        ->whereIn('status', ['succeeded','paid','captured','completed'])
        ->where(function ($q) {
            $q->whereNull('purpose')
              ->orWhereNotIn('purpose', ['hold','bond_hold']);
        })
        ->where(function ($q) {
            $q->whereNull('type')
              ->orWhere('type','!=','hold');
        })
        ->sum('amount');

    return max(0, $total - $paid);
}


    private function hasActiveHold(Booking $booking): bool
{
    if (!empty($booking->bond_captured_at) || !empty($booking->bond_released_at)) {
        return false;
    }

    $q = $booking->payments()
        ->where(function ($q) {
            $q->whereIn('purpose', ['bond_hold','hold'])
              ->orWhere('type', 'hold')
              ->orWhere('mechanism', 'hold');
        })
        ->whereIn('status', ['authorized','requires_capture','processing','succeeded']) // treat authorized as active
        ->whereNotNull('stripe_payment_intent_id');

    return $q->exists();
}



    /* =========================================================================
     | ------------------------------ PORTAL (BOOKINGS) ------------------------
     * ========================================================================= */

    public function portalCreateHoldIntent(Request $request, Booking $booking)
{
    $amount = (int) ($booking->hold_amount ?? 0);
    if ($amount <= 0) {
        throw ValidationException::withMessages(['hold' => 'No hold amount configured for this booking.']);
    }

    $currency = strtoupper($booking->currency ?? 'NZD');
    $stripe   = $this->stripe();

    // Ensure we have a Stripe customer
    $customer = $booking->customer ?: $this->portalCustomer();
    $custId   = $this->ensureStripeCustomer($customer);

    // Canonical Payment record for the bond hold
    $payment = $this->firstOrCreatePayment(
        booking:   $booking,
        customer:  $booking->customer,
        purpose:   'bond_hold',   // canonical
        amount:    $amount,
        currency:  $currency,
        mechanism: 'hold'         // mechanism marks this as a hold in your model
    );

    try {
        // Build/refresh a manual-capture PaymentIntent (no automatic_payment_methods here)
        $pi = $this->upsertPI(
            payment:         $payment,
            amount:          $amount,
            currency:        $currency,
            customerId:      $custId,
            description:     "Security hold {$booking->reference}",
            metadata: [
                'booking_id'  => (string) $booking->id,
                'booking_ref' => (string) ($booking->reference ?? ''),
                'payment_id'  => (string) $payment->id,
                'purpose'     => 'bond_hold',
            ],
            idempotencyKey:  "portal_hold_{$booking->id}_{$amount}",
            manualCapture:   true
        );

        // Map Stripe status → your Payment status; persist PM id if available
        $this->syncPaymentFromPI($payment, $pi, savePm: true);

        return response()->json([
            'ok'            => true,
            'client_secret' => $pi->client_secret,   // <-- front-end reads this
            'intent_id'     => $pi->id,
            'payment_id'    => $payment->id,
            'amount'        => $amount,
            'currency'      => $currency,
            'status'        => $payment->status,
        ]);
    } catch (Throwable $e) {
        Log::error('[portal.hold-intent] Stripe error', [
            'booking' => $booking->id,
            'err'     => $e->getMessage(),
        ]);
        throw ValidationException::withMessages(['stripe' => 'Unable to set up the security hold. Please try again.']);
    }
}



    public function portalCompleteHold(Request $request, Booking $booking)
{
    $piId = $request->query('payment_intent');
    abort_if(!$piId, 400, 'Missing payment_intent');

    try {
        $pi = $this->stripe()->paymentIntents->retrieve($piId);
    } catch (Throwable $e) {
        return redirect()
            ->route('portal.pay', ['booking' => $booking->id])
            ->with('claim_error', 'Hold could not be confirmed. Please try again.');
    }

    // Acceptable statuses for a hold authorisation
    if (!in_array($pi->status, ['requires_capture','processing','succeeded'], true)) {
        return redirect()
            ->route('portal.pay', ['booking' => $booking->id])
            ->with('claim_error', 'Hold not placed. Status: ' . $pi->status);
    }

    // Upsert a Payment row tied to this hold PI
    $payment = Schema::hasColumn('payments','stripe_payment_intent_id')
        ? Payment::firstOrNew(['stripe_payment_intent_id' => $pi->id])
        : new Payment();

    if (Schema::hasColumn('payments','booking_id'))  $payment->booking_id  = $booking->id;
    if (Schema::hasColumn('payments','customer_id')) $payment->customer_id = $booking->customer?->id;

    // Canonical purpose/type/mechanism
    $this->assignPurposeAndType($payment, 'bond_hold', 'hold');

    $this->syncPaymentFromPI($payment, $pi, savePm: true);

    return redirect()
        ->route('portal.pay', ['booking' => $booking->id])
        ->with('claim_ok', 'Bond hold placed successfully.');
}



    /* =========================================================================
     | ------------------------------ DEPOSITS HELPERS -------------------------
     * ========================================================================= */

    protected function depositColumnMap(): array
    {
        $map = [
            'auth' => null,
            'capt' => null,
            'ref'  => null,
            'amount' => null,
            'stripe_pi' => null,
        ];
        if (!Schema::hasTable('deposits')) return $map;

        // Common auth/amount columns
        $map['auth'] = Schema::hasColumn('deposits','authorized_cents') ? 'authorized_cents'
                   : (Schema::hasColumn('deposits','authorised_cents') ? 'authorised_cents'
                   : (Schema::hasColumn('deposits','amount_cents') ? 'amount_cents'
                   : (Schema::hasColumn('deposits','authorized_amount') ? 'authorized_amount'
                   : (Schema::hasColumn('deposits','authorised_amount') ? 'authorised_amount'
                   : (Schema::hasColumn('deposits','amount') ? 'amount' : null)))));

        $map['capt'] = Schema::hasColumn('deposits','captured_cents') ? 'captured_cents'
                   : (Schema::hasColumn('deposits','captured_amount_cents') ? 'captured_amount_cents'
                   : (Schema::hasColumn('deposits','captured_amount') ? 'captured_amount'
                   : (Schema::hasColumn('deposits','captured') ? 'captured' : null)));

        // Reference columns (PI id)
        $map['ref']  = Schema::hasColumn('deposits','reference') ? 'reference'
                   : (Schema::hasColumn('deposits','reference_number') ? 'reference_number'
                   : (Schema::hasColumn('deposits','psp_reference') ? 'psp_reference'
                   : (Schema::hasColumn('deposits','psp_id') ? 'psp_id' : null)));

        // Optional explicit stripe PI column variants
        $map['stripe_pi'] = Schema::hasColumn('deposits','stripe_payment_intent') ? 'stripe_payment_intent'
                        : (Schema::hasColumn('deposits','stripe_pi_id') ? 'stripe_pi_id' : null);

        return $map;
    }

    protected function upsertDepositFor(Job $job, object $spi, bool $wasCaptured, array $extraMeta = []): ?Deposit
    {
        if (!Schema::hasTable('deposits')) return null;

        $amount   = (int) ($spi->amount ?? 0);
        $currency = strtoupper($spi->currency ?? $this->jobCurrency($job));

        // Find existing by any known reference / stripe_pi columns
        $q = Deposit::query();
        $map = $this->depositColumnMap();
        if ($map['stripe_pi']) {
            $q->where($map['stripe_pi'], $spi->id);
        } elseif ($map['ref']) {
            $q->where($map['ref'], $spi->id);
        }
        $deposit = $q->first() ?: new Deposit();

        if (Schema::hasColumn('deposits','job_id'))       $deposit->job_id = $job->id;
        if (Schema::hasColumn('deposits','booking_id'))   $deposit->booking_id = $job->booking_id ?? $deposit->booking_id ?? null;
        if (Schema::hasColumn('deposits','customer_id'))  $deposit->customer_id = $job->customer_id ?? $deposit->customer_id ?? null;
        if (Schema::hasColumn('deposits','brand_id'))     $deposit->brand_id = $job->brand_id ?? $deposit->brand_id ?? null;

        if ($map['auth']) $deposit->{$map['auth']} = $amount;
        if ($map['capt']) $deposit->{$map['capt']} = $wasCaptured ? $amount : 0;
        if (Schema::hasColumn('deposits','currency')) $deposit->currency = $currency;
        if ($map['ref']) $deposit->{$map['ref']} = $spi->id;
        if ($map['stripe_pi']) $deposit->{$map['stripe_pi']} = $spi->id;
        if (Schema::hasColumn('deposits','status')) $deposit->status = $wasCaptured ? 'captured' : 'authorized';
        if (Schema::hasColumn('deposits','type'))   $deposit->type   = 'deposit';
        if (Schema::hasColumn('deposits','stripe_payment_method')) $deposit->stripe_payment_method = $spi->payment_method ?? null;

        if (Schema::hasColumn('deposits','meta')) {
            $existing = (array) ($deposit->meta ?? []);
            $deposit->meta = array_merge($existing, $extraMeta);
        }

        $deposit->save();
        return $deposit;
    }

    protected function discoverHoldPiId(Request $request, Job $job): ?string
{
    // 1) Accept many aliases
    $aliases = [
        'hold_intent','hold_pi','hold_intent_id','bond_intent','bond_pi','bond_payment_intent',
        'holdPaymentIntent','bondPaymentIntent','security_hold_intent','hold_pi_id'
    ];
    foreach ($aliases as $k) {
        $v = (string) $request->input($k, '');
        if ($v !== '') return $v;
    }

    // 2) Fallback: look up a recent Payment record for this job/booking marked as hold
    if (Schema::hasTable('payments')) {
        $q = Payment::query()
            ->whereIn('status', ['authorized','requires_capture','processing','succeeded'])
            ->where(function ($q) {
                $q->whereIn('purpose',['bond_hold','hold'])
                  ->orWhere('type','hold')
                  ->orWhere('mechanism','hold');
            })
            ->orderByDesc('id');

        if (Schema::hasColumn('payments','job_id')) {
            $q->where('job_id', $job->id);
        } elseif (Schema::hasColumn('payments','booking_id') && !empty($job->booking_id)) {
            $q->where('booking_id', $job->booking_id);
        }

        $hold = $q->first();
        if ($hold) {
            $piCol = Schema::hasColumn('payments','stripe_payment_intent_id') ? 'stripe_payment_intent_id'
                : (Schema::hasColumn('payments','stripe_payment_intent') ? 'stripe_payment_intent'
                : (Schema::hasColumn('payments','stripe_pi_id') ? 'stripe_pi_id' : null));
            if ($piCol && !empty($hold->{$piCol})) return (string) $hold->{$piCol};
        }
    }

    return null;
}


    /* =========================================================================
     | --------------------------- RECEIPT MAIL HELPER -------------------------
     * ========================================================================= */

    protected function sendPaymentReceipt(
        \App\Models\Job $job,
        \App\Models\Payment $payment,
        ?\Stripe\PaymentIntent $pi = null,
        ?string $postedEmail = null
    ): void {
        try {
            $to = $this->validCustomerEmail($job)
                ?: $this->validEmailOrNull($postedEmail)
                ?: $this->validEmailOrNull($pi->receipt_email ?? null)
                ?: $this->validEmailOrNull($pi->latest_charge->billing_details->email ?? null);

            if (! $to) {
                Log::info('sendPaymentReceipt: no recipient email; skipping.', [
                    'job_id'     => $job->id ?? null,
                    'payment_id' => $payment->id ?? null,
                ]);
                return;
            }

            // Save the discovered email back to the job for next time
            if (empty($job->customer_email)) {
                $job->customer_email = $to;
                $job->save();
            }

            // Build the mailable (support both ctor orders)
            $mailable = null;
            try {
                $mailable = new \App\Mail\PaymentReceiptMail($payment, $job);
            } catch (\Throwable $e1) {
                try {
                    $mailable = new \App\Mail\PaymentReceiptMail($job, $payment);
                } catch (\Throwable $e2) {
                    Log::error('PaymentReceiptMail ctor failed', [
                        'job_id' => $job->id ?? null,
                        'payment_id' => $payment->id ?? null,
                        'e1' => $e1->getMessage(),
                        'e2' => $e2->getMessage(),
                    ]);
                    return;
                }
            }

            Log::info('About to send PaymentReceiptMail', [
                'job_id'     => $job->id ?? null,
                'payment_id' => $payment->id ?? null,
                'to'         => $to,
            ]);

            Mail::to($to)->send($mailable);

            Log::info('PaymentReceiptMail sent OK', [
                'job_id'     => $job->id ?? null,
                'payment_id' => $payment->id ?? null,
                'to'         => $to,
            ]);
        } catch (\Throwable $e) {
            Log::error('PaymentReceiptMail FAILED', [
                'job_id'     => $job->id ?? null,
                'payment_id' => $payment->id ?? null,
                'error'      => $e->getMessage(),
            ]);
        }
    }

    /* =========================================================================
     | ------------------------------ SMALL HELPERS ----------------------------
     * ========================================================================= */

    private function validCustomerEmail(Job $job): ?string
    {
        $e = trim((string) ($job->customer->email ?? $job->customer_email ?? ''));
        return filter_var($e, FILTER_VALIDATE_EMAIL) ? $e : null;
    }

    private function validEmailOrNull(?string $e): ?string
    {
        $e = trim((string) $e);
        return filter_var($e, FILTER_VALIDATE_EMAIL) ? $e : null;
    }

    private function paymentAmountColumn(): ?string
    {
        return Schema::hasColumn('payments','amount_cents') ? 'amount_cents'
             : (Schema::hasColumn('payments','amount') ? 'amount' : null);
    }

    private function getPaymentAmount(Payment $payment): ?int
    {
        $col = $this->paymentAmountColumn();
        return $col ? (int) $payment->{$col} : null;
    }

    private function setPaymentAmount(Payment $payment, int $cents): void
    {
        $col = $this->paymentAmountColumn();
        if ($col) {
            $payment->{$col} = $cents;
        }
    }
}
