<?php

namespace App\Services\Billing;

use App\Libraries\PaymentGateways\BankTransferGateway;
use App\Libraries\PaymentGateways\EasypaisaGateway;
use App\Libraries\PaymentGateways\JazzCashGateway;
use App\Libraries\PaymentGateways\PayPalGateway;
use App\Libraries\PaymentGateways\PaymentGatewayInterface;
use App\Libraries\PaymentGateways\StripeGateway;
use App\Models\PaymentModel;
use App\Services\Support\ActivityLogger;

/**
 * The seam described in 05-saas-architecture.md §7: this class never
 * branches on gateway type beyond the factory method — all charge/refund/
 * webhook logic is uniform across gateways because they share
 * PaymentGatewayInterface.
 */
class PaymentService
{
    private PaymentModel $payments;
    private CreditService $credits;
    private WalletService $wallet;
    private SubscriptionService $subscriptions;
    private InvoiceService $invoices;
    private \App\Services\Notification\NotificationService $notifications;
    private ActivityLogger $activity;

    public function __construct()
    {
        $this->payments      = new PaymentModel();
        $this->credits       = new CreditService();
        $this->wallet        = new WalletService();
        $this->subscriptions = new SubscriptionService();
        $this->invoices      = new InvoiceService();
        $this->notifications = new \App\Services\Notification\NotificationService();
        $this->activity      = new ActivityLogger();
    }

    public function gatewayFor(string $name): PaymentGatewayInterface
    {
        return match ($name) {
            'stripe'        => new StripeGateway(),
            'paypal'        => new PayPalGateway(),
            'jazzcash'      => new JazzCashGateway(),
            'easypaisa'     => new EasypaisaGateway(),
            'bank_transfer' => new BankTransferGateway(),
            default         => throw new \InvalidArgumentException("Unknown payment gateway: {$name}"),
        };
    }

    public function initiateCharge(
        int $agencyId,
        string $gatewayName,
        float $amount,
        string $currency,
        string $purpose,
        array $metadata = [],
        ?int $invoiceId = null
    ): array {
        $gateway = $this->gatewayFor($gatewayName);
        $result = $gateway->charge($amount, $currency, $metadata);

        $paymentId = $this->payments->insert([
            'agency_id'              => $agencyId,
            'invoice_id'             => $invoiceId,
            'gateway'                => $gatewayName,
            'gateway_transaction_id' => $result['gateway_reference'],
            'purpose'                => $purpose,
            'amount'                 => $amount,
            'currency'               => $currency,
            'status'                 => 'pending',
            'gateway_response'       => json_encode($result),
            'created_at'             => date('Y-m-d H:i:s'),
        ], true);

        return array_merge($result, ['payment_id' => $paymentId]);
    }

    /**
     * Called by WebhookController. Verifies signature, dedupes by
     * gateway_transaction_id (idempotent — a retried webhook delivery
     * can't double-credit), then dispatches the purpose-specific effect.
     */
    public function handleWebhook(string $gatewayName, string $rawPayload, array $headers): array
    {
        $gateway = $this->gatewayFor($gatewayName);
        $result = $gateway->verifyWebhook($rawPayload, $headers);

        if (! $result['verified']) {
            return ['handled' => false, 'reason' => 'Signature verification failed.'];
        }

        $transactionId = $result['gateway_transaction_id'];
        if (! $transactionId) {
            return ['handled' => false, 'reason' => 'No transaction ID in webhook payload.'];
        }

        $payment = $this->payments->findByGatewayTransactionId($gatewayName, $transactionId);
        if (! $payment) {
            return ['handled' => false, 'reason' => 'No matching payment record found.'];
        }

        if ($payment['status'] === 'success') {
            return ['handled' => true, 'reason' => 'Already processed (idempotent skip).'];
        }

        $newStatus = $result['status'] === 'succeeded' ? 'success' : 'failed';
        $this->payments->update($payment['id'], [
            'status'           => $newStatus,
            'gateway_response' => json_encode($result['raw']),
        ]);

        if ($newStatus === 'success') {
            $this->applySuccessEffect($payment);
        }

        $this->activity->log(null, (int) $payment['agency_id'], null, 'payment', [
            'gateway' => $gatewayName, 'status' => $newStatus, 'purpose' => $payment['purpose'],
        ]);

        if ($newStatus === 'success') {
            $owner = \Config\Database::connect()->table('agencies')->where('id', $payment['agency_id'])->get()->getRow();
            if ($owner && $owner->owner_user_id) {
                $this->notifications->send((int) $owner->owner_user_id, 'payment_success', [
                    'amount' => $payment['amount'], 'currency' => $payment['currency'],
                ]);
            }
        }

        return ['handled' => true, 'status' => $newStatus];
    }

    /**
     * Super Admin / Support manual verification path — needed for Bank
     * Transfer (which has no automated webhook callback at all) and as a
     * fallback for any gateway. Marks the payment successful and runs the
     * exact same success effect as an automated webhook would (activates
     * subscription / credits wallet / marks invoice paid), so there's only
     * one code path for "what happens when a payment succeeds" regardless
     * of how that success was confirmed.
     */
    public function manuallyVerifyPayment(int $paymentId, int $verifiedByUserId): array
    {
        $payment = $this->payments->find($paymentId);
        if (! $payment) {
            throw new \RuntimeException('Payment not found.');
        }
        if ($payment['status'] === 'success') {
            return ['already_verified' => true];
        }

        $this->payments->update($paymentId, ['status' => 'success']);
        $payment['status'] = 'success';
        $this->applySuccessEffect($payment);

        $this->activity->log($verifiedByUserId, (int) $payment['agency_id'], 'super_admin', 'payment_manual_verify', ['payment_id' => $paymentId]);

        return ['already_verified' => false, 'payment' => $this->payments->find($paymentId)];
    }

    public function listAll(?string $status = null, ?string $gateway = null): array
    {
        $builder = $this->payments->builder();
        if ($status) {
            $builder->where('status', $status);
        }
        if ($gateway) {
            $builder->where('gateway', $gateway);
        }
        return $builder->orderBy('created_at', 'DESC')->get()->getResultArray();
    }

    private function applySuccessEffect(array $payment): void
    {
        $agencyId = (int) $payment['agency_id'];

        if ($payment['invoice_id']) {
            $this->invoices->markPaid((int) $payment['invoice_id']);
        }

        switch ($payment['purpose']) {
            case 'wallet_topup':
                $this->wallet->deposit($agencyId, (float) $payment['amount'], 'payments', $payment['id'], 'Wallet top-up');
                break;

            case 'credit_purchase':
                // amount-to-credits conversion rate lives in settings;
                // 1 credit = configurable PKR amount, defaulting to 100.
                $rate = 100;
                $creditsToAdd = (int) floor(((float) $payment['amount']) / $rate);
                $this->credits->credit($agencyId, $creditsToAdd, 'purchase', 'payments', $payment['id']);
                break;

            case 'subscription':
                // Plan was already provisioned at checkout initiation in
                // most flows; for redirect-flow gateways (JazzCash/Easypaisa)
                // where confirmation only arrives via webhook, the pending
                // plan change metadata would be read here. Left as a
                // straightforward extension point — subscription plan
                // selection is passed through $metadata at initiateCharge().
                break;
        }
    }
}
