<?php
/**
 * BookiFlex Stripe Payment Extension
 *
 * @package    BookiFlex
 * @subpackage StripePayment
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Extensions\StripePayment\Service;

use BookiFlex\Core\Contract\AccommodationType\AccommodationTypeRepositoryInterface;
use BookiFlex\Core\Model\AccommodationType\Accommodation\AccommodationType;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Reservation\ReservationAccommodationItem;
use BookiFlex\Core\Model\Reservation\ReservationExtraItem;
use BookiFlex\Extensions\Payment\Model\Payment;
use BookiFlex\Extensions\StripePayment\ValueObject\AccommodationGroup;
use BookiFlex\Extensions\StripePayment\ValueObject\StripeLineItem;
use Psr\Log\LoggerInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Builds Stripe Checkout Session line items from reservation data.
 *
 * This service transforms reservation accommodation and extra items into
 * Stripe-compatible line_items array with detailed product information,
 * images, and metadata.
 *
 * @package BookiFlex\Extensions\StripePayment\Pro\Service
 */
final class StripeLineItemBuilder
{
    /**
     * Create a new line item builder.
     *
     * @param AccommodationTypeRepositoryInterface $accommodationTypeRepository Repository for loading accommodation types
     * @param LoggerInterface|null $logger Logger instance
     */
    public function __construct(
        private readonly AccommodationTypeRepositoryInterface $accommodationTypeRepository,
        private readonly ?LoggerInterface $logger = null
    ) {
    }

    /**
     * Build line items from payment allocations.
     *
     * Iterates through all payment allocations and creates line items
     * for both accommodation and extra service items.
     *
     * @param Payment $payment Payment object with allocations to reservations
     * @return array<array<string, mixed>> Array of line items ready for Stripe API
     */
    public function buildLineItems(Payment $payment): array
    {
        $lineItems = [];

        foreach ($payment->getPaymentAllocations() as $allocation) {
            $reservation = $allocation->getReservation();

            // Build accommodation line items (grouped by type)
            foreach ($this->buildAccommodationLineItems($reservation) as $item) {
                $lineItems[] = $item->toArray();
            }

            // Build extra service line items (individual)
            foreach ($this->buildExtraItemLineItems($reservation) as $item) {
                $lineItems[] = $item->toArray();
            }
        }

        return $lineItems;
    }

    /**
     * Build line items for accommodation items.
     *
     * Groups accommodation items by type and creates a single line item
     * per accommodation type with quantity=1 and total stay price.
     * Number of nights is shown in the description.
     *
     * @param Reservation $reservation Reservation to process
     * @return StripeLineItem[] Array of line item value objects
     */
    private function buildAccommodationLineItems(Reservation $reservation): array
    {
        $accommodationItems = $reservation->getAccommodationItems();
        if (empty($accommodationItems)) {
            return [];
        }

        // Group items by accommodation type code
        $groups = $this->groupAccommodationItems($accommodationItems);

        // Load all accommodation types in one query
        $accommodationTypes = $this->loadAccommodationTypes(array_keys($groups));

        $lineItems = [];
        foreach ($groups as $code => $group) {
            $accommodationType = $accommodationTypes[$code] ?? null;

            $lineItems[] = new StripeLineItem(
                name: $group->getAccommodationTypeName(),
                quantity: 1, // Single reservation item
                currency: strtolower($group->getCurrency()),
                unitAmountCents: $group->getTotalAmountCents(), // Total stay price
                description: $this->buildAccommodationDescription($group, $reservation),
                images: $this->extractImageUrls($accommodationType),
                metadata: [
                    'type' => 'accommodation',
                    'accommodation_type_code' => $code,
                    'adults' => (string) $group->getAdults(),
                    'children' => (string) count($group->getChildren()),
                    'check_in' => $group->getCheckInDate()->format('Y-m-d'),
                    'check_out' => $group->getCheckOutDate()->format('Y-m-d'),
                ]
            );
        }

        return $lineItems;
    }

    /**
     * Build line items for extra service items.
     *
     * Creates a separate line item for each extra service (breakfast, parking, etc.).
     * Skips neutral (complimentary) items.
     *
     * @param Reservation $reservation Reservation to process
     * @return StripeLineItem[] Array of line item value objects
     */
    private function buildExtraItemLineItems(Reservation $reservation): array
    {
        $extraItems = $reservation->getExtraItems();
        if (empty($extraItems)) {
            return [];
        }

        $lineItems = [];

        foreach ($extraItems as $item) {
            // Skip neutral (complimentary) items
            if ($item->isNeutral()) {
                continue;
            }

            $lineItems[] = new StripeLineItem(
                name: $item->getName(),
                quantity: $item->getQuantity(),
                currency: strtolower($item->getTotalPrice()->getCurrency()->getCurrencyCode()),
                unitAmountCents: $item->getUnitPrice()->getMinorAmount()->toInt(),
                description: $this->buildExtraItemDescription($item, $reservation),
                images: [], // No images for services
                metadata: [
                    'type' => 'extra',
                    'code' => $item->getCode(),
                    'refundable' => $item->isRefundable() ? 'yes' : 'no',
                ]
            );
        }

        return $lineItems;
    }

    /**
     * Group accommodation items by type code.
     *
     * @param array<ReservationAccommodationItem> $items Items to group
     * @return array<string, AccommodationGroup> Grouped items indexed by accommodation type code
     */
    private function groupAccommodationItems(array $items): array
    {
        $groups = [];

        foreach ($items as $item) {
            $code = $item->getAccommodationTypeCode();

            if (!isset($groups[$code])) {
                $groups[$code] = new AccommodationGroup($code, $item->getName());
            }

            $groups[$code]->addItem($item);
        }

        return $groups;
    }

