<?php

namespace Licenzo\Services\Integrations;

use Licenzo\Framework\Http\Request;
use Licenzo\Helper\Settings;
use Licenzo\Model\SubscriptionTransaction;
use Licenzo\Model\WebhookLog as WebhookLogModel;
use Licenzo\Services\IdempotencyService;
use Licenzo\Services\Logging\WebhookLog as WebhookLogLogger;
use Licenzo\Modules\WooCommerce\WooOrderService;
use Licenzo\Modules\WooCommerce\WooStripeCompatibilityService;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

class StripeWebhookService
{
    private const IDEMPOTENCY_SCOPE = 'stripe_webhook_event';

    private const SUCCESS_EVENTS = [
        'payment_intent.succeeded',
        'charge.succeeded',
        'charge.captured',
    ];

    private const FAILURE_EVENTS = [
        'payment_intent.payment_failed',
        'payment_intent.canceled',
        'charge.failed',
    ];

    private const REFUND_EVENTS = [
        'charge.refunded',
        'charge.refund.updated',
    ];

    private const EDD_SUBSCRIPTION_EVENTS = [
        'invoice.paid',
        'invoice.payment_failed',
        'customer.subscription.updated',
        'customer.subscription.deleted',
    ];

    /**
     * @var IdempotencyService
     */
    private $idempotencyService;

    /**
     * @var WooOrderService
     */
    private $wooOrderService;

    /**
     * @var WooStripeCompatibilityService
     */
    private $stripeCompatibility;

    public function __construct(
        ?IdempotencyService $idempotencyService = null,
        ?WooOrderService $wooOrderService = null,
        ?WooStripeCompatibilityService $stripeCompatibility = null
    ) {
        $this->idempotencyService = $idempotencyService ?? new IdempotencyService();
        $this->wooOrderService = $wooOrderService ?? new WooOrderService();
        $this->stripeCompatibility = $stripeCompatibility ?? new WooStripeCompatibilityService();
    }

    /**
     * Process an incoming Stripe webhook request.
     *
     * @return array<string,mixed>
     */
    public function handle(Request $request): array
    {
        $rawBody = (string) $request->rawBody();
        if ($rawBody === '') {
            $payload = $request->all();
            $rawBody = is_array($payload) ? (string) wp_json_encode($payload) : '';
        }

        if ($rawBody === '') {
            return [
                'success' => false,
                'message' => 'Empty webhook body',
                'status' => 400,
            ];
        }

        $headers = is_array($request->header()) ? $request->header() : [];
        $payload = json_decode($rawBody, true);
        if (!is_array($payload)) {
            $log = $this->createWebhookLog($request, 'stripe.invalid_json', '', ['_raw_body' => $rawBody]);
            if ($log) {
                $log->markAsFailed('Invalid JSON payload');
            }

            return [
                'success' => false,
                'message' => 'Invalid JSON payload',
                'status' => 400,
            ];
        }

        $eventType = (string) ($payload['type'] ?? 'stripe.webhook');
        $eventId = (string) ($payload['id'] ?? '');
        $webhookLog = $this->createWebhookLog($request, $eventType, $eventId, $payload);

        if (!$this->verifyStripeSignature($rawBody, $headers)) {
            if ($webhookLog) {
                $webhookLog->markAsFailed('Invalid Stripe signature');
            }

            return [
                'success' => false,
                'message' => 'Invalid Stripe signature',
                'status' => 401,
            ];
        }

        $eventKey = $eventId !== '' ? $eventId : ('stripe_' . md5($rawBody));
        $reserved = $this->idempotencyService->acquire(
            SubscriptionTransaction::PROVIDER_STRIPE,
            self::IDEMPOTENCY_SCOPE,
            $eventKey,
            null,
            null,
            hash('sha256', $rawBody),
            gmdate('Y-m-d H:i:s', strtotime('+30 days'))
        );

        if (!$reserved) {
            if ($webhookLog) {
                $webhookLog->markAsProcessed();
            }

            return [
                'success' => true,
                'message' => 'Duplicate Stripe webhook ignored',
                'duplicate' => true,
                'event_id' => $eventId ?: null,
                'event_type' => $eventType,
            ];
        }

        try {
            $reconcile = $this->reconcileStripeEvent($payload);

            $this->idempotencyService->markProcessed(
                SubscriptionTransaction::PROVIDER_STRIPE,
                self::IDEMPOTENCY_SCOPE,
                $eventKey
            );

            if ($webhookLog) {
                $webhookLog->markAsProcessed();
            }

            return [
                'success' => true,
                'message' => 'Stripe webhook processed',
                'event_id' => $eventId ?: null,
                'event_type' => $eventType,
                'reconciliation' => $reconcile,
            ];
        } catch (\Throwable $e) {
            $this->idempotencyService->markFailed(
                SubscriptionTransaction::PROVIDER_STRIPE,
                self::IDEMPOTENCY_SCOPE,
                $eventKey
            );

            if ($webhookLog) {
                $webhookLog->markAsFailed($e->getMessage());
            }

            return [
                'success' => false,
                'message' => 'Stripe webhook processing failed',
                'status' => 500,
                'error' => defined('WP_DEBUG') && WP_DEBUG ? $e->getMessage() : null,
            ];
        }
    }

