<?php

namespace App\Http\Controllers;

use App\Models\Job;
use App\Models\Payment;
use App\Models\Communication;
use App\Models\CommunicationEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Stripe\Webhook as StripeWebhook;

class WebhookController extends Controller
{
    /* =========================================================================
     | STRIPE
     * ========================================================================= */

    /** Stripe endpoint */
    public function handleStripe(Request $request)
    {
        $secret = (string) config('services.stripe.webhook_secret');
        if ($secret === '') {
            // If you prefer, abort(500) here; logging keeps prod resilient.
            Log::warning('Stripe webhook secret missing. Skipping signature verification.');
        }

        // --- Verify signature (when secret present) ---
        try {
            $payload = $request->getContent();
            $sig     = $request->header('Stripe-Signature');

            if ($secret) {
                $event = StripeWebhook::constructEvent($payload, $sig, $secret);
            } else {
                // Fallback for local/dev without signing
                $event = json_decode($payload, false, 512, JSON_THROW_ON_ERROR);
            }
        } catch (\Throwable $e) {
            Log::warning('Stripe webhook signature verification failed', ['err' => $e->getMessage()]);
            return response()->json(['ok' => false], 400);
        }

        $type = $event->type ?? null;
        $obj  = $event->data->object ?? null;

        try {
            return $this->handleStripeEvent($type, $obj);
        } catch (\Throwable $e) {
            Log::error('Stripe webhook handler error', ['type' => $type, 'err' => $e->getMessage()]);
            return response()->json(['ok' => false], 500);
        }
    }