    /**
     * Load accommodation types by their short codes.
     *
     * Performs bulk loading to avoid N+1 query problem.
     *
     * @param array<string> $codes Short codes to load
     * @return array<string, AccommodationType> Accommodation types indexed by short code
     */
    private function loadAccommodationTypes(array $codes): array
    {
        if (empty($codes)) {
            return [];
        }

        // Load all accommodation types (Cycle ORM will cache them)
        $allTypes = $this->accommodationTypeRepository->findAll(['onlyActive' => false]);

        $result = [];
        foreach ($allTypes as $type) {
            $shortCode = $type->getShortCode();
            if (in_array($shortCode, $codes, true)) {
                $result[$shortCode] = $type;
            }
        }

        return $result;
    }

    /**
     * Extract image URLs from accommodation type.
     *
     * Returns thumbnail URL if available and publicly accessible.
     * Skips localhost URLs as they won't work with Stripe.
     *
     * @param AccommodationType|null $accommodationType Accommodation type or null if not found
     * @return array<string> Array of image URLs (empty if none available)
     */
    private function extractImageUrls(?AccommodationType $accommodationType): array
    {
        if ($accommodationType === null) {
            return [];
        }

        $thumbnail = $accommodationType->getThumbnail();

        // Thumbnail structure: ['id' => 123, 'url' => 'https://...', 'mime' => 'image/jpeg']
        if (empty($thumbnail['url'])) {
            return [];
        }

        $url = $thumbnail['url'];

        // Ensure URL is publicly accessible (not localhost in development)
        if (!$this->isPublicUrl($url)) {
            $this->logger?->warning('Stripe: Thumbnail URL is not publicly accessible', [
                'url' => $url,
                'accommodation_type' => $accommodationType->getName()
            ]);
            return [];
        }

        return [$url];
    }

    /**
     * Check if URL is publicly accessible.
     *
     * @param string $url URL to check
     * @return bool True if URL is public (not localhost)
     */
    private function isPublicUrl(string $url): bool
    {
        // Check if URL is absolute
        if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
            return false;
        }

        // Check if URL is not localhost
        return !str_contains($url, 'localhost') && !str_contains($url, '127.0.0.1');
    }

    /**
     * Build description for accommodation line item.
     *
     * Format: "3 Nights • Check-in: Jan 10, 2025, Check-out: Jan 13, 2025 • 2 Adults • Booking #12345"
     *
     * @param AccommodationGroup $group Grouped accommodation items
     * @param Reservation $reservation Parent reservation
     * @return string Formatted description
     */
    private function buildAccommodationDescription(
        AccommodationGroup $group,
        Reservation $reservation
    ): string {
        $parts = [];

        // Nights count (since quantity is now 1, we show nights in description)
        $nightsCount = $group->getQuantity();
        $parts[] = sprintf(
            /* translators: %d: number of nights */
            _n('%d night', '%d nights', $nightsCount, 'bookiflex'),
            $nightsCount
        );

        // Date range
        $parts[] = sprintf(
            /* translators: %1$s: check-in date, %2$s: check-out date */
            __('Check-in: %1$s, Check-out: %2$s', 'bookiflex'),
            $group->getCheckInDate()->format('M d, Y'),
            $group->getCheckOutDate()->format('M d, Y')
        );

        // Guest information
        $guestParts = [];
        if ($group->getAdults() > 0) {
            $adultsCount = $group->getAdults();
            $guestParts[] = sprintf(
                /* translators: %d: number of adults */
                _n('%d Adult', '%d Adults', $adultsCount, 'bookiflex'),
                $adultsCount
            );
        }
        if (count($group->getChildren()) > 0) {
            $childrenCount = count($group->getChildren());
            $guestParts[] = sprintf(
                /* translators: %d: number of children */
                _n('%d Child', '%d Children', $childrenCount, 'bookiflex'),
                $childrenCount
            );
        }

        if (!empty($guestParts)) {
            $parts[] = implode(', ', $guestParts);
        }

        // Reservation number
        $parts[] = sprintf(
            /* translators: %s: booking/reservation number */
			__('Booking #%s', 'bookiflex'),
			$reservation->getNumber());

        $description = implode(' • ', $parts);

        // Limit description to 500 characters (Stripe limit)
        if (strlen($description) > 500) {
            $description = substr($description, 0, 497) . '...';
        }

        return $description;
    }

    /**
     * Build description for extra item line item.
     *
     * Format: "Date: Jan 10, 2025 • Booking #12345" or "For entire stay • Booking #12345"
     *
     * @param ReservationExtraItem $item Extra service item
     * @param Reservation $reservation Parent reservation
     * @return string Formatted description
     */
    private function buildExtraItemDescription(
        ReservationExtraItem $item,
        Reservation $reservation
    ): string {
        $parts = [];

        // Date information
        if ($item->getDate() !== null) {
            $parts[] = sprintf(
                /* translators: %s: date in M d, Y format */
                __('Date: %s', 'bookiflex'),
                $item->getDate()->format('M d, Y')
            );
        } else {
            $parts[] = __('For entire stay', 'bookiflex');
        }

        // Reservation number
        $parts[] = sprintf(
            /* translators: %s: booking/reservation number */
			__('Booking #%s', 'bookiflex'),
			$reservation->getNumber());

        $description = implode(' • ', $parts);

        // Limit description to 500 characters
        if (strlen($description) > 500) {
            $description = substr($description, 0, 497) . '...';
        }

        return $description;
    }
}