    /**
     * Reconcile a decoded Stripe event payload into Woo/Licenzo state.
     * Public to support future webhook reprocessing jobs and runtime smoke tests.
     *
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function reconcileStripeEvent(array $payload): array
    {
        $eventType = (string) ($payload['type'] ?? 'stripe.webhook');
        $eventId = (string) ($payload['id'] ?? '');

        if (in_array($eventType, self::EDD_SUBSCRIPTION_EVENTS, true)) {
            $eddResult = $this->reconcileEddStripeSubscriptionEvent($payload, $eventType, $eventId);
            if ($eddResult !== null) {
                return $eddResult;
            }
        }

        $orderId = $this->extractWooOrderIdFromStripeEvent($payload);

        $result = [
            'event_id' => $eventId !== '' ? $eventId : null,
            'event_type' => $eventType,
            'order_id' => $orderId > 0 ? $orderId : null,
            'handled' => false,
            'actions' => [],
            'reason' => null,
        ];

        if ($orderId <= 0) {
            $result['reason'] = 'order_id_not_found_in_event';
            return $result;
        }

        if (!function_exists('wc_get_order')) {
            $result['reason'] = 'woocommerce_unavailable';
            return $result;
        }

        $order = wc_get_order($orderId);
        if (!$order) {
            $result['reason'] = 'woo_order_not_found';
            return $result;
        }

        $result['order_status'] = method_exists($order, 'get_status') ? (string) $order->get_status() : null;

        if (!$this->isLizenzoRelevantOrder($order)) {
            $result['reason'] = 'order_not_linked_to_licenzo_subscription';
            return $result;
        }

        $currentStatus = (string) $order->get_status();
        $eventClass = $this->classifyEvent($eventType);
        $shouldSyncStatus = in_array($currentStatus, ['processing', 'completed', 'pending', 'on-hold', 'failed', 'cancelled', 'refunded'], true)
            && in_array($eventClass, ['success', 'failure', 'refund'], true);

        if (
            $eventClass === 'success'
            && in_array($currentStatus, ['processing', 'completed'], true)
        ) {
            $this->wooOrderService->process($orderId);
            $result['actions'][] = 'woo_order_process';
            $result['handled'] = true;
        }

        if ($shouldSyncStatus) {
            $this->wooOrderService->syncOrderStatus($orderId, $currentStatus, $currentStatus);
            $result['actions'][] = 'woo_order_status_sync';
            $result['handled'] = true;
        }

        if (!$result['handled']) {
            $result['reason'] = 'no_action_required';
        }

        return $result;
    }

    /**
     * Route Stripe Subscription events to EddStripeSubscriptionService.
     *
     * Returns null if the event is not Licenzo-relevant (e.g. a WooCommerce
     * subscription invoice) so the caller can fall through to WooCommerce handling.
     *
     * @return array<string,mixed>|null
     */
    private function reconcileEddStripeSubscriptionEvent(array $payload, string $eventType, string $eventId): ?array
    {
        $object = $payload['data']['object'] ?? null;
        if (!is_array($object)) {
            return null;
        }

        $isLicenzoEvent = $this->isLicenzoSubscriptionEvent($object, $eventType);
        if (!$isLicenzoEvent) {
            return null;
        }

        $result = [
            'event_id'   => $eventId !== '' ? $eventId : null,
            'event_type' => $eventType,
            'handled'    => false,
            'actions'    => [],
            'reason'     => null,
        ];

        $handler = \Licenzo\Modules\EDD\EddStripeSubscriptionService::class;

        switch ($eventType) {
            case 'invoice.paid':
                $outcome = $handler::handleInvoicePaid($object);
                break;
            case 'invoice.payment_failed':
                $outcome = $handler::handleInvoicePaymentFailed($object);
                break;
            case 'customer.subscription.updated':
                $outcome = $handler::handleSubscriptionUpdated($object);
                break;
            case 'customer.subscription.deleted':
                $outcome = $handler::handleSubscriptionDeleted($object);
                break;
            default:
                return null;
        }

        $result['handled'] = $outcome['handled'] ?? false;
        $result['actions'][] = $outcome['action'] ?? ($outcome['reason'] ?? 'unknown');
        if (isset($outcome['reason'])) {
            $result['reason'] = $outcome['reason'];
        }

        return $result;
    }