    protected function handleStripeEvent(?string $type, $object)
    {
        switch ($type) {
            // ---- Main success path for card payments (including APM that map to PIs) ----
            case 'payment_intent.succeeded':
                $this->upsertPaymentFromIntent($object, 'succeeded');
                break;

            // ---- Manual capture auth/hold reached `requires_capture` ----
            case 'payment_intent.amount_capturable_updated':
                $this->upsertPaymentFromIntent($object, 'authorized');
                break;

            // ---- After capture, Stripe emits charge.succeeded with captured=true ----
            case 'charge.succeeded':
                $this->markChargeCaptured($object);
                break;

            // ---- Refunds / partial refunds ----
            case 'charge.refunded':
                $this->markRefunded($object);
                break;

            // ---- Failure bookkeeping (optional) ----
            case 'payment_intent.payment_failed':
                $this->upsertPaymentFromIntent($object, 'failed');
                break;

            default:
                // ignore noisy events
                break;
        }

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

    /**
     * Create or update our Payment row from a PaymentIntent.
     * Also updates the Job’s paid_amount_cents cache if the column exists.
     */
    protected function upsertPaymentFromIntent($pi, string $status)
    {
        // Determine amounts & job id
        $amount   = (int) ($pi->amount_received ?? $pi->amount ?? 0);
        $currency = strtolower((string) ($pi->currency ?? 'nzd'));
        $jobId    = (int) ($pi->metadata->job_id ?? 0);

        // Determine type by capture method
        $type = ($pi->capture_method ?? null) === 'manual'
            ? ($status === 'authorized' ? 'hold' : 'balance')
            : 'balance';

        // Build base attributes
        $attrs = [
            'job_id'       => $jobId ?: null,
            'amount_cents' => $amount,
            'currency'     => $currency,
            'status'       => $status, // 'succeeded' | 'authorized' | 'failed'
            'type'         => $type,   // 'balance' | 'hold'
            'provider'     => 'stripe',
            'provider_id'  => (string) $pi->id, // we key by PaymentIntent ID in our app
            'reference'    => optional($pi->charges->data[0] ?? null)->id,
            'notes'        => 'Recorded via webhook '.$status,
        ];

        $this->persistPayment($attrs);

        if ($jobId) {
            $this->refreshJobPaidTotals($jobId);
        }
    }

    /**
     * Mark a charge as captured/succeeded. Useful for manual-capture holds.
     * We link back to the PI payment row and flip status to 'captured'.
     */
    protected function markChargeCaptured($charge)
    {
        // Only adjust if this is a capture (either direct capture or auth+capture)
        $captured = (bool) ($charge->captured ?? false);
        if (!$captured) return;

        $piId = (string) ($charge->payment_intent ?? '');
        if ($piId === '') return;

        /** @var \App\Models\Payment|null $payment */
        $payment = Payment::where('provider', 'stripe')->where('provider_id', $piId)->first();
        if (!$payment) {
            // create if missing (idempotent)
            $attrs = [
                'job_id'       => $this->jobIdFromCharge($charge),
                'amount_cents' => (int) ($charge->amount_captured ?? $charge->amount ?? 0),
                'currency'     => strtolower((string) $charge->currency),
                'status'       => 'captured',
                'type'         => 'hold',
                'provider'     => 'stripe',
                'provider_id'  => $piId,
                'reference'    => (string) $charge->id,
                'notes'        => 'Recorded via webhook charge.succeeded',
            ];
            $payment = $this->persistPayment($attrs);
        } else {
            $payment->status       = 'captured';
            $payment->amount_cents = (int) ($charge->amount_captured ?? $charge->amount ?? $payment->amount_cents);
            $payment->reference    = (string) $charge->id;
            $payment->save();
        }

        if ($payment && $payment->job_id) {
            $this->refreshJobPaidTotals($payment->job_id);
        }
    }

    protected function markRefunded($charge)
    {
        $piId = (string) ($charge->payment_intent ?? '');
        if ($piId === '') return;

        $payment = Payment::where('provider', 'stripe')->where('provider_id', $piId)->first();
        if ($payment) {
            $payment->status = 'refunded';
            $payment->save();

            if ($payment->job_id) {
                $this->refreshJobPaidTotals($payment->job_id);
            }
        }
    }

    protected function persistPayment(array $candidate): Payment
    {
        // Keep only columns that exist in your DB schema (defensive)
        $cols = Schema::getColumnListing('payments');
        $data = Arr::only($candidate, $cols);

        $lookup = Schema::hasColumn('payments', 'provider')
            ? ['provider' => 'stripe', 'provider_id' => $candidate['provider_id']]
            : ['provider_id' => $candidate['provider_id']];

        return Payment::updateOrCreate($lookup, $data);
    }

    protected function refreshJobPaidTotals(int $jobId): void
    {
        /** @var Job|null $job */
        $job = Job::find($jobId);
        if (!$job) return;

        // Sum successful money that actually reduces balance
        $sum = $job->payments()
            ->whereIn('status', ['succeeded', 'captured'])
            ->sum('amount_cents');

        if (Schema::hasColumn('jobs', 'paid_amount_cents')) {
            $job->paid_amount_cents = (int) $sum;
            $job->save();
        }
    }

    protected function jobIdFromCharge($charge): ?int
    {
        // try metadata on charge, else on PI (best-effort)
        if (!empty($charge->metadata->job_id)) {
            return (int) $charge->metadata->job_id;
        }
        return null;
    }

    /* =========================================================================
     | SENDGRID
     * ========================================================================= */

    /**
     * SendGrid Event Webhook endpoint
     * Docs: https://docs.sendgrid.com/for-developers/tracking-events/event
     *
     * Tip: You can enable signed webhooks and verify using:
     *   X-Twilio-Email-Event-Webhook-Signature / Timestamp / Public-Key
     * (left optional here; add verification if you enable it)
     */
    public function handleSendgrid(Request $request)
    {
        try {
            // Events come as an array; robustly handle single-object fallback:
            $events = json_decode($request->getContent(), true) ?: [];
            if (Arr::isAssoc($events)) {
                $events = [$events];
            }

            foreach ($events as $e) {
                // Prefer unique_args.comm_id (when set via X-SMTPAPI / dynamic template data)
                $commId = Arr::get($e, 'unique_args.comm_id')
                    ?? $this->extractCommIdFromMessageId(Arr::get($e, 'smtp-id'));

                if (!$commId) {
                    continue;
                }

                // Create event row
                CommunicationEvent::create([
                    'communication_id' => (int) $commId,
                    'type'             => $this->mapSendgridEvent((string) Arr::get($e, 'event')),
                    'payload'          => $e,
                    'occurred_at'      => now(), // Alternatively: Arr::get($e, 'timestamp')
                ]);

                // Optionally update the parent Communication.status
                $this->maybeUpdateCommunicationStatus((int) $commId, (string) Arr::get($e, 'event'));
            }

            return response()->json(['ok' => true]);
        } catch (\Throwable $e) {
            Log::error('SendGrid webhook handler error', ['err' => $e->getMessage()]);
            return response()->json(['ok' => false], 500);
        }
    }

    /**
     * Try to pull a communication id from the smtp-id.
     * Example patterns you can emit when sending:
     *   "<comm-123@yourdomain>" or "<123@...>" etc.
     */
    protected function extractCommIdFromMessageId(?string $smtpId): ?int
    {
        if (!$smtpId) return null;

        // strip angle brackets if present
        $raw = trim($smtpId, "<> \t\n\r\0\x0B");

        // Look for "comm-123" first
        if (preg_match('/comm-(\d+)/i', $raw, $m)) {
            return (int) $m[1];
        }

        // fallback: any bare integer token
        if (preg_match('/\b(\d{1,10})\b/', $raw, $m)) {
            return (int) $m[1];
        }

        return null;
    }

    /**
     * Normalize SendGrid event names to your internal enum/strings.
     */
    protected function mapSendgridEvent(string $event): string
    {
        $event = strtolower($event);
        return match ($event) {
            'processed'          => 'processed',
            'deferred'           => 'deferred',
            'delivered'          => 'delivered',
            'open'               => 'opened',
            'click'              => 'clicked',
            'bounce'             => 'bounced',
            'dropped'            => 'dropped',
            'spamreport'         => 'spam_report',
            'unsubscribe'        => 'unsubscribed',
            'group_unsubscribe'  => 'group_unsubscribed',
            'group_resubscribe'  => 'group_resubscribed',
            default              => $event, // keep unknowns visible
        };
    }

    /**
     * Optionally update Communication.status based on notable events.
     */
    protected function maybeUpdateCommunicationStatus(int $commId, string $sendgridEvent): void
    {
        /** @var Communication|null $comm */
        $comm = Communication::find($commId);
        if (!$comm) return;

        $e = strtolower($sendgridEvent);
        $new = match ($e) {
            'delivered'  => 'delivered',
            'bounce'     => 'bounced',
            'dropped'    => 'dropped',
            default      => null,
        };

        if ($new && Schema::hasColumn($comm->getTable(), 'status')) {
            $comm->status = $new;
            $comm->save();
        }
    }
}