    /**
     * Determine if a Stripe event object belongs to a Licenzo-managed subscription.
     */
    private function isLicenzoSubscriptionEvent(array $object, string $eventType): bool
    {
        $metadata = is_array($object['metadata'] ?? null) ? $object['metadata'] : [];
        if (isset($metadata['licenzo_subscription_id'])) {
            return true;
        }

        $stripeSubId = '';
        if (in_array($eventType, ['invoice.paid', 'invoice.payment_failed'], true)) {
            $stripeSubId = (string) ($object['subscription'] ?? '');
        } elseif (in_array($eventType, ['customer.subscription.updated', 'customer.subscription.deleted'], true)) {
            $stripeSubId = (string) ($object['id'] ?? '');
        }

        if ($stripeSubId !== '' && str_starts_with($stripeSubId, 'sub_')) {
            $exists = \Licenzo\Model\Subscription::where('provider_subscription_id', $stripeSubId)->exists();
            if ($exists) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function extractWooOrderIdFromStripeEvent(array $payload): int
    {
        $object = $payload['data']['object'] ?? null;
        if (!is_array($object)) {
            return 0;
        }

        $candidates = [
            $object['metadata']['order_id'] ?? null,
            $object['order_id'] ?? null,
        ];

        // PaymentIntent can include nested charge metadata if top-level metadata is absent.
        if (isset($object['charges']['data']) && is_array($object['charges']['data'])) {
            foreach ($object['charges']['data'] as $charge) {
                if (!is_array($charge)) {
                    continue;
                }

                $candidates[] = $charge['metadata']['order_id'] ?? null;
            }
        }

        foreach ($candidates as $candidate) {
            if (is_numeric($candidate) && (int) $candidate > 0) {
                return (int) $candidate;
            }
        }

        // Fallback: resolve by stored Stripe intent ids on Woo orders.
        $intentId = '';
        $objectType = (string) ($object['object'] ?? '');
        if ($objectType === 'payment_intent') {
            $intentId = (string) ($object['id'] ?? '');
        } elseif ($objectType === 'charge') {
            $intentId = (string) ($object['payment_intent'] ?? '');
        } elseif ($objectType === 'setup_intent') {
            $intentId = (string) ($object['id'] ?? '');
        }

        if ($intentId !== '') {
            $resolved = $this->resolveWooOrderIdByStripeIntentId($intentId, $objectType);
            if ($resolved > 0) {
                return $resolved;
            }
        }

        return 0;
    }

    private function resolveWooOrderIdByStripeIntentId(string $intentId, string $objectType = ''): int
    {
        if ($intentId === '') {
            return 0;
        }

        if (class_exists('WC_Stripe_Helper')) {
            if (
                $objectType === 'setup_intent'
                && method_exists('WC_Stripe_Helper', 'get_order_by_setup_intent_id')
            ) {
                $order = \WC_Stripe_Helper::get_order_by_setup_intent_id($intentId);
                if ($order && method_exists($order, 'get_id')) {
                    return (int) $order->get_id();
                }
            }

            if (method_exists('WC_Stripe_Helper', 'get_order_by_intent_id')) {
                $order = \WC_Stripe_Helper::get_order_by_intent_id($intentId);
                if ($order && method_exists($order, 'get_id')) {
                    return (int) $order->get_id();
                }
            }
        }

        if (!function_exists('wc_get_orders')) {
            return 0;
        }

        $metaKeys = ['_stripe_intent_id'];
        if ($objectType === 'setup_intent') {
            $metaKeys[] = '_stripe_setup_intent';
        }

        foreach ($metaKeys as $metaKey) {
            // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- WooCommerce wc_get_orders API params.
            $orders = wc_get_orders([
                'limit' => 1,
                'return' => 'objects',
                'meta_key' => $metaKey,
                'meta_value' => $intentId,
            ]);
            // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value

            $order = is_array($orders) ? reset($orders) : null;
            if ($order && method_exists($order, 'get_id')) {
                return (int) $order->get_id();
            }
        }

        return 0;
    }

    private function classifyEvent(string $eventType): string
    {
        if (in_array($eventType, self::SUCCESS_EVENTS, true)) {
            return 'success';
        }

        if (in_array($eventType, self::FAILURE_EVENTS, true)) {
            return 'failure';
        }

        if (in_array($eventType, self::REFUND_EVENTS, true)) {
            return 'refund';
        }

        return 'other';
    }

    private function isLizenzoRelevantOrder($order): bool
    {
        if (!$order || !method_exists($order, 'get_id')) {
            return false;
        }

        if (method_exists($order, 'get_meta')) {
            $singleId = (int) $order->get_meta('_licenzo_subscription_id', true);
            if ($singleId > 0) {
                return true;
            }

            $multi = $order->get_meta('_licenzo_subscription_ids', true);
            if (is_array($multi) && !empty($multi)) {
                return true;
            }

            $renewalFlag = $order->get_meta('_licenzo_renewal_order', true);
            if (in_array($renewalFlag, [1, '1', true, 'true', 'yes'], true)) {
                return true;
            }
        }

        return $this->stripeCompatibility->orderHasRecurringLicenzoSubscriptionItems((int) $order->get_id());
    }

    /**
     * @param array<string,mixed> $headers
     */
    private function verifyStripeSignature(string $rawBody, array $headers): bool
    {
        if ($rawBody === '') {
            return false;
        }

        $secret = $this->resolveStripeWebhookSecret();
        if ($secret === '') {
            return false;
        }

        $signatureHeader = $this->extractHeader($headers, [
            'Stripe-Signature',
            'stripe-signature',
            'HTTP_STRIPE_SIGNATURE',
        ]);
        if ($signatureHeader === '') {
            return false;
        }

        $parsed = $this->parseStripeSignatureHeader($signatureHeader);
        $timestamp = (int) ($parsed['timestamp'] ?? 0);
        $signatures = is_array($parsed['v1']) ? $parsed['v1'] : [];

        if ($timestamp <= 0 || empty($signatures)) {
            return false;
        }

        $tolerance = (int) Settings::get('webhook_settings.tolerance_seconds', 300);
        if ($tolerance <= 0) {
            $tolerance = 300;
        }

        if (abs(time() - $timestamp) > $tolerance) {
            return false;
        }

        $signedPayload = $timestamp . '.' . $rawBody;
        $expected = hash_hmac('sha256', $signedPayload, $secret);

        foreach ($signatures as $signature) {
            if (!is_string($signature) || $signature === '') {
                continue;
            }

            if (hash_equals($expected, trim($signature))) {
                return true;
            }
        }

        return false;
    }

    private function resolveStripeWebhookSecret(): string
    {
        $settingsSecret = (string) Settings::get('webhook_settings.stripe_secret', '');
        if ($settingsSecret !== '') {
            return $settingsSecret;
        }

        if (defined('LICENZO_STRIPE_WEBHOOK_SECRET') && (string) LICENZO_STRIPE_WEBHOOK_SECRET !== '') {
            return (string) LICENZO_STRIPE_WEBHOOK_SECRET;
        }

        if (defined('LICENZO_WEBHOOK_STRIPE_SECRET') && (string) LICENZO_WEBHOOK_STRIPE_SECRET !== '') {
            return (string) LICENZO_WEBHOOK_STRIPE_SECRET;
        }

        $wooStripeSettings = get_option('woocommerce_stripe_settings', []);
        if (!is_array($wooStripeSettings)) {
            return '';
        }

        $isTest = isset($wooStripeSettings['testmode'])
            && in_array($wooStripeSettings['testmode'], ['yes', '1', 1, true], true);

        $candidateKey = $isTest ? 'test_webhook_secret' : 'webhook_secret';
        $candidate = (string) ($wooStripeSettings[$candidateKey] ?? '');

        return trim($candidate);
    }

    /**
     * @return array{timestamp:int,v1:array<int,string>}
     */
    private function parseStripeSignatureHeader(string $header): array
    {
        $parsed = [
            'timestamp' => 0,
            'v1' => [],
        ];

        foreach (explode(',', $header) as $part) {
            $part = trim($part);
            if ($part === '' || !str_contains($part, '=')) {
                continue;
            }

            [$key, $value] = explode('=', $part, 2);
            $key = trim($key);
            $value = trim($value);

            if ($key === 't' && is_numeric($value)) {
                $parsed['timestamp'] = (int) $value;
                continue;
            }

            if ($key === 'v1' && $value !== '') {
                $parsed['v1'][] = $value;
            }
        }

        return $parsed;
    }

    /**
     * @param array<string,mixed> $headers
     * @param array<int,string> $candidates
     */
    private function extractHeader(array $headers, array $candidates): string
    {
        foreach ($candidates as $candidate) {
            if (!array_key_exists($candidate, $headers)) {
                continue;
            }

            $value = $headers[$candidate];
            if (is_array($value)) {
                $value = reset($value);
            }

            $value = trim((string) $value);
            if ($value !== '') {
                return $value;
            }
        }

        return '';
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function createWebhookLog(Request $request, string $eventType, string $eventId, array $payload): ?WebhookLogModel
    {
        $log = WebhookLogLogger::log(
            WebhookLogModel::PROVIDER_STRIPE,
            $eventType,
            $payload,
            $request,
            $eventId !== '' ? $eventId : null
        );

        if ($log) {
            return $log;
        }

        try {
            return WebhookLogModel::create([
                'provider' => WebhookLogModel::PROVIDER_STRIPE,
                'event_type' => $eventType,
                'event_id' => $eventId !== '' ? $eventId : null,
                'payload' => $payload,
                'headers' => is_array($request->header()) ? $request->header() : [],
                'status' => WebhookLogModel::STATUS_PENDING,
            ]);
        } catch (\Throwable $e) {
            return null;
        }
    }
}
